Repository: apolloconfig/apollo Branch: master Commit: 4c527a469866 Files: 1211 Total size: 6.3 MB Directory structure: gitextract_66_nu3aa/ ├── .gitattributes ├── .github/ │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report_en.md │ │ ├── bug_report_zh.md │ │ ├── feature_request_en.md │ │ └── feature_request_zh.md │ ├── PULL_REQUEST_TEMPLATE.md │ ├── aw/ │ │ └── actions-lock.json │ ├── stale.yml │ └── workflows/ │ ├── build.yml │ ├── cla.yml │ ├── code-style-check.yml │ ├── codeql.yml │ ├── commit_lint.yml │ ├── docker-publish.yml │ ├── docker-validation.yml │ ├── issue-triage.lock.yml │ ├── issue-triage.md │ ├── javascript-test.yml │ ├── license.yml │ ├── portal-login-e2e.yml │ ├── portal-ui-e2e.yml │ └── release-packages.yml ├── .gitignore ├── .licenserc.yaml ├── .mergify.yml ├── .mvn/ │ └── wrapper/ │ ├── MavenWrapperDownloader.java │ └── maven-wrapper.properties ├── AGENTS.md ├── CHANGES.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── GOVERNANCE.md ├── LICENSE ├── README.md ├── SECURITY.md ├── apollo-adminservice/ │ ├── pom.xml │ └── src/ │ ├── assembly/ │ │ └── assembly-descriptor.xml │ ├── main/ │ │ ├── docker/ │ │ │ └── Dockerfile │ │ ├── java/ │ │ │ └── com/ │ │ │ └── ctrip/ │ │ │ └── framework/ │ │ │ └── apollo/ │ │ │ └── adminservice/ │ │ │ ├── AdminServiceApplication.java │ │ │ ├── AdminServiceAssemblyConfiguration.java │ │ │ ├── AdminServiceAutoConfiguration.java │ │ │ ├── AdminServiceHealthIndicator.java │ │ │ ├── ServletInitializer.java │ │ │ ├── aop/ │ │ │ │ ├── NamespaceAcquireLockAspect.java │ │ │ │ ├── NamespaceUnlockAspect.java │ │ │ │ └── PreAcquireNamespaceLock.java │ │ │ ├── controller/ │ │ │ │ ├── AccessKeyController.java │ │ │ │ ├── AppController.java │ │ │ │ ├── AppNamespaceController.java │ │ │ │ ├── ClusterController.java │ │ │ │ ├── CommitController.java │ │ │ │ ├── IndexController.java │ │ │ │ ├── InstanceConfigController.java │ │ │ │ ├── ItemController.java │ │ │ │ ├── ItemSetController.java │ │ │ │ ├── NamespaceBranchController.java │ │ │ │ ├── NamespaceController.java │ │ │ │ ├── NamespaceLockController.java │ │ │ │ ├── ReleaseController.java │ │ │ │ ├── ReleaseHistoryController.java │ │ │ │ └── ServerConfigController.java │ │ │ └── filter/ │ │ │ └── AdminServiceAuthenticationFilter.java │ │ ├── resources/ │ │ │ ├── adminservice.properties │ │ │ ├── apollo-adminservice.conf │ │ │ ├── application-consul-discovery.properties │ │ │ ├── application-custom-defined-discovery.properties │ │ │ ├── application-database-discovery.properties │ │ │ ├── application-github.properties │ │ │ ├── application-kubernetes.properties │ │ │ ├── application-nacos-discovery.properties │ │ │ ├── application-zookeeper-discovery.properties │ │ │ ├── application.properties │ │ │ ├── application.yml │ │ │ └── logback.xml │ │ └── scripts/ │ │ ├── shutdown.sh │ │ └── startup.sh │ └── test/ │ ├── java/ │ │ └── com/ │ │ └── ctrip/ │ │ └── framework/ │ │ └── apollo/ │ │ ├── AdminServiceTestConfiguration.java │ │ ├── LocalAdminServiceApplication.java │ │ └── adminservice/ │ │ ├── GracefulShutdownConfigurationTest.java │ │ ├── aop/ │ │ │ ├── NamespaceLockTest.java │ │ │ └── NamespaceUnlockAspectTest.java │ │ ├── controller/ │ │ │ ├── AbstractControllerTest.java │ │ │ ├── AppControllerTest.java │ │ │ ├── AppNamespaceControllerTest.java │ │ │ ├── ClusterControllerTest.java │ │ │ ├── ControllerExceptionTest.java │ │ │ ├── ControllerIntegrationExceptionTest.java │ │ │ ├── InstanceConfigControllerTest.java │ │ │ ├── ItemControllerTest.java │ │ │ ├── ItemSetControllerTest.java │ │ │ ├── NamespaceControllerTest.java │ │ │ ├── ReleaseControllerTest.java │ │ │ ├── ServerConfigControllerTest.java │ │ │ └── TestWebSecurityConfig.java │ │ └── filter/ │ │ ├── AdminServiceAuthenticationFilterTest.java │ │ └── AdminServiceAuthenticationIntegrationTest.java │ └── resources/ │ ├── application.properties │ ├── application.yml │ ├── controller/ │ │ ├── cleanup.sql │ │ ├── test-itemset.sql │ │ ├── test-release.sql │ │ └── test-server-config.sql │ ├── data.sql │ ├── filter/ │ │ ├── test-access-control-disabled.sql │ │ ├── test-access-control-enabled-no-token.sql │ │ └── test-access-control-enabled.sql │ ├── import.sql │ └── logback-test.xml ├── apollo-assembly/ │ ├── pom.xml │ └── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── ctrip/ │ │ │ └── framework/ │ │ │ └── apollo/ │ │ │ └── assembly/ │ │ │ └── ApolloApplication.java │ │ └── resources/ │ │ ├── application-database-discovery.properties │ │ ├── application-github.properties │ │ ├── application.properties │ │ ├── application.yml │ │ └── logback.xml │ └── test/ │ ├── java/ │ │ └── com/ │ │ └── ctrip/ │ │ └── framework/ │ │ └── apollo/ │ │ └── assembly/ │ │ └── LocalApolloApplication.java │ └── resources/ │ ├── application.properties │ ├── application.yml │ └── logback-test.xml ├── apollo-audit/ │ ├── README.md │ ├── apollo-audit-annotation/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ └── java/ │ │ └── com/ │ │ └── ctrip/ │ │ └── framework/ │ │ └── apollo/ │ │ └── audit/ │ │ └── annotation/ │ │ ├── ApolloAuditLog.java │ │ ├── ApolloAuditLogDataInfluence.java │ │ ├── ApolloAuditLogDataInfluenceTable.java │ │ ├── ApolloAuditLogDataInfluenceTableField.java │ │ └── OpType.java │ ├── apollo-audit-api/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ └── java/ │ │ └── com/ │ │ └── ctrip/ │ │ └── framework/ │ │ └── apollo/ │ │ └── audit/ │ │ ├── api/ │ │ │ ├── ApolloAuditLogApi.java │ │ │ ├── ApolloAuditLogQueryApi.java │ │ │ └── ApolloAuditLogRecordApi.java │ │ ├── dto/ │ │ │ ├── ApolloAuditLogDTO.java │ │ │ ├── ApolloAuditLogDataInfluenceDTO.java │ │ │ └── ApolloAuditLogDetailsDTO.java │ │ └── event/ │ │ └── ApolloAuditLogDataInfluenceEvent.java │ ├── apollo-audit-impl/ │ │ ├── pom.xml │ │ └── src/ │ │ ├── main/ │ │ │ └── java/ │ │ │ └── com/ │ │ │ └── ctrip/ │ │ │ └── framework/ │ │ │ └── apollo/ │ │ │ └── audit/ │ │ │ ├── ApolloAuditProperties.java │ │ │ ├── ApolloAuditRegistrar.java │ │ │ ├── aop/ │ │ │ │ └── ApolloAuditSpanAspect.java │ │ │ ├── component/ │ │ │ │ ├── ApolloAuditHttpInterceptor.java │ │ │ │ ├── ApolloAuditLogApiJpaImpl.java │ │ │ │ └── ApolloAuditLogApiNoOpImpl.java │ │ │ ├── constants/ │ │ │ │ └── ApolloAuditConstants.java │ │ │ ├── context/ │ │ │ │ ├── ApolloAuditScope.java │ │ │ │ ├── ApolloAuditScopeManager.java │ │ │ │ ├── ApolloAuditSpan.java │ │ │ │ ├── ApolloAuditSpanContext.java │ │ │ │ ├── ApolloAuditTraceContext.java │ │ │ │ └── ApolloAuditTracer.java │ │ │ ├── controller/ │ │ │ │ └── ApolloAuditController.java │ │ │ ├── entity/ │ │ │ │ ├── ApolloAuditLog.java │ │ │ │ ├── ApolloAuditLogDataInfluence.java │ │ │ │ └── BaseEntity.java │ │ │ ├── listener/ │ │ │ │ └── ApolloAuditLogDataInfluenceEventListener.java │ │ │ ├── repository/ │ │ │ │ ├── ApolloAuditLogDataInfluenceRepository.java │ │ │ │ └── ApolloAuditLogRepository.java │ │ │ ├── service/ │ │ │ │ ├── ApolloAuditLogDataInfluenceService.java │ │ │ │ └── ApolloAuditLogService.java │ │ │ ├── spi/ │ │ │ │ ├── ApolloAuditLogQueryApiPreAuthorizer.java │ │ │ │ ├── ApolloAuditOperatorSupplier.java │ │ │ │ └── defaultimpl/ │ │ │ │ ├── ApolloAuditLogQueryApiDefaultPreAuthorizer.java │ │ │ │ └── ApolloAuditOperatorDefaultSupplier.java │ │ │ └── util/ │ │ │ └── ApolloAuditUtil.java │ │ └── test/ │ │ └── java/ │ │ └── com/ │ │ └── ctrip/ │ │ └── framework/ │ │ └── apollo/ │ │ └── audit/ │ │ ├── MockBeanFactory.java │ │ ├── MockDataInfluenceEntity.java │ │ ├── aop/ │ │ │ └── ApolloAuditSpanAspectTest.java │ │ ├── component/ │ │ │ ├── ApolloAuditHttpInterceptorTest.java │ │ │ ├── ApolloAuditLogApiJpaImplTest.java │ │ │ └── ApolloAuditScopeManagerTest.java │ │ ├── context/ │ │ │ ├── ApolloAuditTraceContextTest.java │ │ │ └── ApolloAuditTracerTest.java │ │ ├── controller/ │ │ │ └── ApolloAuditControllerTest.java │ │ └── spi/ │ │ └── ApolloAuditOperatorSupplierTest.java │ ├── apollo-audit-spring-boot-starter/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── ctrip/ │ │ │ └── framework/ │ │ │ └── apollo/ │ │ │ └── audit/ │ │ │ └── configuration/ │ │ │ ├── ApolloAuditAutoConfiguration.java │ │ │ └── ApolloAuditNoOpAutoConfiguration.java │ │ └── resources/ │ │ └── META-INF/ │ │ ├── spring/ │ │ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports │ │ └── spring.factories │ └── pom.xml ├── apollo-biz/ │ ├── pom.xml │ └── src/ │ ├── main/ │ │ └── java/ │ │ └── com/ │ │ └── ctrip/ │ │ └── framework/ │ │ └── apollo/ │ │ └── biz/ │ │ ├── ApolloBizAssemblyConfiguration.java │ │ ├── ApolloBizConfig.java │ │ ├── auth/ │ │ │ └── WebSecurityConfig.java │ │ ├── config/ │ │ │ └── BizConfig.java │ │ ├── entity/ │ │ │ ├── AccessKey.java │ │ │ ├── Audit.java │ │ │ ├── Cluster.java │ │ │ ├── Commit.java │ │ │ ├── GrayReleaseRule.java │ │ │ ├── Instance.java │ │ │ ├── InstanceConfig.java │ │ │ ├── Item.java │ │ │ ├── JpaMapFieldJsonConverter.java │ │ │ ├── Namespace.java │ │ │ ├── NamespaceLock.java │ │ │ ├── Privilege.java │ │ │ ├── Release.java │ │ │ ├── ReleaseHistory.java │ │ │ ├── ReleaseMessage.java │ │ │ ├── ServerConfig.java │ │ │ └── ServiceRegistry.java │ │ ├── eureka/ │ │ │ └── ApolloEurekaClientConfig.java │ │ ├── grayReleaseRule/ │ │ │ ├── GrayReleaseRuleCache.java │ │ │ └── GrayReleaseRulesHolder.java │ │ ├── message/ │ │ │ ├── DatabaseMessageSender.java │ │ │ ├── MessageSender.java │ │ │ ├── ReleaseMessageListener.java │ │ │ ├── ReleaseMessageScanner.java │ │ │ └── Topics.java │ │ ├── registry/ │ │ │ ├── DatabaseDiscoveryClient.java │ │ │ ├── DatabaseDiscoveryClientAlwaysAddSelfInstanceDecoratorImpl.java │ │ │ ├── DatabaseDiscoveryClientImpl.java │ │ │ ├── DatabaseDiscoveryClientMemoryCacheDecoratorImpl.java │ │ │ ├── DatabaseServiceRegistry.java │ │ │ ├── DatabaseServiceRegistryImpl.java │ │ │ ├── ServiceInstance.java │ │ │ ├── configuration/ │ │ │ │ ├── ApolloServiceDiscoveryAutoConfiguration.java │ │ │ │ ├── ApolloServiceRegistryAutoConfiguration.java │ │ │ │ └── support/ │ │ │ │ ├── ApolloServiceDiscoveryProperties.java │ │ │ │ ├── ApolloServiceRegistryClearApplicationRunner.java │ │ │ │ ├── ApolloServiceRegistryDeregisterApplicationListener.java │ │ │ │ ├── ApolloServiceRegistryHeartbeatApplicationRunner.java │ │ │ │ └── ApolloServiceRegistryProperties.java │ │ │ └── package-info.java │ │ ├── repository/ │ │ │ ├── AccessKeyRepository.java │ │ │ ├── AppNamespaceRepository.java │ │ │ ├── AppRepository.java │ │ │ ├── AuditRepository.java │ │ │ ├── ClusterRepository.java │ │ │ ├── CommitRepository.java │ │ │ ├── GrayReleaseRuleRepository.java │ │ │ ├── InstanceConfigRepository.java │ │ │ ├── InstanceRepository.java │ │ │ ├── ItemRepository.java │ │ │ ├── NamespaceLockRepository.java │ │ │ ├── NamespaceRepository.java │ │ │ ├── PrivilegeRepository.java │ │ │ ├── ReleaseHistoryRepository.java │ │ │ ├── ReleaseMessageRepository.java │ │ │ ├── ReleaseRepository.java │ │ │ ├── ServerConfigRepository.java │ │ │ └── ServiceRegistryRepository.java │ │ ├── service/ │ │ │ ├── AccessKeyService.java │ │ │ ├── AdminService.java │ │ │ ├── AppNamespaceService.java │ │ │ ├── AppService.java │ │ │ ├── AuditService.java │ │ │ ├── BizDBPropertySource.java │ │ │ ├── ClusterService.java │ │ │ ├── CommitService.java │ │ │ ├── InstanceService.java │ │ │ ├── ItemService.java │ │ │ ├── ItemSetService.java │ │ │ ├── NamespaceBranchService.java │ │ │ ├── NamespaceLockService.java │ │ │ ├── NamespaceService.java │ │ │ ├── ReleaseHistoryService.java │ │ │ ├── ReleaseMessageService.java │ │ │ ├── ReleaseService.java │ │ │ ├── ServerConfigService.java │ │ │ └── ServiceRegistryService.java │ │ └── utils/ │ │ ├── ConfigChangeContentBuilder.java │ │ ├── EntityManagerUtil.java │ │ ├── ReleaseKeyGenerator.java │ │ └── ReleaseMessageKeyGenerator.java │ └── test/ │ ├── java/ │ │ └── com/ │ │ └── ctrip/ │ │ └── framework/ │ │ └── apollo/ │ │ └── biz/ │ │ ├── AbstractIntegrationTest.java │ │ ├── AbstractUnitTest.java │ │ ├── BizTestConfiguration.java │ │ ├── MockBeanFactory.java │ │ ├── config/ │ │ │ └── BizConfigTest.java │ │ ├── entity/ │ │ │ └── JpaMapFieldJsonConverterTest.java │ │ ├── grayReleaseRule/ │ │ │ └── GrayReleaseRulesHolderTest.java │ │ ├── message/ │ │ │ ├── DatabaseMessageSenderTest.java │ │ │ └── ReleaseMessageScannerTest.java │ │ ├── registry/ │ │ │ ├── DatabaseDiscoveryClientAlwaysAddSelfInstanceDecoratorImplTest.java │ │ │ ├── DatabaseDiscoveryClientImplTest.java │ │ │ ├── DatabaseDiscoveryClientMemoryCacheDecoratorImplTest.java │ │ │ ├── DatabaseDiscoveryIntegrationTest.java │ │ │ ├── DatabaseDiscoveryWithoutDecoratorIntegrationTest.java │ │ │ ├── ServiceInstanceFactory.java │ │ │ └── configuration/ │ │ │ ├── ApolloServiceRegistryAutoConfigurationNotEnabledTest.java │ │ │ └── support/ │ │ │ └── ApolloServiceRegistryClearApplicationRunnerIntegrationTest.java │ │ ├── repository/ │ │ │ ├── AccessKeyRepositoryTest.java │ │ │ ├── AppNamespaceRepositoryTest.java │ │ │ ├── AppRepositoryTest.java │ │ │ ├── InstanceConfigRepositoryTest.java │ │ │ └── ReleaseHistoryRepositoryTest.java │ │ ├── service/ │ │ │ ├── AccessKeyServiceTest.java │ │ │ ├── AdminServiceTest.java │ │ │ ├── AdminServiceTransactionTest.java │ │ │ ├── BizDBPropertySourceTest.java │ │ │ ├── ClusterServiceTest.java │ │ │ ├── InstanceServiceTest.java │ │ │ ├── ItemServiceTest.java │ │ │ ├── ItemSetServiceTest.java │ │ │ ├── NamespaceBranchServiceTest.java │ │ │ ├── NamespacePublishInfoTest.java │ │ │ ├── NamespaceServiceIntegrationTest.java │ │ │ ├── NamespaceServiceTest.java │ │ │ ├── ReleaseCreationTest.java │ │ │ ├── ReleaseHistoryServiceTest.java │ │ │ ├── ReleaseServiceTest.java │ │ │ └── ServerConfigServiceTest.java │ │ └── utils/ │ │ ├── ConfigChangeContentBuilderTest.java │ │ └── ReleaseKeyGeneratorTest.java │ └── resources/ │ ├── application.properties │ ├── data.sql │ ├── import.sql │ ├── json/ │ │ └── converter/ │ │ ├── element.1.json │ │ └── element.2.json │ ├── logback-test.xml │ └── sql/ │ ├── accesskey-test.sql │ ├── clean.sql │ ├── item-test.sql │ ├── itemset-test.sql │ ├── namespace-branch-test.sql │ ├── namespace-test.sql │ ├── release-creation-test.sql │ └── release-history-test.sql ├── apollo-build-sql-converter/ │ ├── pom.xml │ └── src/ │ ├── main/ │ │ └── java/ │ │ └── com/ │ │ └── ctrip/ │ │ └── framework/ │ │ └── apollo/ │ │ └── build/ │ │ └── sql/ │ │ └── converter/ │ │ ├── ApolloH2ConverterUtil.java │ │ ├── ApolloMysqlDefaultConverterUtil.java │ │ ├── ApolloSqlConverter.java │ │ ├── ApolloSqlConverterUtil.java │ │ ├── SqlStatement.java │ │ ├── SqlTemplate.java │ │ ├── SqlTemplateContext.java │ │ └── SqlTemplateGist.java │ └── test/ │ ├── java/ │ │ └── com/ │ │ └── ctrip/ │ │ └── framework/ │ │ └── apollo/ │ │ └── build/ │ │ └── sql/ │ │ └── converter/ │ │ ├── ApolloSqlConverterAutoGeneratedTest.java │ │ ├── ApolloSqlConverterH2Test.java │ │ └── TestH2Function.java │ └── resources/ │ └── META-INF/ │ └── sql/ │ └── h2-test/ │ └── delta/ │ ├── v000-v010/ │ │ ├── apolloconfigdb-v000-v010-after.sql │ │ ├── apolloconfigdb-v000-v010-base.sql │ │ ├── apolloconfigdb-v000-v010-before.sql │ │ ├── apolloconfigdb-v000-v010.sql │ │ ├── apolloportaldb-v000-v010-base.sql │ │ └── apolloportaldb-v000-v010.sql │ ├── v040-v050/ │ │ ├── apolloconfigdb-v040-v050.sql │ │ └── apolloportaldb-v040-v050.sql │ ├── v060-v062/ │ │ ├── apolloconfigdb-v060-v062.sql │ │ └── apolloportaldb-v060-v062.sql │ ├── v080-v090/ │ │ └── apolloportaldb-v080-v090.sql │ ├── v151-v160/ │ │ └── apolloconfigdb-v151-v160.sql │ ├── v170-v180/ │ │ ├── apolloconfigdb-v170-v180.sql │ │ └── apolloportaldb-v170-v180.sql │ ├── v180-v190/ │ │ ├── apolloconfigdb-v180-v190.sql │ │ └── apolloportaldb-v180-v190.sql │ ├── v190-v200/ │ │ ├── apolloconfigdb-v190-v200-after.sql │ │ ├── apolloconfigdb-v190-v200.sql │ │ ├── apolloportaldb-v190-v200-after.sql │ │ └── apolloportaldb-v190-v200.sql │ ├── v200-v210/ │ │ └── apolloconfigdb-v200-v210.sql │ └── v210-v220/ │ ├── apolloconfigdb-v210-v220.sql │ └── apolloportaldb-v210-v220.sql ├── apollo-buildtools/ │ ├── .gitignore │ ├── pom.xml │ ├── src/ │ │ └── main/ │ │ ├── resources/ │ │ │ ├── LICENSE-2.0.txt │ │ │ └── google_checks.xml │ │ └── scripts/ │ │ └── deploy_jenkins.sh │ └── style/ │ ├── eclipse-java-google-style.xml │ ├── intellij-java-google-style.xml │ └── license/ │ └── apollo-license ├── apollo-common/ │ ├── pom.xml │ └── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── ctrip/ │ │ │ └── framework/ │ │ │ └── apollo/ │ │ │ └── common/ │ │ │ ├── ApolloCommonConfig.java │ │ │ ├── aop/ │ │ │ │ └── RepositoryAspect.java │ │ │ ├── condition/ │ │ │ │ ├── ConditionalOnMissingProfile.java │ │ │ │ ├── ConditionalOnProfile.java │ │ │ │ └── OnProfileCondition.java │ │ │ ├── config/ │ │ │ │ ├── RefreshableConfig.java │ │ │ │ └── RefreshablePropertySource.java │ │ │ ├── constants/ │ │ │ │ ├── AccessKeyMode.java │ │ │ │ ├── ApolloServer.java │ │ │ │ ├── GsonType.java │ │ │ │ ├── NamespaceBranchStatus.java │ │ │ │ ├── ReleaseOperation.java │ │ │ │ └── ReleaseOperationContext.java │ │ │ ├── controller/ │ │ │ │ ├── ApolloInfoController.java │ │ │ │ ├── CharacterEncodingFilterConfiguration.java │ │ │ │ ├── GlobalDefaultExceptionHandler.java │ │ │ │ ├── HttpMessageConverterConfiguration.java │ │ │ │ └── WebMvcConfig.java │ │ │ ├── datasource/ │ │ │ │ ├── ApolloDataSourceScriptDatabaseInitializer.java │ │ │ │ ├── ApolloDataSourceScriptDatabaseInitializerFactory.java │ │ │ │ └── ApolloSqlInitializationProperties.java │ │ │ ├── dto/ │ │ │ │ ├── AccessKeyDTO.java │ │ │ │ ├── AppDTO.java │ │ │ │ ├── AppNamespaceDTO.java │ │ │ │ ├── BaseDTO.java │ │ │ │ ├── ClusterDTO.java │ │ │ │ ├── CommitDTO.java │ │ │ │ ├── GrayReleaseRuleDTO.java │ │ │ │ ├── GrayReleaseRuleItemDTO.java │ │ │ │ ├── InstanceConfigDTO.java │ │ │ │ ├── InstanceDTO.java │ │ │ │ ├── ItemChangeSets.java │ │ │ │ ├── ItemDTO.java │ │ │ │ ├── ItemInfoDTO.java │ │ │ │ ├── NamespaceDTO.java │ │ │ │ ├── NamespaceLockDTO.java │ │ │ │ ├── PageDTO.java │ │ │ │ ├── ReleaseDTO.java │ │ │ │ └── ReleaseHistoryDTO.java │ │ │ ├── entity/ │ │ │ │ ├── App.java │ │ │ │ ├── AppNamespace.java │ │ │ │ ├── BaseEntity.java │ │ │ │ └── EntityPair.java │ │ │ ├── exception/ │ │ │ │ ├── AbstractApolloHttpException.java │ │ │ │ ├── BadRequestException.java │ │ │ │ ├── BeanUtilsException.java │ │ │ │ ├── NotFoundException.java │ │ │ │ └── ServiceException.java │ │ │ ├── http/ │ │ │ │ ├── MultiResponseEntity.java │ │ │ │ ├── RichResponseEntity.java │ │ │ │ └── SearchResponseEntity.java │ │ │ ├── jpa/ │ │ │ │ ├── H2Function.java │ │ │ │ └── SqlFunctionsMetadataBuilderContributor.java │ │ │ └── utils/ │ │ │ ├── BeanUtils.java │ │ │ ├── ExceptionUtils.java │ │ │ ├── GrayReleaseRuleItemTransformer.java │ │ │ ├── InputValidator.java │ │ │ ├── RequestPrecondition.java │ │ │ ├── UniqueKeyGenerator.java │ │ │ └── WebUtils.java │ │ └── resources/ │ │ ├── application-h2.properties │ │ ├── application-mysql.properties │ │ ├── application-postgre.properties │ │ ├── application.yaml │ │ └── banner.txt │ └── test/ │ └── java/ │ └── com/ │ └── ctrip/ │ └── framework/ │ └── apollo/ │ └── common/ │ ├── conditional/ │ │ └── ConditionalOnProfileTest.java │ ├── dto/ │ │ └── ItemInfoDTOTest.java │ ├── exception/ │ │ ├── BadRequestExceptionTest.java │ │ └── NotFoundExceptionTest.java │ ├── http/ │ │ └── SearchResponseEntityTest.java │ └── utils/ │ ├── BeanUtilsTest.java │ ├── InputValidatorTest.java │ └── WebUtilsTest.java ├── apollo-configservice/ │ ├── pom.xml │ └── src/ │ ├── assembly/ │ │ └── assembly-descriptor.xml │ ├── main/ │ │ ├── docker/ │ │ │ └── Dockerfile │ │ ├── java/ │ │ │ └── com/ │ │ │ └── ctrip/ │ │ │ └── framework/ │ │ │ └── apollo/ │ │ │ ├── configservice/ │ │ │ │ ├── ConfigServerEurekaServerConfigure.java │ │ │ │ ├── ConfigServiceApplication.java │ │ │ │ ├── ConfigServiceAssemblyConfiguration.java │ │ │ │ ├── ConfigServiceAutoConfiguration.java │ │ │ │ ├── ConfigServiceHealthIndicator.java │ │ │ │ ├── ServletInitializer.java │ │ │ │ ├── controller/ │ │ │ │ │ ├── ConfigController.java │ │ │ │ │ ├── ConfigFileController.java │ │ │ │ │ ├── NotificationController.java │ │ │ │ │ └── NotificationControllerV2.java │ │ │ │ ├── filter/ │ │ │ │ │ └── ClientAuthenticationFilter.java │ │ │ │ ├── service/ │ │ │ │ │ ├── AccessKeyServiceWithCache.java │ │ │ │ │ ├── AppNamespaceServiceWithCache.java │ │ │ │ │ ├── ReleaseMessageServiceWithCache.java │ │ │ │ │ └── config/ │ │ │ │ │ ├── AbstractConfigService.java │ │ │ │ │ ├── ConfigService.java │ │ │ │ │ ├── ConfigServiceWithCache.java │ │ │ │ │ ├── DefaultConfigService.java │ │ │ │ │ ├── DefaultIncrementalSyncService.java │ │ │ │ │ └── IncrementalSyncService.java │ │ │ │ ├── util/ │ │ │ │ │ ├── AccessKeyUtil.java │ │ │ │ │ ├── InstanceConfigAuditUtil.java │ │ │ │ │ ├── NamespaceUtil.java │ │ │ │ │ └── WatchKeysUtil.java │ │ │ │ └── wrapper/ │ │ │ │ ├── CaseInsensitiveMapWrapper.java │ │ │ │ ├── CaseInsensitiveMultimapWrapper.java │ │ │ │ └── DeferredResultWrapper.java │ │ │ └── metaservice/ │ │ │ ├── ApolloMetaServiceConfig.java │ │ │ ├── controller/ │ │ │ │ ├── HomePageController.java │ │ │ │ └── ServiceController.java │ │ │ └── service/ │ │ │ ├── DatabaseDiscoveryService.java │ │ │ ├── DefaultDiscoveryService.java │ │ │ ├── DiscoveryService.java │ │ │ ├── KubernetesDiscoveryService.java │ │ │ ├── NacosDiscoveryService.java │ │ │ └── SpringCloudInnerDiscoveryService.java │ │ ├── resources/ │ │ │ ├── apollo-configservice.conf │ │ │ ├── application-consul-discovery.properties │ │ │ ├── application-custom-defined-discovery.properties │ │ │ ├── application-database-discovery.properties │ │ │ ├── application-github.properties │ │ │ ├── application-kubernetes.properties │ │ │ ├── application-nacos-discovery.properties │ │ │ ├── application-zookeeper-discovery.properties │ │ │ ├── application.properties │ │ │ ├── application.yml │ │ │ ├── configservice.properties │ │ │ ├── jpa/ │ │ │ │ └── configdb.init.h2.sql │ │ │ └── logback.xml │ │ └── scripts/ │ │ ├── shutdown.sh │ │ └── startup.sh │ └── test/ │ ├── java/ │ │ └── com/ │ │ └── ctrip/ │ │ └── framework/ │ │ └── apollo/ │ │ ├── ConfigServiceTestConfiguration.java │ │ ├── LocalConfigServiceApplication.java │ │ ├── configservice/ │ │ │ ├── GracefulShutdownConfigurationTest.java │ │ │ ├── controller/ │ │ │ │ ├── ConfigControllerTest.java │ │ │ │ ├── ConfigFileControllerTest.java │ │ │ │ ├── NotificationControllerTest.java │ │ │ │ ├── NotificationControllerV2Test.java │ │ │ │ └── TestWebSecurityConfig.java │ │ │ ├── filter/ │ │ │ │ └── ClientAuthenticationFilterTest.java │ │ │ ├── integration/ │ │ │ │ ├── AbstractBaseIntegrationTest.java │ │ │ │ ├── ConfigControllerIntegrationTest.java │ │ │ │ ├── ConfigFileControllerIntegrationTest.java │ │ │ │ ├── NotificationControllerIntegrationTest.java │ │ │ │ └── NotificationControllerV2IntegrationTest.java │ │ │ ├── service/ │ │ │ │ ├── AccessKeyServiceWithCacheTest.java │ │ │ │ ├── AppNamespaceServiceWithCacheTest.java │ │ │ │ ├── ReleaseMessageServiceWithCacheTest.java │ │ │ │ └── config/ │ │ │ │ ├── ConfigServiceWithCacheAndCacheKeyIgnoreCaseTest.java │ │ │ │ ├── ConfigServiceWithCacheTest.java │ │ │ │ ├── DefaultConfigServiceTest.java │ │ │ │ └── DefaultIncrementalSyncServiceTest.java │ │ │ ├── util/ │ │ │ │ ├── AccessKeyUtilTest.java │ │ │ │ ├── InstanceConfigAuditUtilTest.java │ │ │ │ ├── NamespaceUtilTest.java │ │ │ │ └── WatchKeysUtilTest.java │ │ │ └── wrapper/ │ │ │ ├── CaseInsensitiveMapWrapperTest.java │ │ │ └── CaseInsensitiveMultimapWrapperTest.java │ │ └── metaservice/ │ │ ├── controller/ │ │ │ ├── HomePageControllerTest.java │ │ │ └── ServiceControllerTest.java │ │ └── service/ │ │ ├── ConsulDiscoveryServiceTest.java │ │ ├── DefaultDiscoveryServiceTest.java │ │ ├── KubernetesDiscoveryServiceTest.java │ │ ├── NacosDiscoveryServiceTest.java │ │ └── ZookeeperDiscoveryServiceTest.java │ └── resources/ │ ├── application.properties │ ├── application.yml │ ├── data.sql │ ├── import.sql │ ├── integration-test/ │ │ ├── cleanup.sql │ │ ├── test-gray-release.sql │ │ ├── test-release-message.sql │ │ ├── test-release-public-dc-override.sql │ │ ├── test-release-public-default-override.sql │ │ └── test-release.sql │ └── logback-test.xml ├── apollo-portal/ │ ├── pom.xml │ └── src/ │ ├── assembly/ │ │ └── assembly-descriptor.xml │ ├── main/ │ │ ├── docker/ │ │ │ └── Dockerfile │ │ ├── java/ │ │ │ └── com/ │ │ │ └── ctrip/ │ │ │ └── framework/ │ │ │ └── apollo/ │ │ │ ├── openapi/ │ │ │ │ ├── PortalOpenApiConfig.java │ │ │ │ ├── auth/ │ │ │ │ │ └── ConsumerPermissionValidator.java │ │ │ │ ├── entity/ │ │ │ │ │ ├── Consumer.java │ │ │ │ │ ├── ConsumerAudit.java │ │ │ │ │ ├── ConsumerRole.java │ │ │ │ │ └── ConsumerToken.java │ │ │ │ ├── filter/ │ │ │ │ │ └── ConsumerAuthenticationFilter.java │ │ │ │ ├── repository/ │ │ │ │ │ ├── ConsumerAuditRepository.java │ │ │ │ │ ├── ConsumerRepository.java │ │ │ │ │ ├── ConsumerRoleRepository.java │ │ │ │ │ └── ConsumerTokenRepository.java │ │ │ │ ├── server/ │ │ │ │ │ └── service/ │ │ │ │ │ ├── AppOpenApiService.java │ │ │ │ │ ├── ClusterOpenApiService.java │ │ │ │ │ ├── EnvOpenApiService.java │ │ │ │ │ ├── OrganizationOpenApiService.java │ │ │ │ │ ├── ServerAppOpenApiService.java │ │ │ │ │ ├── ServerClusterOpenApiService.java │ │ │ │ │ ├── ServerEnvOpenApiService.java │ │ │ │ │ ├── ServerInstanceOpenApiService.java │ │ │ │ │ ├── ServerItemOpenApiService.java │ │ │ │ │ ├── ServerNamespaceOpenApiService.java │ │ │ │ │ ├── ServerOrganizationOpenApiService.java │ │ │ │ │ └── ServerReleaseOpenApiService.java │ │ │ │ ├── service/ │ │ │ │ │ ├── ConsumerRolePermissionService.java │ │ │ │ │ └── ConsumerService.java │ │ │ │ ├── util/ │ │ │ │ │ ├── ConsumerAuditUtil.java │ │ │ │ │ ├── ConsumerAuthUtil.java │ │ │ │ │ ├── OpenApiBeanUtils.java │ │ │ │ │ └── OpenApiModelConverters.java │ │ │ │ └── v1/ │ │ │ │ └── controller/ │ │ │ │ ├── AppController.java │ │ │ │ ├── ClusterController.java │ │ │ │ ├── EnvController.java │ │ │ │ ├── InstanceController.java │ │ │ │ ├── ItemController.java │ │ │ │ ├── NamespaceBranchController.java │ │ │ │ ├── NamespaceController.java │ │ │ │ ├── OrganizationController.java │ │ │ │ └── ReleaseController.java │ │ │ └── portal/ │ │ │ ├── PortalApplication.java │ │ │ ├── PortalAssemblyConfiguration.java │ │ │ ├── ServletInitializer.java │ │ │ ├── api/ │ │ │ │ ├── API.java │ │ │ │ └── AdminServiceAPI.java │ │ │ ├── audit/ │ │ │ │ ├── ApolloAuditLogQueryApiPortalPreAuthorizer.java │ │ │ │ └── ApolloAuditOperatorPortalSupplier.java │ │ │ ├── component/ │ │ │ │ ├── AbstractPermissionValidator.java │ │ │ │ ├── AdminServiceAddressLocator.java │ │ │ │ ├── ConfigReleaseWebhookNotifier.java │ │ │ │ ├── ItemsComparator.java │ │ │ │ ├── PermissionValidator.java │ │ │ │ ├── PortalSettings.java │ │ │ │ ├── RestTemplateFactory.java │ │ │ │ ├── RetryableRestTemplate.java │ │ │ │ ├── UnifiedPermissionValidator.java │ │ │ │ ├── UserIdentityContextHolder.java │ │ │ │ ├── UserPermissionValidator.java │ │ │ │ ├── config/ │ │ │ │ │ ├── PortalConfig.java │ │ │ │ │ └── SpringSessionConfig.java │ │ │ │ ├── emailbuilder/ │ │ │ │ │ ├── ConfigPublishEmailBuilder.java │ │ │ │ │ ├── GrayPublishEmailBuilder.java │ │ │ │ │ ├── MergeEmailBuilder.java │ │ │ │ │ ├── NormalPublishEmailBuilder.java │ │ │ │ │ └── RollbackEmailBuilder.java │ │ │ │ └── txtresolver/ │ │ │ │ ├── ConfigTextResolver.java │ │ │ │ ├── FileTextResolver.java │ │ │ │ └── PropertyResolver.java │ │ │ ├── constant/ │ │ │ │ ├── PermissionType.java │ │ │ │ ├── RoleType.java │ │ │ │ ├── TracerEventType.java │ │ │ │ └── UserIdentityConstants.java │ │ │ ├── controller/ │ │ │ │ ├── AccessKeyController.java │ │ │ │ ├── AppController.java │ │ │ │ ├── ClusterController.java │ │ │ │ ├── CommitController.java │ │ │ │ ├── ConfigsExportController.java │ │ │ │ ├── ConfigsImportController.java │ │ │ │ ├── ConsumerController.java │ │ │ │ ├── EnvController.java │ │ │ │ ├── FavoriteController.java │ │ │ │ ├── GlobalSearchController.java │ │ │ │ ├── InstanceController.java │ │ │ │ ├── ItemController.java │ │ │ │ ├── NamespaceBranchController.java │ │ │ │ ├── NamespaceController.java │ │ │ │ ├── NamespaceLockController.java │ │ │ │ ├── OrganizationController.java │ │ │ │ ├── PageSettingController.java │ │ │ │ ├── PermissionController.java │ │ │ │ ├── PrefixPathController.java │ │ │ │ ├── ReleaseController.java │ │ │ │ ├── ReleaseHistoryController.java │ │ │ │ ├── SearchController.java │ │ │ │ ├── ServerConfigController.java │ │ │ │ ├── SignInController.java │ │ │ │ ├── SsoHeartbeatController.java │ │ │ │ ├── SystemInfoController.java │ │ │ │ └── UserInfoController.java │ │ │ ├── enricher/ │ │ │ │ ├── AdditionalUserInfoEnricher.java │ │ │ │ ├── adapter/ │ │ │ │ │ ├── AppDtoUserInfoEnrichedAdapter.java │ │ │ │ │ ├── BaseDtoUserInfoEnrichedAdapter.java │ │ │ │ │ └── UserInfoEnrichedAdapter.java │ │ │ │ └── impl/ │ │ │ │ └── UserDisplayNameEnricher.java │ │ │ ├── entity/ │ │ │ │ ├── bo/ │ │ │ │ │ ├── ConfigBO.java │ │ │ │ │ ├── Email.java │ │ │ │ │ ├── ItemBO.java │ │ │ │ │ ├── KVEntity.java │ │ │ │ │ ├── NamespaceBO.java │ │ │ │ │ ├── ReleaseBO.java │ │ │ │ │ ├── ReleaseHistoryBO.java │ │ │ │ │ └── UserInfo.java │ │ │ │ ├── model/ │ │ │ │ │ ├── AppModel.java │ │ │ │ │ ├── NamespaceCreationModel.java │ │ │ │ │ ├── NamespaceGrayDelReleaseModel.java │ │ │ │ │ ├── NamespaceReleaseModel.java │ │ │ │ │ ├── NamespaceSyncModel.java │ │ │ │ │ ├── NamespaceTextModel.java │ │ │ │ │ └── Verifiable.java │ │ │ │ ├── po/ │ │ │ │ │ ├── Authority.java │ │ │ │ │ ├── Favorite.java │ │ │ │ │ ├── Permission.java │ │ │ │ │ ├── Role.java │ │ │ │ │ ├── RolePermission.java │ │ │ │ │ ├── ServerConfig.java │ │ │ │ │ ├── UserPO.java │ │ │ │ │ └── UserRole.java │ │ │ │ └── vo/ │ │ │ │ ├── AppRolesAssignedUsers.java │ │ │ │ ├── Change.java │ │ │ │ ├── ClusterNamespaceRolesAssignedUsers.java │ │ │ │ ├── EnvClusterInfo.java │ │ │ │ ├── EnvironmentInfo.java │ │ │ │ ├── ItemDiffs.java │ │ │ │ ├── ItemInfo.java │ │ │ │ ├── LockInfo.java │ │ │ │ ├── NamespaceEnvRolesAssignedUsers.java │ │ │ │ ├── NamespaceIdentifier.java │ │ │ │ ├── NamespaceRolesAssignedUsers.java │ │ │ │ ├── NamespaceUsage.java │ │ │ │ ├── Number.java │ │ │ │ ├── Organization.java │ │ │ │ ├── PageSetting.java │ │ │ │ ├── PermissionCondition.java │ │ │ │ ├── ReleaseCompareResult.java │ │ │ │ ├── SystemInfo.java │ │ │ │ └── consumer/ │ │ │ │ ├── ConsumerCreateRequestVO.java │ │ │ │ └── ConsumerInfo.java │ │ │ ├── enums/ │ │ │ │ └── ChangeType.java │ │ │ ├── environment/ │ │ │ │ ├── DatabasePortalMetaServerProvider.java │ │ │ │ ├── DefaultPortalMetaServerProvider.java │ │ │ │ ├── Env.java │ │ │ │ ├── PortalMetaDomainService.java │ │ │ │ └── PortalMetaServerProvider.java │ │ │ ├── filter/ │ │ │ │ ├── PortalUserSessionFilter.java │ │ │ │ └── UserTypeResolverFilter.java │ │ │ ├── listener/ │ │ │ │ ├── AppCreationEvent.java │ │ │ │ ├── AppDeletionEvent.java │ │ │ │ ├── AppInfoChangedEvent.java │ │ │ │ ├── AppInfoChangedListener.java │ │ │ │ ├── AppNamespaceCreationEvent.java │ │ │ │ ├── AppNamespaceDeletionEvent.java │ │ │ │ ├── ConfigPublishEvent.java │ │ │ │ ├── ConfigPublishListener.java │ │ │ │ ├── CreationListener.java │ │ │ │ └── DeletionListener.java │ │ │ ├── repository/ │ │ │ │ ├── AppNamespaceRepository.java │ │ │ │ ├── AppRepository.java │ │ │ │ ├── AuthorityRepository.java │ │ │ │ ├── FavoriteRepository.java │ │ │ │ ├── PermissionRepository.java │ │ │ │ ├── RolePermissionRepository.java │ │ │ │ ├── RoleRepository.java │ │ │ │ ├── ServerConfigRepository.java │ │ │ │ ├── UserRepository.java │ │ │ │ └── UserRoleRepository.java │ │ │ ├── service/ │ │ │ │ ├── AccessKeyService.java │ │ │ │ ├── AdditionalUserInfoEnrichService.java │ │ │ │ ├── AdditionalUserInfoEnrichServiceImpl.java │ │ │ │ ├── AppNamespaceService.java │ │ │ │ ├── AppService.java │ │ │ │ ├── ClusterService.java │ │ │ │ ├── CommitService.java │ │ │ │ ├── ConfigsExportService.java │ │ │ │ ├── ConfigsImportService.java │ │ │ │ ├── FavoriteService.java │ │ │ │ ├── GlobalSearchService.java │ │ │ │ ├── InstanceService.java │ │ │ │ ├── ItemService.java │ │ │ │ ├── NamespaceBranchService.java │ │ │ │ ├── NamespaceLockService.java │ │ │ │ ├── NamespaceService.java │ │ │ │ ├── PortalDBPropertySource.java │ │ │ │ ├── ReleaseHistoryService.java │ │ │ │ ├── ReleaseService.java │ │ │ │ ├── RoleInitializationService.java │ │ │ │ ├── RolePermissionService.java │ │ │ │ ├── ServerConfigService.java │ │ │ │ └── SystemRoleManagerService.java │ │ │ ├── spi/ │ │ │ │ ├── EmailService.java │ │ │ │ ├── LogoutHandler.java │ │ │ │ ├── MQService.java │ │ │ │ ├── SsoHeartbeatHandler.java │ │ │ │ ├── UserInfoHolder.java │ │ │ │ ├── UserService.java │ │ │ │ ├── configuration/ │ │ │ │ │ ├── AuthConfiguration.java │ │ │ │ │ ├── AuthFilterConfiguration.java │ │ │ │ │ ├── EmailConfiguration.java │ │ │ │ │ ├── LdapExtendProperties.java │ │ │ │ │ ├── LdapGroupProperties.java │ │ │ │ │ ├── LdapMappingProperties.java │ │ │ │ │ ├── LdapProperties.java │ │ │ │ │ ├── MQConfiguration.java │ │ │ │ │ ├── OidcExtendProperties.java │ │ │ │ │ └── RoleConfiguration.java │ │ │ │ ├── defaultimpl/ │ │ │ │ │ ├── DefaultEmailService.java │ │ │ │ │ ├── DefaultLogoutHandler.java │ │ │ │ │ ├── DefaultMQService.java │ │ │ │ │ ├── DefaultRoleInitializationService.java │ │ │ │ │ ├── DefaultRolePermissionService.java │ │ │ │ │ ├── DefaultSsoHeartbeatHandler.java │ │ │ │ │ ├── DefaultUserInfoHolder.java │ │ │ │ │ └── DefaultUserService.java │ │ │ │ ├── ldap/ │ │ │ │ │ ├── ApolloLdapAuthenticationProvider.java │ │ │ │ │ ├── FilterLdapByGroupUserSearch.java │ │ │ │ │ └── LdapUserService.java │ │ │ │ ├── oidc/ │ │ │ │ │ ├── ExcludeClientCredentialsClientRegistrationRepository.java │ │ │ │ │ ├── OidcAuthenticationSuccessEventListener.java │ │ │ │ │ ├── OidcLocalUserService.java │ │ │ │ │ ├── OidcLocalUserServiceImpl.java │ │ │ │ │ ├── OidcLogoutHandler.java │ │ │ │ │ ├── OidcUserInfoHolder.java │ │ │ │ │ ├── OidcUserInfoUtil.java │ │ │ │ │ └── PlaceholderPasswordEncoder.java │ │ │ │ ├── package-info.java │ │ │ │ └── springsecurity/ │ │ │ │ ├── ApolloPasswordEncoderFactory.java │ │ │ │ ├── PasswordEncoderAdapter.java │ │ │ │ ├── SpringSecurityUserInfoHolder.java │ │ │ │ └── SpringSecurityUserService.java │ │ │ └── util/ │ │ │ ├── ConfigFileUtils.java │ │ │ ├── ConfigToFileUtils.java │ │ │ ├── KeyValueUtils.java │ │ │ ├── NamespaceBOUtils.java │ │ │ ├── RelativeDateFormat.java │ │ │ ├── RoleUtils.java │ │ │ └── checker/ │ │ │ ├── AuthUserPasswordChecker.java │ │ │ ├── CheckResult.java │ │ │ └── UserPasswordChecker.java │ │ ├── resources/ │ │ │ ├── apollo-env.properties │ │ │ ├── apollo-portal.conf │ │ │ ├── application-github.properties │ │ │ ├── application-ldap-activedirectory-sample.yml │ │ │ ├── application-ldap-apacheds-sample.yml │ │ │ ├── application-ldap-openldap-sample.yml │ │ │ ├── application-oidc-sample.yml │ │ │ ├── application.properties │ │ │ ├── application.yml │ │ │ ├── jpa/ │ │ │ │ └── portaldb.init.h2.sql │ │ │ ├── logback.xml │ │ │ ├── portal.properties │ │ │ └── static/ │ │ │ ├── app/ │ │ │ │ ├── access_key.html │ │ │ │ ├── manage_cluster.html │ │ │ │ └── setting.html │ │ │ ├── app.html │ │ │ ├── audit_log_menu.html │ │ │ ├── audit_log_trace_detail.html │ │ │ ├── cluster/ │ │ │ │ └── ns_role.html │ │ │ ├── cluster.html │ │ │ ├── config/ │ │ │ │ ├── diff.html │ │ │ │ ├── history.html │ │ │ │ └── sync.html │ │ │ ├── config.html │ │ │ ├── config_export.html │ │ │ ├── default_sso_heartbeat.html │ │ │ ├── delete_app_cluster_namespace.html │ │ │ ├── global_search_value.html │ │ │ ├── i18n/ │ │ │ │ ├── en.json │ │ │ │ └── zh-CN.json │ │ │ ├── index.html │ │ │ ├── login.html │ │ │ ├── namespace/ │ │ │ │ └── role.html │ │ │ ├── namespace.html │ │ │ ├── open/ │ │ │ │ ├── add-consumer.html │ │ │ │ ├── grant-permission-modal.html │ │ │ │ └── manage.html │ │ │ ├── scripts/ │ │ │ │ ├── AppUtils.js │ │ │ │ ├── PageCommon.js │ │ │ │ ├── app.js │ │ │ │ ├── controller/ │ │ │ │ │ ├── AccessKeyController.js │ │ │ │ │ ├── AppController.js │ │ │ │ │ ├── AuditLogMenuController.js │ │ │ │ │ ├── AuditLogTraceDetailController.js │ │ │ │ │ ├── BackTopController.js │ │ │ │ │ ├── ClusterController.js │ │ │ │ │ ├── ConfigExportController.js │ │ │ │ │ ├── DeleteAppClusterNamespaceController.js │ │ │ │ │ ├── GlobalSearchValueController.js │ │ │ │ │ ├── IndexController.js │ │ │ │ │ ├── LoginController.js │ │ │ │ │ ├── ManageClusterController.js │ │ │ │ │ ├── NamespaceController.js │ │ │ │ │ ├── ServerConfigController.js │ │ │ │ │ ├── SettingController.js │ │ │ │ │ ├── SystemInfoController.js │ │ │ │ │ ├── UserController.js │ │ │ │ │ ├── config/ │ │ │ │ │ │ ├── ConfigBaseInfoController.js │ │ │ │ │ │ ├── ConfigNamespaceController.js │ │ │ │ │ │ ├── DiffConfigController.js │ │ │ │ │ │ ├── ReleaseHistoryController.js │ │ │ │ │ │ └── SyncConfigController.js │ │ │ │ │ ├── open/ │ │ │ │ │ │ └── OpenManageController.js │ │ │ │ │ └── role/ │ │ │ │ │ ├── ClusterNamespaceRoleController.js │ │ │ │ │ ├── NamespaceRoleController.js │ │ │ │ │ └── SystemRoleController.js │ │ │ │ ├── directive/ │ │ │ │ │ ├── delete-namespace-modal-directive.js │ │ │ │ │ ├── diff-directive.js │ │ │ │ │ ├── directive.js │ │ │ │ │ ├── gray-release-rules-modal-directive.js │ │ │ │ │ ├── import-namespace-modal-directive.js │ │ │ │ │ ├── item-modal-directive.js │ │ │ │ │ ├── merge-and-publish-modal-directive.js │ │ │ │ │ ├── namespace-panel-directive.js │ │ │ │ │ ├── open-manage-grant-permission-modal-directive.js │ │ │ │ │ ├── publish-deny-modal-directive.js │ │ │ │ │ ├── release-modal-directive.js │ │ │ │ │ ├── rollback-modal-directive.js │ │ │ │ │ └── show-text-modal-directive.js │ │ │ │ ├── services/ │ │ │ │ │ ├── AccessKeyService.js │ │ │ │ │ ├── AppService.js │ │ │ │ │ ├── AuditLogService.js │ │ │ │ │ ├── ClusterService.js │ │ │ │ │ ├── CommitService.js │ │ │ │ │ ├── CommonService.js │ │ │ │ │ ├── ConfigService.js │ │ │ │ │ ├── ConsumerService.js │ │ │ │ │ ├── EnvService.js │ │ │ │ │ ├── EventManager.js │ │ │ │ │ ├── ExportService.js │ │ │ │ │ ├── FavoriteService.js │ │ │ │ │ ├── GlobalSearchValueService.js │ │ │ │ │ ├── InstanceService.js │ │ │ │ │ ├── NamespaceBranchService.js │ │ │ │ │ ├── NamespaceLockService.js │ │ │ │ │ ├── NamespaceService.js │ │ │ │ │ ├── OrganizationService.js │ │ │ │ │ ├── PermissionService.js │ │ │ │ │ ├── ReleaseHistoryService.js │ │ │ │ │ ├── ReleaseService.js │ │ │ │ │ ├── ServerConfigService.js │ │ │ │ │ ├── SystemInfoService.js │ │ │ │ │ ├── SystemRoleService.js │ │ │ │ │ └── UserService.js │ │ │ │ └── valdr.js │ │ │ ├── server_config_manage.html │ │ │ ├── styles/ │ │ │ │ ├── audit-log.css │ │ │ │ └── common-style.css │ │ │ ├── system-role-manage.html │ │ │ ├── system_info.html │ │ │ ├── user-manage.html │ │ │ ├── vendor/ │ │ │ │ ├── iconfont/ │ │ │ │ │ └── iconfont.css │ │ │ │ ├── jquery-plugin/ │ │ │ │ │ ├── jquery.textareafullscreen.js │ │ │ │ │ └── textareafullscreen.css │ │ │ │ └── ui-ace/ │ │ │ │ ├── ace.js │ │ │ │ ├── ext-searchbox.js │ │ │ │ ├── mode-json.js │ │ │ │ ├── mode-properties.js │ │ │ │ ├── mode-xml.js │ │ │ │ ├── mode-yaml.js │ │ │ │ ├── theme-eclipse.js │ │ │ │ ├── worker-json.js │ │ │ │ └── worker-xml.js │ │ │ └── views/ │ │ │ ├── common/ │ │ │ │ ├── footer.html │ │ │ │ └── nav.html │ │ │ └── component/ │ │ │ ├── back-top.html │ │ │ ├── confirm-dialog.html │ │ │ ├── delete-namespace-modal.html │ │ │ ├── diff.html │ │ │ ├── entrance.html │ │ │ ├── env-selector.html │ │ │ ├── gray-release-rules-modal.html │ │ │ ├── import-namespace-modal.html │ │ │ ├── item-modal.html │ │ │ ├── merge-and-publish-modal.html │ │ │ ├── multiple-user-selector.html │ │ │ ├── namespace-panel-branch-tab.html │ │ │ ├── namespace-panel-header.html │ │ │ ├── namespace-panel-master-tab.html │ │ │ ├── namespace-panel.html │ │ │ ├── publish-deny-modal.html │ │ │ ├── release-modal.html │ │ │ ├── rollback-modal.html │ │ │ ├── show-text-modal.html │ │ │ └── user-selector.html │ │ └── scripts/ │ │ ├── shutdown.sh │ │ └── startup.sh │ └── test/ │ ├── java/ │ │ └── com/ │ │ └── ctrip/ │ │ └── framework/ │ │ └── apollo/ │ │ ├── ControllableAuthorizationConfiguration.java │ │ ├── LocalPortalApplication.java │ │ ├── SkipAuthorizationConfiguration.java │ │ ├── openapi/ │ │ │ ├── auth/ │ │ │ │ └── ConsumerPermissionValidatorTest.java │ │ │ ├── filter/ │ │ │ │ └── ConsumerAuthenticationFilterTest.java │ │ │ ├── service/ │ │ │ │ ├── ConsumerRolePermissionServiceTest.java │ │ │ │ ├── ConsumerServiceIntegrationTest.java │ │ │ │ └── ConsumerServiceTest.java │ │ │ ├── util/ │ │ │ │ ├── ConsumerAuditUtilTest.java │ │ │ │ └── ConsumerAuthUtilTest.java │ │ │ └── v1/ │ │ │ └── controller/ │ │ │ ├── AbstractControllerTest.java │ │ │ ├── AppControllerIntegrationTest.java │ │ │ ├── AppControllerParamBindLowLevelTest.java │ │ │ ├── AppControllerTest.java │ │ │ ├── ClusterControllerParamBindLowLevelTest.java │ │ │ ├── ClusterControllerTest.java │ │ │ ├── EnvControllerTest.java │ │ │ ├── NamespaceControllerTest.java │ │ │ ├── NamespaceControllerWithAuthorizationTest.java │ │ │ └── OrganizationControllerTest.java │ │ └── portal/ │ │ ├── AbstractIntegrationTest.java │ │ ├── AbstractUnitTest.java │ │ ├── RetryableRestTemplateTest.java │ │ ├── ServiceExceptionTest.java │ │ ├── component/ │ │ │ ├── AbstractPermissionValidatorTest.java │ │ │ ├── UnifiedPermissionValidatorTest.java │ │ │ ├── UserIdentityContextHolderTest.java │ │ │ ├── UserPermissionValidatorTest.java │ │ │ ├── UserPermissionValidatorTestSupplement.java │ │ │ ├── config/ │ │ │ │ └── PortalConfigTest.java │ │ │ └── txtresolver/ │ │ │ ├── FileTextResolverTest.java │ │ │ └── PropertyResolverTest.java │ │ ├── config/ │ │ │ └── ConfigTest.java │ │ ├── controller/ │ │ │ ├── ClusterControllerTest.java │ │ │ ├── CommitControllerTest.java │ │ │ ├── ConfigsExportControllerTest.java │ │ │ ├── ConfigsImportControllerTest.java │ │ │ ├── ConsumerControllerTest.java │ │ │ ├── EnvControllerTest.java │ │ │ ├── GlobalSearchControllerTest.java │ │ │ ├── InstanceControllerTest.java │ │ │ ├── ItemControllerAuthIntegrationTest.java │ │ │ ├── ItemControllerTest.java │ │ │ ├── NamespaceLockControllerTest.java │ │ │ ├── OrganizationControllerTest.java │ │ │ ├── PageSettingControllerTest.java │ │ │ ├── PermissionControllerTest.java │ │ │ ├── PrefixPathControllerTest.java │ │ │ ├── ReleaseHistoryControllerTest.java │ │ │ ├── SearchControllerTest.java │ │ │ ├── ServerConfigControllerTest.java │ │ │ ├── SignInControllerTest.java │ │ │ ├── SsoHeartbeatControllerTest.java │ │ │ ├── SystemInfoControllerTest.java │ │ │ └── UserInfoControllerTest.java │ │ ├── environment/ │ │ │ ├── BaseIntegrationTest.java │ │ │ ├── DatabasePortalMetaServerProviderTest.java │ │ │ ├── DefaultPortalMetaServerProviderTest.java │ │ │ ├── EnvTest.java │ │ │ └── PortalMetaDomainServiceTest.java │ │ ├── filter/ │ │ │ ├── PortalOpenApiAuthenticationScenariosTest.java │ │ │ └── UserTypeResolverFilter.java │ │ ├── service/ │ │ │ ├── AppNamespaceServiceTest.java │ │ │ ├── AppServiceTest.java │ │ │ ├── ConfigServiceTest.java │ │ │ ├── ConfigsExportServiceTest.java │ │ │ ├── FavoriteServiceTest.java │ │ │ ├── GlobalSearchServiceTest.java │ │ │ └── NamespaceServiceTest.java │ │ ├── spi/ │ │ │ └── defaultImpl/ │ │ │ ├── RoleInitializationServiceTest.java │ │ │ └── RolePermissionServiceTest.java │ │ └── util/ │ │ ├── AuthUserPasswordCheckerTest.java │ │ ├── ConfigFileUtilsTest.java │ │ ├── KeyValueUtilsTest.java │ │ └── RoleUtilsTest.java │ └── resources/ │ ├── application.properties │ ├── application.yml │ ├── import.sql │ ├── logback-test.xml │ ├── sql/ │ │ ├── appnamespaceservice/ │ │ │ └── init-appnamespace.sql │ │ ├── cleanup.sql │ │ ├── favorites/ │ │ │ └── favorites.sql │ │ ├── openapi/ │ │ │ ├── ConsumerServiceIntegrationTest.commonData.sql │ │ │ ├── ConsumerServiceIntegrationTest.testFindAppIdsAuthorizedByConsumerId.sql │ │ │ └── NamespaceControllerTest.testCreateAppNamespace.sql │ │ └── permission/ │ │ ├── RolePermissionServiceTest.deleteRolePermissionsByAppIdWithClusterRoles.sql │ │ ├── consumer_role_permission_service/ │ │ │ ├── test_get_user_permission_set_different_users.sql │ │ │ ├── test_get_user_permission_set_no_roles.sql │ │ │ ├── test_get_user_permission_set_roles_without_permissions.sql │ │ │ └── test_get_user_permission_set_with_permissions.sql │ │ ├── insert-test-consumerroles.sql │ │ ├── insert-test-getUserPermissionSet.sql │ │ ├── insert-test-permissions.sql │ │ ├── insert-test-rolepermissions.sql │ │ ├── insert-test-roles.sql │ │ └── insert-test-userroles.sql │ ├── static/ │ │ └── scripts/ │ │ └── test_hasDuplicateKeys.js │ └── yaml/ │ ├── case1.yaml │ ├── case2.yaml │ └── case3.yaml ├── changes/ │ ├── changes-1.9.0.md │ ├── changes-1.9.1.md │ ├── changes-1.9.2.md │ ├── changes-2.0.0.md │ ├── changes-2.0.1.md │ ├── changes-2.1.0.md │ ├── changes-2.2.0.md │ ├── changes-2.3.0.md │ ├── changes-2.4.0.md │ └── changes-2.5.0.md ├── docs/ │ ├── .nojekyll │ ├── CNAME │ ├── _coverpage.md │ ├── charts/ │ │ ├── apollo-portal-0.1.0.tgz │ │ ├── apollo-portal-0.1.1.tgz │ │ ├── apollo-portal-0.1.2.tgz │ │ ├── apollo-portal-0.2.0.tgz │ │ ├── apollo-portal-0.2.1.tgz │ │ ├── apollo-portal-0.2.2.tgz │ │ ├── apollo-portal-0.3.0.tgz │ │ ├── apollo-portal-0.3.1.tgz │ │ ├── apollo-service-0.1.0.tgz │ │ ├── apollo-service-0.1.1.tgz │ │ ├── apollo-service-0.1.2.tgz │ │ ├── apollo-service-0.2.0.tgz │ │ ├── apollo-service-0.2.1.tgz │ │ ├── apollo-service-0.2.2.tgz │ │ ├── apollo-service-0.3.0.tgz │ │ ├── apollo-service-0.3.1.tgz │ │ └── index.yaml │ ├── css/ │ │ ├── buble.css │ │ ├── dark.css │ │ ├── fonts.css │ │ ├── pure.css │ │ └── vue.css │ ├── en/ │ │ ├── README.md │ │ ├── _navbar.md │ │ ├── _sidebar.md │ │ ├── client/ │ │ │ ├── c-sdks-user-guide.md │ │ │ ├── cpp-sdks-user-guide.md │ │ │ ├── dotnet-sdk-user-guide.md │ │ │ ├── golang-sdks-user-guide.md │ │ │ ├── java-sdk-user-guide.md │ │ │ ├── k8s-configmap-user-guide.md │ │ │ ├── nodejs-sdks-user-guide.md │ │ │ ├── other-language-client-user-guide.md │ │ │ ├── php-sdks-user-guide.md │ │ │ ├── python-sdks-user-guide.md │ │ │ └── rust-sdks-user-guide.md │ │ ├── community/ │ │ │ ├── team.md │ │ │ └── thank-you.md │ │ ├── contribution/ │ │ │ ├── apollo-development-guide.md │ │ │ └── apollo-release-guide.md │ │ ├── deployment/ │ │ │ ├── deployment-architecture.md │ │ │ ├── distributed-deployment-guide.md │ │ │ ├── quick-start-docker.md │ │ │ ├── quick-start.md │ │ │ ├── third-party-tool-btpanel.md │ │ │ └── third-party-tool-rainbond.md │ │ ├── design/ │ │ │ ├── apollo-core-concept-namespace.md │ │ │ ├── apollo-design.md │ │ │ └── apollo-introduction.md │ │ ├── extension/ │ │ │ ├── portal-how-to-enable-email-service.md │ │ │ ├── portal-how-to-enable-session-store.md │ │ │ ├── portal-how-to-enable-webhook-notification.md │ │ │ └── portal-how-to-implement-user-login-function.md │ │ ├── faq/ │ │ │ ├── common-issues-in-deployment-and-development-phase.md │ │ │ └── faq.md │ │ ├── misc/ │ │ │ └── apollo-benchmark.md │ │ ├── portal/ │ │ │ ├── apollo-open-api-platform.md │ │ │ ├── apollo-user-guide.md │ │ │ └── apollo-user-practices.md │ │ └── quick-start.md │ ├── index.html │ ├── scripts/ │ │ └── multiple-language-redirect.js │ └── zh/ │ ├── README.md │ ├── _navbar.md │ ├── _sidebar.md │ ├── client/ │ │ ├── c-sdks-user-guide.md │ │ ├── cpp-sdks-user-guide.md │ │ ├── dotnet-sdk-user-guide.md │ │ ├── golang-sdks-user-guide.md │ │ ├── java-sdk-user-guide.md │ │ ├── k8s-configmap-user-guide.md │ │ ├── nodejs-sdks-user-guide.md │ │ ├── other-language-client-user-guide.md │ │ ├── php-sdks-user-guide.md │ │ ├── python-sdks-user-guide.md │ │ └── rust-sdks-user-guide.md │ ├── community/ │ │ ├── team.md │ │ └── thank-you.md │ ├── contribution/ │ │ ├── apollo-development-guide.md │ │ └── apollo-release-guide.md │ ├── deployment/ │ │ ├── deployment-architecture.md │ │ ├── distributed-deployment-guide.md │ │ ├── quick-start-docker.md │ │ ├── quick-start.md │ │ ├── third-party-tool-btpanel.md │ │ └── third-party-tool-rainbond.md │ ├── design/ │ │ ├── apollo-core-concept-namespace.md │ │ ├── apollo-design.md │ │ └── apollo-introduction.md │ ├── extension/ │ │ ├── portal-how-to-enable-email-service.md │ │ ├── portal-how-to-enable-session-store.md │ │ ├── portal-how-to-enable-webhook-notification.md │ │ └── portal-how-to-implement-user-login-function.md │ ├── faq/ │ │ ├── common-issues-in-deployment-and-development-phase.md │ │ └── faq.md │ ├── misc/ │ │ └── apollo-benchmark.md │ └── portal/ │ ├── apollo-open-api-platform.md │ ├── apollo-user-guide.md │ └── apollo-user-practices.md ├── e2e/ │ ├── README.md │ └── portal-e2e/ │ ├── .gitignore │ ├── config/ │ │ ├── application-ldap-e2e.yml │ │ └── application-oidc-e2e.yml │ ├── package.json │ ├── playwright.config.js │ ├── scripts/ │ │ ├── auth/ │ │ │ ├── setup-ldap.sh │ │ │ ├── setup-oidc.sh │ │ │ └── teardown-auth.sh │ │ └── wait-for-ready.sh │ └── tests/ │ ├── helpers/ │ │ ├── auth-helpers.js │ │ └── portal-helpers.js │ ├── portal-auth-matrix.spec.js │ ├── portal-configservice.spec.js │ ├── portal-core.spec.js │ ├── portal-priority.spec.js │ └── portal-regression.spec.js ├── mvnw ├── mvnw.cmd ├── pom.xml └── scripts/ ├── build.bat ├── build.sh ├── openapi/ │ └── bash/ │ ├── openapi-usage-example.sh │ └── openapi.sh └── sql/ ├── profiles/ │ ├── h2-default/ │ │ ├── apolloconfigdb.sql │ │ ├── apolloportaldb.sql │ │ └── delta/ │ │ ├── v220-v230/ │ │ │ ├── apolloconfigdb-v220-v230.sql │ │ │ └── apolloportaldb-v220-v230.sql │ │ └── v230-v240/ │ │ ├── apolloconfigdb-v230-v240.sql │ │ └── apolloportaldb-v230-v240.sql │ ├── mysql-database-not-specified/ │ │ ├── apolloconfigdb.sql │ │ ├── apolloportaldb.sql │ │ └── delta/ │ │ ├── v220-v230/ │ │ │ ├── apolloconfigdb-v220-v230.sql │ │ │ └── apolloportaldb-v220-v230.sql │ │ └── v230-v240/ │ │ ├── apolloconfigdb-v230-v240.sql │ │ └── apolloportaldb-v230-v240.sql │ └── mysql-default/ │ ├── apolloconfigdb.sql │ ├── apolloportaldb.sql │ └── delta/ │ ├── v040-v050/ │ │ ├── apolloconfigdb-v040-v050.sql │ │ └── apolloportaldb-v040-v050.sql │ ├── v060-v062/ │ │ ├── apolloconfigdb-v060-v062.sql │ │ └── apolloportaldb-v060-v062.sql │ ├── v080-v090/ │ │ └── apolloportaldb-v080-v090.sql │ ├── v151-v160/ │ │ └── apolloconfigdb-v151-v160.sql │ ├── v170-v180/ │ │ ├── apolloconfigdb-v170-v180.sql │ │ └── apolloportaldb-v170-v180.sql │ ├── v180-v190/ │ │ ├── apolloconfigdb-v180-v190.sql │ │ └── apolloportaldb-v180-v190.sql │ ├── v190-v200/ │ │ ├── apolloconfigdb-v190-v200-after.sql │ │ ├── apolloconfigdb-v190-v200.sql │ │ ├── apolloportaldb-v190-v200-after.sql │ │ └── apolloportaldb-v190-v200.sql │ ├── v200-v210/ │ │ └── apolloconfigdb-v200-v210.sql │ ├── v210-v220/ │ │ ├── apolloconfigdb-v210-v220.sql │ │ └── apolloportaldb-v210-v220.sql │ ├── v220-v230/ │ │ ├── apolloconfigdb-v220-v230.sql │ │ └── apolloportaldb-v220-v230.sql │ └── v230-v240/ │ ├── apolloconfigdb-v230-v240.sql │ └── apolloportaldb-v230-v240.sql └── src/ ├── apolloconfigdb.sql ├── apolloportaldb.sql ├── delta/ │ ├── v220-v230/ │ │ ├── apolloconfigdb-v220-v230.sql │ │ └── apolloportaldb-v220-v230.sql │ └── v230-v240/ │ ├── apolloconfigdb-v230-v240.sql │ └── apolloportaldb-v230-v240.sql └── gist/ ├── autoGeneratedDeclaration.sql ├── h2Function.sql ├── setupDatabase.sql └── useDatabase.sql ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitattributes ================================================ text=auto *.sh text eol=lf .github/workflows/*.lock.yml linguist-generated=true merge=ours ================================================ FILE: .github/FUNDING.yml ================================================ # # Copyright 2024 Apollo Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # github: [apolloconfig] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] open_collective: apollo # Replace with a single Open Collective username ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report_en.md ================================================ --- name: Bug report about: Create a report to help us improve title: '' labels: '' assignees: '' --- - [ ] I have checked the [discussions](https://github.com/ctripcorp/apollo/discussions) - [ ] I have searched the [issues](https://github.com/ctripcorp/apollo/issues) of this repository and believe that this is not a duplicate. - [ ] I have checked the [FAQ](https://www.apolloconfig.com/#/zh/faq/common-issues-in-deployment-and-development-phase) of this repository and believe that this is not a duplicate. **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: 1. 2. 3. 4. **Expected behavior** A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. ### Additional Details & Logs - Version - Error logs - Configuration - Platform and Operating System ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report_zh.md ================================================ --- name: 报告Bug/使用疑问 about: 提交Apollo Bug/使用疑问,使用这个模板 title: '' labels: '' assignees: '' --- - [ ] 我已经检查过[discussions](https://github.com/ctripcorp/apollo/discussions) - [ ] 我已经搜索过[issues](https://github.com/ctripcorp/apollo/issues) - [ ] 我已经仔细检查过[FAQ](https://www.apolloconfig.com/#/zh/faq/common-issues-in-deployment-and-development-phase) **描述bug** 简洁明了地描述一下bug **复现** 通过如下步骤可以复现: 1. 2. 3. 4. **期望** 简介明了地描述你希望正常情况下应该发生什么 **截图** 如果可以,附上截图来描述你的问题 ### 额外的细节和日志 - 版本: - 错误日志 - 配置: - 平台和操作系统 ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request_en.md ================================================ --- name: Feature request about: Suggest an idea for this project title: '' labels: '' assignees: '' --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request_zh.md ================================================ --- name: 请求特性 about: 给这个项目提一些建议、想法 title: '' labels: '' assignees: '' --- **你的特性请求和某个问题有关吗?请描述** 清晰简洁地描述这个问题是什么。即,当碰到xxx时,总是感觉很麻烦 **清晰简洁地描述一下你希望的解决方案** **清晰简洁地描述一下这个特性的备选方案** **其它背景** 在这里添加和这个特性请求有关的背景说明、截图 ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ ## What's the purpose of this PR XXXXX ## Which issue(s) this PR fixes: Fixes # ## Brief changelog XXXXX Follow this checklist to help us incorporate your contribution quickly and easily: - [ ] Read the [Contributing Guide](https://github.com/apolloconfig/apollo/blob/master/CONTRIBUTING.md) before making this pull request. - [ ] Write a pull request description that is detailed enough to understand what the pull request does, how, and why. - [ ] Write necessary unit tests to verify the code. - [ ] Run `mvn clean test` to make sure this pull request doesn't break anything. - [ ] Run `mvn spotless:apply` to format your code. - [ ] Update the [`CHANGES` log](https://github.com/apolloconfig/apollo/blob/master/CHANGES.md). ================================================ FILE: .github/aw/actions-lock.json ================================================ { "entries": { "actions/github-script@v8": { "repo": "actions/github-script", "version": "v8", "sha": "ed597411d8f924073f98dfc5c65a23a2325f34cd" }, "github/gh-aw/actions/setup@v0.43.0": { "repo": "github/gh-aw/actions/setup", "version": "v0.43.0", "sha": "c549967b5808d783bc1537cf5687896de4dec7ee" } } } ================================================ FILE: .github/stale.yml ================================================ # # Copyright 2024 Apollo Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # Configuration for probot-stale - https://github.com/probot/stale # General configuration # Number of days of inactivity before an issue becomes stale daysUntilStale: 30 # Issues with these labels will never be considered stale exemptLabels: - bug - discussion - enhancement - feature - feature request - help wanted - info - need investigation - tips # Set to true to ignore issues in a project (defaults to false) exemptProjects: true # Set to true to ignore issues in a milestone (defaults to false) exemptMilestones: true # Set to true to ignore issues with an assignee (defaults to false) exemptAssignees: true # Label to use when marking an issue as stale staleLabel: stale # Pull request specific configuration pulls: # Number of days of inactivity before a stale Issue or Pull Request is closed. # Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale. daysUntilClose: 14 # Comment to post when marking as stale. Set to `false` to disable markComment: > This pull request has been automatically marked as stale because it has not had activity in the last 30 days. It will be closed in 14 days if no further activity occurs. Please feel free to give a status update now, ping for review, or re-open when it's ready. Thank you for your contributions! # Comment to post when closing a stale Issue or Pull Request. closeComment: > This pull request has been automatically closed because it has not had activity in the last 14 days. Please feel free to give a status update now, ping for review, or re-open when it's ready. Thank you for your contributions! # Limit the number of actions per hour, from 1-30. Default is 30 limitPerRun: 30 # Issue specific configuration issues: # Number of days of inactivity before a stale Issue or Pull Request is closed. daysUntilClose: 7 # Comment to post when marking as stale. Set to `false` to disable markComment: > This issue has been automatically marked as stale because it has not had activity in the last 30 days. It will be closed in 7 days unless it is tagged "help wanted" or other activity occurs. Thank you for your contributions. # Comment to post when closing a stale Issue or Pull Request. closeComment: > This issue has been automatically closed because it has not had activity in the last 7 days. If this issue is still valid, please ping a maintainer and ask them to label it as "help wanted". Thank you for your contributions. # Limit the number of actions per hour, from 1-30. Default is 30 limitPerRun: 30 ================================================ FILE: .github/workflows/build.yml ================================================ # # Copyright 2024 Apollo Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # This workflow will build a Java project with Maven # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven name: build on: push: branches: [ master ] pull_request: branches: [ master ] jobs: build: runs-on: ubuntu-latest strategy: matrix: jdk: [17, 21] steps: - uses: actions/checkout@v4 - name: Set up JDK uses: actions/setup-java@v4 with: distribution: temurin java-version: ${{ matrix.jdk }} - name: Cache Maven packages uses: actions/cache@v4 with: path: ~/.m2/repository key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} restore-keys: | ${{ runner.os }}-maven- - name: JDK 17 if: matrix.jdk == '17' uses: nick-fields/retry@v3 with: timeout_minutes: 5 max_attempts: 3 retry_wait_seconds: 1 command: mvn -B clean package jacoco:report -Dmaven.gitcommitid.skip=true - name: JDK 21 if: matrix.jdk == '21' run: mvn -B clean compile -Dmaven.gitcommitid.skip=true - name: Upload coverage to Codecov if: matrix.jdk == '17' uses: codecov/codecov-action@v1 with: file: ${{ github.workspace }}/apollo-*/target/site/jacoco/jacoco.xml ================================================ FILE: .github/workflows/cla.yml ================================================ # # Copyright 2024 Apollo Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # name: "CLA Assistant" on: issue_comment: types: [created] pull_request_target: types: [opened,closed,synchronize] jobs: CLAssistant: runs-on: ubuntu-latest steps: - name: "CLA Assistant" if: (github.event.comment.body == 'recheck' || startsWith(github.event.comment.body, 'I have read the CLA Document and I hereby sign the CLA')) || github.event_name == 'pull_request_target' # Beta Release uses: cla-assistant/github-action@v2.1.2-beta env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # the below token should have repo scope and must be manually added by you in the repository's secret PERSONAL_ACCESS_TOKEN : ${{ secrets.PERSONAL_ACCESS_TOKEN_FOR_CLA_ASSISTANT }} with: path-to-signatures: 'signatures/version1/cla.json' path-to-document: 'https://github.com/apolloconfig/apollo-community/blob/master/CLA.md' # e.g. a CLA or a DCO document # branch should not be protected branch: 'master' allowlist: dependabot,bot*,github-actions remote-repository-name: apollo-community #below are the optional inputs - If the optional inputs are not given, then default values will be taken #remote-organization-name: enter the remote organization name where the signatures should be stored (Default is storing the signatures in the same repository) #remote-repository-name: enter the remote repository name where the signatures should be stored (Default is storing the signatures in the same repository) #create-file-commit-message: 'For example: Creating file for storing CLA Signatures' #signed-commit-message: 'For example: $contributorName has signed the CLA in #$pullRequestNo' #custom-notsigned-prcomment: 'pull request comment with Introductory message to ask new contributors to sign' #custom-pr-sign-comment: 'The signature to be committed in order to sign the CLA' #custom-allsigned-prcomment: 'pull request comment when all contributors has signed, defaults to **CLA Assistant Lite bot** All Contributors have signed the CLA.' #lock-pullrequest-aftermerge: false - if you don't want this bot to automatically lock the pull request after merging (default - true) #use-dco-flag: true - If you are using DCO instead of CLA ================================================ FILE: .github/workflows/code-style-check.yml ================================================ # # Copyright 2025 Apollo Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # This workflow checks code style using Spotless name: code style check on: push: branches: [ master ] pull_request: branches: [ master ] jobs: code-style-check: runs-on: ubuntu-latest permissions: contents: read steps: - uses: actions/checkout@v4 - name: Set up JDK 17 uses: actions/setup-java@v1 with: java-version: 17 - name: Cache Maven packages uses: actions/cache@v4 with: path: ~/.m2/repository key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} restore-keys: | ${{ runner.os }}-maven- - name: Code Style Check run: mvn spotless:check ================================================ FILE: .github/workflows/codeql.yml ================================================ name: "CodeQL" on: push: branches: [ 'master' ] pull_request: # The branches below must be a subset of the branches above branches: [ 'master' ] schedule: - cron: '25 18 * * 2' jobs: analyze: name: Analyze runs-on: ubuntu-latest permissions: actions: read contents: read security-events: write strategy: fail-fast: false matrix: language: [ 'javascript', 'java' ] # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] # Use only 'java' to analyze code written in Java, Kotlin or both # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support steps: - name: Checkout repository uses: actions/checkout@v3 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v2 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. # By default, queries listed here will override any specified in a config file. # Prefix the list here with "+" to use these queries and those in the config file. # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs # queries: security-extended,security-and-quality # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild uses: github/codeql-action/autobuild@v2 # ℹ️ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun # If the Autobuild fails above, remove it and uncomment the following three lines. # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. # - run: | # echo "Run, Build Application using script" # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v2 with: category: "/language:${{matrix.language}}" ================================================ FILE: .github/workflows/commit_lint.yml ================================================ name: commit lint on: pull_request: branches: - main jobs: commitlint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: wagoid/commitlint-github-action@v5 ================================================ FILE: .github/workflows/docker-publish.yml ================================================ # # Copyright 2024 Apollo Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # name: Publish Docker Image on: workflow_dispatch: inputs: version: description: 'version' required: true jobs: check: runs-on: ubuntu-latest outputs: apollo-config-tags: ${{ steps.check-tags.outputs.apollo-config-tags }} apollo-admin-tags: ${{ steps.check-tags.outputs.apollo-admin-tags }} apollo-portal-tags: ${{ steps.check-tags.outputs.apollo-portal-tags }} steps: - id: check-tags name: Check tags run: | if [[ ${{ github.event.inputs.version }} == *-SNAPSHOT ]]; then echo "apollo-config-tags=apolloconfig/apollo-configservice:${{ github.event.inputs.version }}" >> $GITHUB_OUTPUT echo "apollo-admin-tags=apolloconfig/apollo-adminservice:${{ github.event.inputs.version }}" >> $GITHUB_OUTPUT echo "apollo-portal-tags=apolloconfig/apollo-portal:${{ github.event.inputs.version }}" >> $GITHUB_OUTPUT else echo "apollo-config-tags=apolloconfig/apollo-configservice:${{ github.event.inputs.version }},apolloconfig/apollo-configservice:latest" >> $GITHUB_OUTPUT echo "apollo-admin-tags=apolloconfig/apollo-adminservice:${{ github.event.inputs.version }},apolloconfig/apollo-adminservice:latest" >> $GITHUB_OUTPUT echo "apollo-portal-tags=apolloconfig/apollo-portal:${{ github.event.inputs.version }},apolloconfig/apollo-portal:latest" >> $GITHUB_OUTPUT fi publish: needs: check runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v3 - name: Set up JDK uses: actions/setup-java@v4 with: distribution: temurin java-version: 17 - name: Build package run: ./scripts/build.sh - name: Login to Docker Hub uses: docker/login-action@v2 with: username: ${{ secrets.DOCKER_HUB_USER_NAME }} password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} - name: Set up QEMU uses: docker/setup-qemu-action@v2 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 - name: Build and push apollo-configservice uses: docker/build-push-action@v3 with: context: ./apollo-configservice/target platforms: linux/amd64,linux/arm64 file: ./apollo-configservice/src/main/docker/Dockerfile push: true build-args: VERSION=${{ github.event.inputs.version }} tags: ${{ needs.check.outputs.apollo-config-tags }} - name: Build and push apollo-adminservice uses: docker/build-push-action@v3 with: context: ./apollo-adminservice/target platforms: linux/amd64,linux/arm64 file: ./apollo-adminservice/src/main/docker/Dockerfile push: true build-args: VERSION=${{ github.event.inputs.version }} tags: ${{ needs.check.outputs.apollo-admin-tags }} - name: Build and push apollo-portal uses: docker/build-push-action@v3 with: context: ./apollo-portal/target platforms: linux/amd64,linux/arm64 file: ./apollo-portal/src/main/docker/Dockerfile push: true build-args: VERSION=${{ github.event.inputs.version }} tags: ${{ needs.check.outputs.apollo-portal-tags }} ================================================ FILE: .github/workflows/docker-validation.yml ================================================ # # Copyright 2024 Apollo Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # name: Docker Validation on: pull_request: branches: [ master ] paths: - 'apollo-configservice/src/main/docker/Dockerfile' - 'apollo-adminservice/src/main/docker/Dockerfile' - 'apollo-portal/src/main/docker/Dockerfile' workflow_dispatch: jobs: docker-smoke: runs-on: ubuntu-latest timeout-minutes: 40 steps: - uses: actions/checkout@v4 - name: Set up JDK uses: actions/setup-java@v4 with: distribution: temurin java-version: 17 - name: Cache Maven packages uses: actions/cache@v4 with: path: ~/.m2/repository key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} restore-keys: | ${{ runner.os }}-maven- - name: Build package run: ./scripts/build.sh - name: Resolve Maven project version id: project-version run: | echo "version=$(mvn -q -N help:evaluate -Dexpression=project.version -DforceStdout)" >> $GITHUB_OUTPUT - name: Validate Dockerfiles env: VERSION: ${{ steps.project-version.outputs.version }} run: | set -euo pipefail validate_dockerfile() { local service="$1" local dockerfile="$2" local context="$3" local startup_script="$4" local host_port="$5" local container_port="$6" local datasource_url="$7" local active_profiles="$8" local image_tag="${service}:ci-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" local container_name="${service}-smoke-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" local max_health_check_attempts=180 local healthy=0 local run_args=( --detach --name "$container_name" --publish "127.0.0.1:${host_port}:${container_port}" --env "APOLLO_PROFILE=${active_profiles}" --env "SPRING_PROFILES_ACTIVE=${active_profiles}" --env "SPRING_DATASOURCE_URL=${datasource_url}" --env "SERVER_PORT=${container_port}" --entrypoint /bin/bash ) docker build \ --file "$dockerfile" \ --build-arg VERSION="${VERSION}" \ --tag "$image_tag" \ "$context" if [[ "$service" == "apollo-portal" ]]; then run_args+=( --env "APOLLO_PORTAL_ENVS=dev" --env "DEV_META=http://127.0.0.1:8080" --env "FAT_META=http://127.0.0.1:8080" --env "UAT_META=http://127.0.0.1:8080" --env "PRO_META=http://127.0.0.1:8080" ) fi docker run "${run_args[@]}" \ "$image_tag" \ -c "set -euo pipefail; test -x '${startup_script}'; exec '${startup_script}'" for i in $(seq 1 "$max_health_check_attempts"); do if curl --silent --fail "http://127.0.0.1:${host_port}/health" | grep -q "UP"; then healthy=1 break fi if [[ -z "$(docker ps --filter "name=^/${container_name}$" --filter status=running --quiet)" ]]; then echo "${service} container exited before health check succeeded" docker logs "$container_name" || true docker rm -f "$container_name" >/dev/null 2>&1 || true return 1 fi sleep 1 done if [[ "$healthy" -ne 1 ]]; then echo "${service} health check timeout" docker logs "$container_name" || true docker rm -f "$container_name" >/dev/null 2>&1 || true return 1 fi docker rm -f "$container_name" >/dev/null 2>&1 || true docker run --rm --entrypoint /bin/bash "$image_tag" \ -c "set -euo pipefail; java -version" } validate_dockerfile \ "apollo-configservice" \ "./apollo-configservice/src/main/docker/Dockerfile" \ "./apollo-configservice/target" \ "/apollo-configservice/scripts/startup.sh" \ "18080" \ "8080" \ "jdbc:h2:mem:apollo-config-db;mode=mysql;DB_CLOSE_ON_EXIT=FALSE;DB_CLOSE_DELAY=-1;BUILTIN_ALIAS_OVERRIDE=TRUE;DATABASE_TO_UPPER=FALSE" \ "h2" validate_dockerfile \ "apollo-adminservice" \ "./apollo-adminservice/src/main/docker/Dockerfile" \ "./apollo-adminservice/target" \ "/apollo-adminservice/scripts/startup.sh" \ "18090" \ "8090" \ "jdbc:h2:mem:apollo-config-db;mode=mysql;DB_CLOSE_ON_EXIT=FALSE;DB_CLOSE_DELAY=-1;BUILTIN_ALIAS_OVERRIDE=TRUE;DATABASE_TO_UPPER=FALSE" \ "h2" validate_dockerfile \ "apollo-portal" \ "./apollo-portal/src/main/docker/Dockerfile" \ "./apollo-portal/target" \ "/apollo-portal/scripts/startup.sh" \ "18070" \ "8070" \ "jdbc:h2:mem:apollo-portal-db;mode=mysql;DB_CLOSE_ON_EXIT=FALSE;DB_CLOSE_DELAY=-1;BUILTIN_ALIAS_OVERRIDE=TRUE;DATABASE_TO_UPPER=FALSE" \ "h2,auth" ================================================ FILE: .github/workflows/issue-triage.lock.yml ================================================ # # ___ _ _ # / _ \ | | (_) # | |_| | __ _ ___ _ __ | |_ _ ___ # | _ |/ _` |/ _ \ '_ \| __| |/ __| # | | | | (_| | __/ | | | |_| | (__ # \_| |_/\__, |\___|_| |_|\__|_|\___| # __/ | # _ _ |___/ # | | | | / _| | # | | | | ___ _ __ _ __| |_| | _____ ____ # | |/\| |/ _ \ '__| |/ /| _| |/ _ \ \ /\ / / ___| # \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ # \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ # # This file was automatically generated by gh-aw (v0.43.0). DO NOT EDIT. # # To update this file, edit the corresponding .md file and run: # gh aw compile # For more information: https://github.com/github/gh-aw/blob/main/.github/aw/github-agentic-workflows.md # # # frontmatter-hash: 9136db5aec7e414124ab034d3cea6924d002246d9cd31e3017bebdbd239e6c3c name: "Apollo Issue Triage" "on": issues: types: - opened permissions: {} concurrency: cancel-in-progress: true group: apollo-issue-triage-${{ github.event.issue.number }} run-name: "Apollo Issue Triage" jobs: activation: runs-on: ubuntu-slim permissions: contents: read outputs: comment_id: "" comment_repo: "" steps: - name: Setup Scripts uses: github/gh-aw/actions/setup@c549967b5808d783bc1537cf5687896de4dec7ee # v0.43.0 with: destination: /opt/gh-aw/actions - name: Check workflow file timestamps uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: GH_AW_WORKFLOW_FILE: "issue-triage.lock.yml" with: script: | const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); setupGlobals(core, github, context, exec, io); const { main } = require('/opt/gh-aw/actions/check_workflow_timestamp_api.cjs'); await main(); agent: needs: activation runs-on: ubuntu-latest permissions: read-all env: DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} GH_AW_ASSETS_ALLOWED_EXTS: "" GH_AW_ASSETS_BRANCH: "" GH_AW_ASSETS_MAX_SIZE_KB: 0 GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs GH_AW_SAFE_OUTPUTS: /opt/gh-aw/safeoutputs/outputs.jsonl GH_AW_SAFE_OUTPUTS_CONFIG_PATH: /opt/gh-aw/safeoutputs/config.json GH_AW_SAFE_OUTPUTS_TOOLS_PATH: /opt/gh-aw/safeoutputs/tools.json outputs: checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }} has_patch: ${{ steps.collect_output.outputs.has_patch }} model: ${{ steps.generate_aw_info.outputs.model }} output: ${{ steps.collect_output.outputs.output }} output_types: ${{ steps.collect_output.outputs.output_types }} secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }} steps: - name: Setup Scripts uses: github/gh-aw/actions/setup@c549967b5808d783bc1537cf5687896de4dec7ee # v0.43.0 with: destination: /opt/gh-aw/actions - name: Checkout .github and .agents folders uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 with: sparse-checkout: | .github .agents depth: 1 persist-credentials: false - name: Create gh-aw temp directory run: bash /opt/gh-aw/actions/create_gh_aw_tmp_dir.sh - name: Configure Git credentials env: REPO_NAME: ${{ github.repository }} SERVER_URL: ${{ github.server_url }} run: | git config --global user.email "github-actions[bot]@users.noreply.github.com" git config --global user.name "github-actions[bot]" # Re-authenticate git with GitHub token SERVER_URL_STRIPPED="${SERVER_URL#https://}" git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" echo "Git configured with standard GitHub Actions identity" - name: Checkout PR branch id: checkout-pr if: | github.event.pull_request uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} with: github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); setupGlobals(core, github, context, exec, io); const { main } = require('/opt/gh-aw/actions/checkout_pr_branch.cjs'); await main(); - name: Generate agentic run info id: generate_aw_info uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: script: | const fs = require('fs'); const awInfo = { engine_id: "copilot", engine_name: "GitHub Copilot CLI", model: process.env.GH_AW_MODEL_AGENT_COPILOT || "", version: "", agent_version: "0.0.405", cli_version: "v0.43.0", workflow_name: "Apollo Issue Triage", experimental: false, supports_tools_allowlist: true, supports_http_transport: true, run_id: context.runId, run_number: context.runNumber, run_attempt: process.env.GITHUB_RUN_ATTEMPT, repository: context.repo.owner + '/' + context.repo.repo, ref: context.ref, sha: context.sha, actor: context.actor, event_name: context.eventName, staged: false, allowed_domains: ["github"], firewall_enabled: true, awf_version: "v0.13.12", awmg_version: "", steps: { firewall: "squid" }, created_at: new Date().toISOString() }; // Write to /tmp/gh-aw directory to avoid inclusion in PR const tmpPath = '/tmp/gh-aw/aw_info.json'; fs.writeFileSync(tmpPath, JSON.stringify(awInfo, null, 2)); console.log('Generated aw_info.json at:', tmpPath); console.log(JSON.stringify(awInfo, null, 2)); // Set model as output for reuse in other steps/jobs core.setOutput('model', awInfo.model); - name: Validate COPILOT_GITHUB_TOKEN secret id: validate-secret run: /opt/gh-aw/actions/validate_multi_secret.sh COPILOT_GITHUB_TOKEN 'GitHub Copilot CLI' https://github.github.com/gh-aw/reference/engines/#github-copilot-default env: COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - name: Install GitHub Copilot CLI run: /opt/gh-aw/actions/install_copilot_cli.sh 0.0.405 - name: Install awf binary run: bash /opt/gh-aw/actions/install_awf_binary.sh v0.13.12 - name: Determine automatic lockdown mode for GitHub MCP server id: determine-automatic-lockdown env: TOKEN_CHECK: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} if: env.TOKEN_CHECK != '' uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | const determineAutomaticLockdown = require('/opt/gh-aw/actions/determine_automatic_lockdown.cjs'); await determineAutomaticLockdown(github, context, core); - name: Download container images run: bash /opt/gh-aw/actions/download_docker_images.sh ghcr.io/github/gh-aw-firewall/agent:0.13.12 ghcr.io/github/gh-aw-firewall/squid:0.13.12 ghcr.io/github/gh-aw-mcpg:v0.0.113 ghcr.io/github/github-mcp-server:v0.30.3 node:lts-alpine - name: Write Safe Outputs Config run: | mkdir -p /opt/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs cat > /opt/gh-aw/safeoutputs/config.json << 'EOF' {"add_comment":{"max":1},"add_labels":{"max":3},"mentions":{"allowContext":false,"allowTeamMembers":false},"missing_data":{},"missing_tool":{},"noop":{"max":1}} EOF cat > /opt/gh-aw/safeoutputs/tools.json << 'EOF' [ { "description": "Add a comment to an existing GitHub issue, pull request, or discussion. Use this to provide feedback, answer questions, or add information to an existing conversation. For creating new items, use create_issue, create_discussion, or create_pull_request instead. CONSTRAINTS: Maximum 1 comment(s) can be added.", "inputSchema": { "additionalProperties": false, "properties": { "body": { "description": "The comment text in Markdown format. This is the 'body' field - do not use 'comment_body' or other variations. Provide helpful, relevant information that adds value to the conversation.", "type": "string" }, "item_number": { "description": "The issue, pull request, or discussion number to comment on. This is the numeric ID from the GitHub URL (e.g., 123 in github.com/owner/repo/issues/123). If omitted, the tool will attempt to resolve the target from the current workflow context (triggering issue, PR, or discussion).", "type": "number" } }, "required": [ "body" ], "type": "object" }, "name": "add_comment" }, { "description": "Add labels to an existing GitHub issue or pull request for categorization and filtering. Labels must already exist in the repository. For creating new issues with labels, use create_issue with the labels property instead. CONSTRAINTS: Maximum 3 label(s) can be added.", "inputSchema": { "additionalProperties": false, "properties": { "item_number": { "description": "Issue or PR number to add labels to. This is the numeric ID from the GitHub URL (e.g., 456 in github.com/owner/repo/issues/456). If omitted, adds labels to the item that triggered this workflow.", "type": "number" }, "labels": { "description": "Label names to add (e.g., ['bug', 'priority-high']). Labels must exist in the repository.", "items": { "type": "string" }, "type": "array" } }, "type": "object" }, "name": "add_labels" }, { "description": "Report that a tool or capability needed to complete the task is not available, or share any information you deem important about missing functionality or limitations. Use this when you cannot accomplish what was requested because the required functionality is missing or access is restricted.", "inputSchema": { "additionalProperties": false, "properties": { "alternatives": { "description": "Any workarounds, manual steps, or alternative approaches the user could take (max 256 characters).", "type": "string" }, "reason": { "description": "Explanation of why this tool is needed or what information you want to share about the limitation (max 256 characters).", "type": "string" }, "tool": { "description": "Optional: Name or description of the missing tool or capability (max 128 characters). Be specific about what functionality is needed.", "type": "string" } }, "required": [ "reason" ], "type": "object" }, "name": "missing_tool" }, { "description": "Log a transparency message when no significant actions are needed. Use this to confirm workflow completion and provide visibility when analysis is complete but no changes or outputs are required (e.g., 'No issues found', 'All checks passed'). This ensures the workflow produces human-visible output even when no other actions are taken.", "inputSchema": { "additionalProperties": false, "properties": { "message": { "description": "Status or completion message to log. Should explain what was analyzed and the outcome (e.g., 'Code review complete - no issues found', 'Analysis complete - all tests passing').", "type": "string" } }, "required": [ "message" ], "type": "object" }, "name": "noop" }, { "description": "Report that data or information needed to complete the task is not available. Use this when you cannot accomplish what was requested because required data, context, or information is missing.", "inputSchema": { "additionalProperties": false, "properties": { "alternatives": { "description": "Any workarounds, manual steps, or alternative approaches the user could take (max 256 characters).", "type": "string" }, "context": { "description": "Additional context about the missing data or where it should come from (max 256 characters).", "type": "string" }, "data_type": { "description": "Type or description of the missing data or information (max 128 characters). Be specific about what data is needed.", "type": "string" }, "reason": { "description": "Explanation of why this data is needed to complete the task (max 256 characters).", "type": "string" } }, "required": [], "type": "object" }, "name": "missing_data" } ] EOF cat > /opt/gh-aw/safeoutputs/validation.json << 'EOF' { "add_comment": { "defaultMax": 1, "fields": { "body": { "required": true, "type": "string", "sanitize": true, "maxLength": 65000 }, "item_number": { "issueOrPRNumber": true } } }, "add_labels": { "defaultMax": 5, "fields": { "item_number": { "issueOrPRNumber": true }, "labels": { "required": true, "type": "array", "itemType": "string", "itemSanitize": true, "itemMaxLength": 128 } } }, "missing_tool": { "defaultMax": 20, "fields": { "alternatives": { "type": "string", "sanitize": true, "maxLength": 512 }, "reason": { "required": true, "type": "string", "sanitize": true, "maxLength": 256 }, "tool": { "type": "string", "sanitize": true, "maxLength": 128 } } }, "noop": { "defaultMax": 1, "fields": { "message": { "required": true, "type": "string", "sanitize": true, "maxLength": 65000 } } } } EOF - name: Generate Safe Outputs MCP Server Config id: safe-outputs-config run: | # Generate a secure random API key (360 bits of entropy, 40+ chars) # Mask immediately to prevent timing vulnerabilities API_KEY=$(openssl rand -base64 45 | tr -d '/+=') echo "::add-mask::${API_KEY}" PORT=3001 # Set outputs for next steps { echo "safe_outputs_api_key=${API_KEY}" echo "safe_outputs_port=${PORT}" } >> "$GITHUB_OUTPUT" echo "Safe Outputs MCP server will run on port ${PORT}" - name: Start Safe Outputs MCP HTTP Server id: safe-outputs-start env: DEBUG: '*' GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-config.outputs.safe_outputs_port }} GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-config.outputs.safe_outputs_api_key }} GH_AW_SAFE_OUTPUTS_TOOLS_PATH: /opt/gh-aw/safeoutputs/tools.json GH_AW_SAFE_OUTPUTS_CONFIG_PATH: /opt/gh-aw/safeoutputs/config.json GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs run: | # Environment variables are set above to prevent template injection export DEBUG export GH_AW_SAFE_OUTPUTS_PORT export GH_AW_SAFE_OUTPUTS_API_KEY export GH_AW_SAFE_OUTPUTS_TOOLS_PATH export GH_AW_SAFE_OUTPUTS_CONFIG_PATH export GH_AW_MCP_LOG_DIR bash /opt/gh-aw/actions/start_safe_outputs_server.sh - name: Start MCP gateway id: start-mcp-gateway env: GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-start.outputs.api_key }} GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-start.outputs.port }} GITHUB_MCP_LOCKDOWN: ${{ steps.determine-automatic-lockdown.outputs.lockdown == 'true' && '1' || '0' }} GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} run: | set -eo pipefail mkdir -p /tmp/gh-aw/mcp-config # Export gateway environment variables for MCP config and gateway script export MCP_GATEWAY_PORT="80" export MCP_GATEWAY_DOMAIN="host.docker.internal" MCP_GATEWAY_API_KEY=$(openssl rand -base64 45 | tr -d '/+=') echo "::add-mask::${MCP_GATEWAY_API_KEY}" export MCP_GATEWAY_API_KEY export MCP_GATEWAY_PAYLOAD_DIR="/tmp/gh-aw/mcp-payloads" mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}" export DEBUG="*" export GH_AW_ENGINE="copilot" export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_LOCKDOWN -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.0.113' mkdir -p /home/runner/.copilot cat << MCPCONFIG_EOF | bash /opt/gh-aw/actions/start_mcp_gateway.sh { "mcpServers": { "github": { "type": "stdio", "container": "ghcr.io/github/github-mcp-server:v0.30.3", "env": { "GITHUB_LOCKDOWN_MODE": "$GITHUB_MCP_LOCKDOWN", "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_MCP_SERVER_TOKEN}", "GITHUB_READ_ONLY": "1", "GITHUB_TOOLSETS": "issues" } }, "safeoutputs": { "type": "http", "url": "http://host.docker.internal:$GH_AW_SAFE_OUTPUTS_PORT", "headers": { "Authorization": "\${GH_AW_SAFE_OUTPUTS_API_KEY}" } } }, "gateway": { "port": $MCP_GATEWAY_PORT, "domain": "${MCP_GATEWAY_DOMAIN}", "apiKey": "${MCP_GATEWAY_API_KEY}", "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } MCPCONFIG_EOF - name: Generate workflow overview uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: script: | const { generateWorkflowOverview } = require('/opt/gh-aw/actions/generate_workflow_overview.cjs'); await generateWorkflowOverview(core); - name: Create prompt with built-in context env: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} GH_AW_GITHUB_ACTOR: ${{ github.actor }} GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }} GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} run: | bash /opt/gh-aw/actions/create_prompt_first.sh cat << 'PROMPT_EOF' > "$GH_AW_PROMPT" PROMPT_EOF cat "/opt/gh-aw/prompts/temp_folder_prompt.md" >> "$GH_AW_PROMPT" cat "/opt/gh-aw/prompts/markdown.md" >> "$GH_AW_PROMPT" cat << 'PROMPT_EOF' >> "$GH_AW_PROMPT" GitHub API Access Instructions The gh CLI is NOT authenticated. Do NOT use gh commands for GitHub operations. To create or modify GitHub resources (issues, discussions, pull requests, etc.), you MUST call the appropriate safe output tool. Simply writing content will NOT work - the workflow requires actual tool calls. Discover available tools from the safeoutputs MCP server. **Critical**: Tool calls write structured data that downstream jobs process. Without tool calls, follow-up actions will be skipped. **Note**: If you made no other safe output tool calls during this workflow execution, call the "noop" tool to provide a status message indicating completion or that no actions were needed. The following GitHub context information is available for this workflow: {{#if __GH_AW_GITHUB_ACTOR__ }} - **actor**: __GH_AW_GITHUB_ACTOR__ {{/if}} {{#if __GH_AW_GITHUB_REPOSITORY__ }} - **repository**: __GH_AW_GITHUB_REPOSITORY__ {{/if}} {{#if __GH_AW_GITHUB_WORKSPACE__ }} - **workspace**: __GH_AW_GITHUB_WORKSPACE__ {{/if}} {{#if __GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ }} - **issue-number**: #__GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ {{/if}} {{#if __GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ }} - **discussion-number**: #__GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ {{/if}} {{#if __GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ }} - **pull-request-number**: #__GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ {{/if}} {{#if __GH_AW_GITHUB_EVENT_COMMENT_ID__ }} - **comment-id**: __GH_AW_GITHUB_EVENT_COMMENT_ID__ {{/if}} {{#if __GH_AW_GITHUB_RUN_ID__ }} - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__ {{/if}} PROMPT_EOF cat << 'PROMPT_EOF' >> "$GH_AW_PROMPT" PROMPT_EOF cat << 'PROMPT_EOF' >> "$GH_AW_PROMPT" {{#runtime-import .github/workflows/issue-triage.md}} PROMPT_EOF - name: Substitute placeholders uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GH_AW_GITHUB_ACTOR: ${{ github.actor }} GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }} GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} with: script: | const substitutePlaceholders = require('/opt/gh-aw/actions/substitute_placeholders.cjs'); // Call the substitution function return await substitutePlaceholders({ file: process.env.GH_AW_PROMPT, substitutions: { GH_AW_GITHUB_ACTOR: process.env.GH_AW_GITHUB_ACTOR, GH_AW_GITHUB_EVENT_COMMENT_ID: process.env.GH_AW_GITHUB_EVENT_COMMENT_ID, GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: process.env.GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER, GH_AW_GITHUB_EVENT_ISSUE_NUMBER: process.env.GH_AW_GITHUB_EVENT_ISSUE_NUMBER, GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: process.env.GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER, GH_AW_GITHUB_REPOSITORY: process.env.GH_AW_GITHUB_REPOSITORY, GH_AW_GITHUB_RUN_ID: process.env.GH_AW_GITHUB_RUN_ID, GH_AW_GITHUB_WORKSPACE: process.env.GH_AW_GITHUB_WORKSPACE } }); - name: Interpolate variables and render templates uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt with: script: | const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); setupGlobals(core, github, context, exec, io); const { main } = require('/opt/gh-aw/actions/interpolate_prompt.cjs'); await main(); - name: Validate prompt placeholders env: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt run: bash /opt/gh-aw/actions/validate_prompt_placeholders.sh - name: Print prompt env: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt run: bash /opt/gh-aw/actions/print_prompt_summary.sh - name: Clean git credentials run: bash /opt/gh-aw/actions/clean_git_credentials.sh - name: Execute GitHub Copilot CLI id: agentic_execution # Copilot CLI tool arguments (sorted): timeout-minutes: 20 run: | set -o pipefail sudo -E awf --enable-chroot --env-all --container-workdir "${GITHUB_WORKSPACE}" --allow-domains '*.githubusercontent.com,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,codeload.github.com,github-cloud.githubusercontent.com,github-cloud.s3.amazonaws.com,github.com,github.githubassets.com,host.docker.internal,lfs.github.com,objects.githubusercontent.com,raw.githubusercontent.com,registry.npmjs.org,telemetry.enterprise.githubcopilot.com' --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --enable-host-access --image-tag 0.13.12 --skip-pull \ -- '/usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --add-dir "${GITHUB_WORKSPACE}" --disable-builtin-mcps --allow-all-tools --allow-all-paths --share /tmp/gh-aw/sandbox/agent/logs/conversation.md --prompt "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"${GH_AW_MODEL_AGENT_COPILOT:+ --model "$GH_AW_MODEL_AGENT_COPILOT"}' \ 2>&1 | tee /tmp/gh-aw/agent-stdio.log env: COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} GH_AW_MCP_CONFIG: /home/runner/.copilot/mcp-config.json GH_AW_MODEL_AGENT_COPILOT: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || '' }} GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} GITHUB_HEAD_REF: ${{ github.head_ref }} GITHUB_REF_NAME: ${{ github.ref_name }} GITHUB_STEP_SUMMARY: ${{ env.GITHUB_STEP_SUMMARY }} GITHUB_WORKSPACE: ${{ github.workspace }} XDG_CONFIG_HOME: /home/runner - name: Configure Git credentials env: REPO_NAME: ${{ github.repository }} SERVER_URL: ${{ github.server_url }} run: | git config --global user.email "github-actions[bot]@users.noreply.github.com" git config --global user.name "github-actions[bot]" # Re-authenticate git with GitHub token SERVER_URL_STRIPPED="${SERVER_URL#https://}" git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" echo "Git configured with standard GitHub Actions identity" - name: Copy Copilot session state files to logs if: always() continue-on-error: true run: | # Copy Copilot session state files to logs folder for artifact collection # This ensures they are in /tmp/gh-aw/ where secret redaction can scan them SESSION_STATE_DIR="$HOME/.copilot/session-state" LOGS_DIR="/tmp/gh-aw/sandbox/agent/logs" if [ -d "$SESSION_STATE_DIR" ]; then echo "Copying Copilot session state files from $SESSION_STATE_DIR to $LOGS_DIR" mkdir -p "$LOGS_DIR" cp -v "$SESSION_STATE_DIR"/*.jsonl "$LOGS_DIR/" 2>/dev/null || true echo "Session state files copied successfully" else echo "No session-state directory found at $SESSION_STATE_DIR" fi - name: Stop MCP gateway if: always() continue-on-error: true env: MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} GATEWAY_PID: ${{ steps.start-mcp-gateway.outputs.gateway-pid }} run: | bash /opt/gh-aw/actions/stop_mcp_gateway.sh "$GATEWAY_PID" - name: Redact secrets in logs if: always() uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: script: | const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); setupGlobals(core, github, context, exec, io); const { main } = require('/opt/gh-aw/actions/redact_secrets.cjs'); await main(); env: GH_AW_SECRET_NAMES: 'COPILOT_GITHUB_TOKEN,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' SECRET_COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} SECRET_GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} SECRET_GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} SECRET_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Upload Safe Outputs if: always() uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: safe-output path: ${{ env.GH_AW_SAFE_OUTPUTS }} if-no-files-found: warn - name: Ingest agent output id: collect_output uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} GH_AW_ALLOWED_DOMAINS: "*.githubusercontent.com,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,codeload.github.com,github-cloud.githubusercontent.com,github-cloud.s3.amazonaws.com,github.com,github.githubassets.com,host.docker.internal,lfs.github.com,objects.githubusercontent.com,raw.githubusercontent.com,registry.npmjs.org,telemetry.enterprise.githubcopilot.com" GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_API_URL: ${{ github.api_url }} with: script: | const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); setupGlobals(core, github, context, exec, io); const { main } = require('/opt/gh-aw/actions/collect_ndjson_output.cjs'); await main(); - name: Upload sanitized agent output if: always() && env.GH_AW_AGENT_OUTPUT uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: agent-output path: ${{ env.GH_AW_AGENT_OUTPUT }} if-no-files-found: warn - name: Upload engine output files uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: agent_outputs path: | /tmp/gh-aw/sandbox/agent/logs/ /tmp/gh-aw/redacted-urls.log if-no-files-found: ignore - name: Parse agent logs for step summary if: always() uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: GH_AW_AGENT_OUTPUT: /tmp/gh-aw/sandbox/agent/logs/ with: script: | const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); setupGlobals(core, github, context, exec, io); const { main } = require('/opt/gh-aw/actions/parse_copilot_log.cjs'); await main(); - name: Parse MCP gateway logs for step summary if: always() uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: script: | const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); setupGlobals(core, github, context, exec, io); const { main } = require('/opt/gh-aw/actions/parse_mcp_gateway_log.cjs'); await main(); - name: Print firewall logs if: always() continue-on-error: true env: AWF_LOGS_DIR: /tmp/gh-aw/sandbox/firewall/logs run: | # Fix permissions on firewall logs so they can be uploaded as artifacts # AWF runs with sudo, creating files owned by root sudo chmod -R a+r /tmp/gh-aw/sandbox/firewall/logs 2>/dev/null || true awf logs summary | tee -a "$GITHUB_STEP_SUMMARY" - name: Upload agent artifacts if: always() continue-on-error: true uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: agent-artifacts path: | /tmp/gh-aw/aw-prompts/prompt.txt /tmp/gh-aw/aw_info.json /tmp/gh-aw/mcp-logs/ /tmp/gh-aw/sandbox/firewall/logs/ /tmp/gh-aw/agent-stdio.log /tmp/gh-aw/agent/ if-no-files-found: ignore conclusion: needs: - activation - agent - detection - safe_outputs if: (always()) && (needs.agent.result != 'skipped') runs-on: ubuntu-slim permissions: contents: read discussions: write issues: write pull-requests: write outputs: noop_message: ${{ steps.noop.outputs.noop_message }} tools_reported: ${{ steps.missing_tool.outputs.tools_reported }} total_count: ${{ steps.missing_tool.outputs.total_count }} steps: - name: Setup Scripts uses: github/gh-aw/actions/setup@c549967b5808d783bc1537cf5687896de4dec7ee # v0.43.0 with: destination: /opt/gh-aw/actions - name: Debug job inputs env: COMMENT_ID: ${{ needs.activation.outputs.comment_id }} COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} AGENT_OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} AGENT_CONCLUSION: ${{ needs.agent.result }} run: | echo "Comment ID: $COMMENT_ID" echo "Comment Repo: $COMMENT_REPO" echo "Agent Output Types: $AGENT_OUTPUT_TYPES" echo "Agent Conclusion: $AGENT_CONCLUSION" - name: Download agent output artifact continue-on-error: true uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 with: name: agent-output path: /tmp/gh-aw/safeoutputs/ - name: Setup agent output environment variable run: | mkdir -p /tmp/gh-aw/safeoutputs/ find "/tmp/gh-aw/safeoutputs/" -type f -print echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" - name: Process No-Op Messages id: noop uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} GH_AW_NOOP_MAX: 1 GH_AW_WORKFLOW_NAME: "Apollo Issue Triage" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); setupGlobals(core, github, context, exec, io); const { main } = require('/opt/gh-aw/actions/noop.cjs'); await main(); - name: Record Missing Tool id: missing_tool uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} GH_AW_WORKFLOW_NAME: "Apollo Issue Triage" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); setupGlobals(core, github, context, exec, io); const { main } = require('/opt/gh-aw/actions/missing_tool.cjs'); await main(); - name: Handle Agent Failure id: handle_agent_failure uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} GH_AW_WORKFLOW_NAME: "Apollo Issue Triage" GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} GH_AW_SECRET_VERIFICATION_RESULT: ${{ needs.agent.outputs.secret_verification_result }} GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); setupGlobals(core, github, context, exec, io); const { main } = require('/opt/gh-aw/actions/handle_agent_failure.cjs'); await main(); - name: Handle No-Op Message id: handle_noop_message uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} GH_AW_WORKFLOW_NAME: "Apollo Issue Triage" GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} GH_AW_NOOP_MESSAGE: ${{ steps.noop.outputs.noop_message }} GH_AW_NOOP_REPORT_AS_ISSUE: "true" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); setupGlobals(core, github, context, exec, io); const { main } = require('/opt/gh-aw/actions/handle_noop_message.cjs'); await main(); - name: Update reaction comment with completion status id: conclusion uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} GH_AW_COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} GH_AW_WORKFLOW_NAME: "Apollo Issue Triage" GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.result }} with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); setupGlobals(core, github, context, exec, io); const { main } = require('/opt/gh-aw/actions/notify_comment_error.cjs'); await main(); detection: needs: agent if: needs.agent.outputs.output_types != '' || needs.agent.outputs.has_patch == 'true' runs-on: ubuntu-latest permissions: {} timeout-minutes: 10 outputs: success: ${{ steps.parse_results.outputs.success }} steps: - name: Setup Scripts uses: github/gh-aw/actions/setup@c549967b5808d783bc1537cf5687896de4dec7ee # v0.43.0 with: destination: /opt/gh-aw/actions - name: Download agent artifacts continue-on-error: true uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 with: name: agent-artifacts path: /tmp/gh-aw/threat-detection/ - name: Download agent output artifact continue-on-error: true uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 with: name: agent-output path: /tmp/gh-aw/threat-detection/ - name: Echo agent output types env: AGENT_OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} run: | echo "Agent output-types: $AGENT_OUTPUT_TYPES" - name: Setup threat detection uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: WORKFLOW_NAME: "Apollo Issue Triage" WORKFLOW_DESCRIPTION: "No description provided" HAS_PATCH: ${{ needs.agent.outputs.has_patch }} with: script: | const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); setupGlobals(core, github, context, exec, io); const { main } = require('/opt/gh-aw/actions/setup_threat_detection.cjs'); await main(); - name: Ensure threat-detection directory and log run: | mkdir -p /tmp/gh-aw/threat-detection touch /tmp/gh-aw/threat-detection/detection.log - name: Validate COPILOT_GITHUB_TOKEN secret id: validate-secret run: /opt/gh-aw/actions/validate_multi_secret.sh COPILOT_GITHUB_TOKEN 'GitHub Copilot CLI' https://github.github.com/gh-aw/reference/engines/#github-copilot-default env: COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - name: Install GitHub Copilot CLI run: /opt/gh-aw/actions/install_copilot_cli.sh 0.0.405 - name: Execute GitHub Copilot CLI id: agentic_execution # Copilot CLI tool arguments (sorted): # --allow-tool shell(cat) # --allow-tool shell(grep) # --allow-tool shell(head) # --allow-tool shell(jq) # --allow-tool shell(ls) # --allow-tool shell(tail) # --allow-tool shell(wc) timeout-minutes: 20 run: | set -o pipefail COPILOT_CLI_INSTRUCTION="$(cat /tmp/gh-aw/aw-prompts/prompt.txt)" mkdir -p /tmp/ mkdir -p /tmp/gh-aw/ mkdir -p /tmp/gh-aw/agent/ mkdir -p /tmp/gh-aw/sandbox/agent/logs/ copilot --add-dir /tmp/ --add-dir /tmp/gh-aw/ --add-dir /tmp/gh-aw/agent/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --allow-tool 'shell(cat)' --allow-tool 'shell(grep)' --allow-tool 'shell(head)' --allow-tool 'shell(jq)' --allow-tool 'shell(ls)' --allow-tool 'shell(tail)' --allow-tool 'shell(wc)' --share /tmp/gh-aw/sandbox/agent/logs/conversation.md --prompt "$COPILOT_CLI_INSTRUCTION"${GH_AW_MODEL_DETECTION_COPILOT:+ --model "$GH_AW_MODEL_DETECTION_COPILOT"} 2>&1 | tee /tmp/gh-aw/threat-detection/detection.log env: COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} GH_AW_MODEL_DETECTION_COPILOT: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || '' }} GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GITHUB_HEAD_REF: ${{ github.head_ref }} GITHUB_REF_NAME: ${{ github.ref_name }} GITHUB_STEP_SUMMARY: ${{ env.GITHUB_STEP_SUMMARY }} GITHUB_WORKSPACE: ${{ github.workspace }} XDG_CONFIG_HOME: /home/runner - name: Parse threat detection results id: parse_results uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: script: | const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); setupGlobals(core, github, context, exec, io); const { main } = require('/opt/gh-aw/actions/parse_threat_detection_results.cjs'); await main(); - name: Upload threat detection log if: always() uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: threat-detection.log path: /tmp/gh-aw/threat-detection/detection.log if-no-files-found: ignore safe_outputs: needs: - agent - detection if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (needs.detection.outputs.success == 'true') runs-on: ubuntu-slim permissions: contents: read discussions: write issues: write pull-requests: write timeout-minutes: 15 env: GH_AW_ENGINE_ID: "copilot" GH_AW_WORKFLOW_ID: "issue-triage" GH_AW_WORKFLOW_NAME: "Apollo Issue Triage" outputs: create_discussion_error_count: ${{ steps.process_safe_outputs.outputs.create_discussion_error_count }} create_discussion_errors: ${{ steps.process_safe_outputs.outputs.create_discussion_errors }} process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} steps: - name: Setup Scripts uses: github/gh-aw/actions/setup@c549967b5808d783bc1537cf5687896de4dec7ee # v0.43.0 with: destination: /opt/gh-aw/actions - name: Download agent output artifact continue-on-error: true uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 with: name: agent-output path: /tmp/gh-aw/safeoutputs/ - name: Setup agent output environment variable run: | mkdir -p /tmp/gh-aw/safeoutputs/ find "/tmp/gh-aw/safeoutputs/" -type f -print echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" - name: Process Safe Outputs id: process_safe_outputs uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":1},\"add_labels\":{\"max\":3},\"missing_data\":{},\"missing_tool\":{}}" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); setupGlobals(core, github, context, exec, io); const { main } = require('/opt/gh-aw/actions/safe_output_handler_manager.cjs'); await main(); ================================================ FILE: .github/workflows/issue-triage.md ================================================ --- name: Apollo Issue Triage on: issues: types: [opened] permissions: read-all roles: all network: allowed: - github tools: github: toolsets: - issues safe-outputs: add-labels: max: 3 add-comment: max: 1 mentions: allow-team-members: false allow-context: false allowed: [] concurrency: group: apollo-issue-triage-${{ github.event.issue.number }} cancel-in-progress: true --- # Apollo Issue Triage Assistant You are the issue triage assistant for `apolloconfig/apollo`. Your job is to classify newly opened issues, add minimal labels, and ask for any missing information. ## Hard Safety Rules 1. Never close issues. 2. Never remove labels. 3. Never ask users to share secrets, passwords, access tokens, private keys, connection strings, or internal URLs. 4. Never claim a root cause without clear evidence from issue content. 5. If uncertain, say you are uncertain and request only the minimum missing details. ## Label Policy (Apollo-aligned) Add at most 3 labels total. Prefer existing labels only. ### Primary type labels - Bug or regression: - `bug` - `kind/report-problem` - Question or support: - `discussion` - `kind/question` - Feature request: - `feature request` - Dependency/security advisory update: - `dependencies` - `kind/dependencies` - Unclear but potentially valid: - `need investigation` ### Area labels (optional, choose at most 1) - Config service keywords: `area/configservice` - Admin service keywords: `area/adminservice` - Portal keywords: `area/portal` - OpenAPI keywords: `area/openapi` - SDK/client keywords: `area/sdk` or `area/client` - Docker keywords: `area/docker` - Kubernetes keywords: `area/kubernetes` - MySQL keywords: `area/mysql` - UI keywords: `area/ui` - Security keywords: `area/security` ### Missing info label If critical details are missing for bug reports, add: - `status/need-feedback` Critical details: - Apollo version or commit - runtime/deployment environment (JDK, OS, DB mode) - minimal reproduction steps - expected vs actual behavior - relevant error logs (with sensitive values redacted) ## Security Issue Handling If the issue appears to describe a vulnerability (for example, credential leak, RCE, SQL injection, auth bypass): 1. Add `area/security` and `need investigation`. 2. Do not request exploit details publicly. 3. Ask the reporter to follow the private disclosure process in `SECURITY.md`. ## Language Handling If issue title/body is mainly Chinese, reply in Chinese. Otherwise reply in English. ## Comment Style Rules 1. Post exactly one concise triage comment. 2. Keep comments actionable and neutral. 3. If issue content is already sufficient, acknowledge and avoid asking repetitive questions. 4. Prefix comment with ``. ## Comment Templates Use one of the following templates and tailor it to the issue. ### Chinese template 感谢反馈。为便于维护者快速定位,请补充以下最小信息(如已提供可忽略): 1. Apollo 版本号或 commit 2. 部署与运行环境(JDK、OS、MySQL/H2、部署模式) 3. 最小复现步骤 4. 期望结果与实际结果 5. 相关错误日志(请先脱敏,不要包含密钥/密码/Token) 收到后我们会继续跟进。 ### English template Thanks for opening this issue. To help maintainers triage quickly, please share the minimum details below (skip items already provided): 1. Apollo version or commit 2. Deployment/runtime environment (JDK, OS, MySQL/H2, deployment mode) 3. Minimal reproduction steps 4. Expected behavior vs actual behavior 5. Relevant error logs (please redact secrets, passwords, and tokens) Once available, maintainers can follow up faster. ## Output Actions For every run: 1. Add labels according to policy (max 3). 2. Add one triage comment in the same language as the issue. ================================================ FILE: .github/workflows/javascript-test.yml ================================================ # # Copyright 2025 Apollo Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # This workflow runs JavaScript tests in apollo-portal static/scripts directory. # Tests are discovered automatically by pattern: test_*.js name: JavaScript tests on: push: branches: [master] paths: - 'apollo-portal/src/main/resources/static/scripts/**/*.js' - 'apollo-portal/src/test/resources/static/scripts/test_*.js' - '.github/workflows/javascript-test.yml' pull_request: branches: [master] paths: - 'apollo-portal/src/main/resources/static/scripts/**/*.js' - 'apollo-portal/src/test/resources/static/scripts/test_*.js' - '.github/workflows/javascript-test.yml' jobs: js-test: runs-on: ubuntu-latest permissions: contents: read steps: - uses: actions/checkout@v4 - name: Set up Node.js uses: actions/setup-node@v4 with: node-version: '20' - name: Find and run JavaScript tests run: | TEST_DIR="apollo-portal/src/test/resources/static/scripts" if [ ! -d "$TEST_DIR" ]; then echo "WARNING: Test directory $TEST_DIR does not exist yet. Skipping JS tests." exit 0 fi TEST_FILES=$(find "$TEST_DIR" -name 'test_*.js' -type f) if [ -z "$TEST_FILES" ]; then echo "ERROR: No test files found matching test_*.js in $TEST_DIR" exit 1 fi FAILED=0 while IFS= read -r f; do echo "--- Running: $f ---" node "$f" || FAILED=1 done <<< "$TEST_FILES" if [ "$FAILED" -ne 0 ]; then echo "Some tests failed." exit 1 fi ================================================ FILE: .github/workflows/license.yml ================================================ # # Copyright 2024 Apollo Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # This workflow will build a Java project with Maven # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven name: license on: push: branches: [ master ] pull_request: branches: [ master ] jobs: license: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Check License uses: apache/skywalking-eyes/header@501a28d2fb4a9b962661987e50cf0219631b32ff ================================================ FILE: .github/workflows/portal-login-e2e.yml ================================================ # # Copyright 2026 Apollo Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # name: portal-login-e2e on: pull_request: branches: [ master ] paths: - 'apollo-portal/**' - 'apollo-assembly/**' - 'e2e/portal-e2e/**' - '.github/workflows/portal-login-e2e.yml' jobs: portal-login-e2e: name: portal-login-e2e (${{ matrix.mode }}) runs-on: ubuntu-latest timeout-minutes: 120 strategy: fail-fast: false matrix: mode: [ ldap, oidc ] steps: - uses: actions/checkout@v4 - name: Set up JDK 17 uses: actions/setup-java@v4 with: distribution: temurin java-version: 17 cache: maven - name: Set up Node.js 20 uses: actions/setup-node@v4 with: node-version: 20 - name: Cache Playwright browsers uses: actions/cache@v4 with: path: ~/.cache/ms-playwright key: ${{ runner.os }}-playwright-${{ hashFiles('e2e/portal-e2e/package-lock.json') }} restore-keys: | ${{ runner.os }}-playwright- - name: Build Apollo assembly run: ./mvnw -B -pl apollo-assembly -am -DskipTests package - name: Setup auth provider env: PORTAL_BASE_URL: http://127.0.0.1:8070 LDAP_ALLOWED_USER: apollo LDAP_ALLOWED_USER_PASSWORD: admin LDAP_ALLOWED_USER_SECONDARY: devops1 LDAP_ALLOWED_USER_SECONDARY_PASSWORD: admin LDAP_ALLOWED_USER_SECONDARY_DISPLAY_NAME: Dev Ops One LDAP_ALLOWED_USER_SECONDARY_EMAIL: devops1@example.org LDAP_BLOCKED_USER: blocked1 LDAP_BLOCKED_USER_PASSWORD: admin OIDC_USERNAME: apollo OIDC_PASSWORD: admin OIDC_SECONDARY_USERNAME: oidcdev1 OIDC_SECONDARY_PASSWORD: admin OIDC_SECONDARY_EMAIL: oidcdev1@example.org OIDC_CLIENT_ID: apollo-portal OIDC_CLIENT_SECRET: apollo-secret run: ./e2e/portal-e2e/scripts/auth/setup-${{ matrix.mode }}.sh - name: Start Apollo assembly env: OIDC_ISSUER_URI: http://127.0.0.1:9080/realms/apollo OIDC_CLIENT_ID: apollo-portal OIDC_CLIENT_SECRET: apollo-secret run: | JAR="" for candidate in apollo-assembly/target/apollo-assembly-*.jar; do if [[ "$candidate" == *"-sources.jar" || "$candidate" == *"-javadoc.jar" ]]; then continue fi if [[ -f "$candidate" ]]; then JAR="$candidate" break fi done if [[ -z "$JAR" ]]; then echo "No runnable apollo-assembly jar found in apollo-assembly/target" >&2 exit 1 fi if [[ "${{ matrix.mode }}" == "ldap" ]]; then ACTIVE_PROFILES="github,database-discovery,ldap" else ACTIVE_PROFILES="github,database-discovery,oidc" fi SPRING_PROFILES_ACTIVE="${ACTIVE_PROFILES}" \ SPRING_SQL_CONFIG_INIT_MODE="always" \ SPRING_SQL_PORTAL_INIT_MODE="always" \ SPRING_CONFIG_DATASOURCE_URL="jdbc:h2:mem:apollo-config-db;mode=mysql;DB_CLOSE_ON_EXIT=FALSE;DB_CLOSE_DELAY=-1;BUILTIN_ALIAS_OVERRIDE=TRUE;DATABASE_TO_UPPER=FALSE" \ SPRING_PORTAL_DATASOURCE_URL="jdbc:h2:mem:apollo-portal-db;mode=mysql;DB_CLOSE_ON_EXIT=FALSE;DB_CLOSE_DELAY=-1;BUILTIN_ALIAS_OVERRIDE=TRUE;DATABASE_TO_UPPER=FALSE" \ SPRING_CONFIG_ADDITIONAL_LOCATION="file:${GITHUB_WORKSPACE}/e2e/portal-e2e/config/application-${{ matrix.mode }}-e2e.yml" \ java -jar "$JAR" > /tmp/apollo-assembly-run.log 2>&1 & echo $! > /tmp/apollo-assembly.pid - name: Wait for Apollo readiness env: PORTAL_AUTH_MODE: ${{ matrix.mode }} PORTAL_USERNAME: apollo PORTAL_PASSWORD: admin run: ./e2e/portal-e2e/scripts/wait-for-ready.sh - name: Run Playwright auth matrix tests env: BASE_URL: http://127.0.0.1:8070 PORTAL_AUTH_MODE: ${{ matrix.mode }} PORTAL_USERNAME: apollo PORTAL_PASSWORD: admin LDAP_ALLOWED_USER: apollo LDAP_ALLOWED_USER_PASSWORD: admin LDAP_ALLOWED_USER_SECONDARY: devops1 LDAP_ALLOWED_USER_SECONDARY_DISPLAY_NAME: Dev Ops One LDAP_ALLOWED_USER_SECONDARY_EMAIL: devops1@example.org LDAP_BLOCKED_USER: blocked1 LDAP_BLOCKED_USER_PASSWORD: admin OIDC_USERNAME: apollo OIDC_PASSWORD: admin OIDC_SECONDARY_USERNAME: oidcdev1 OIDC_SECONDARY_PASSWORD: admin OIDC_SECONDARY_EMAIL: oidcdev1@example.org OIDC_CLIENT_ID: apollo-portal OIDC_CLIENT_SECRET: apollo-secret OIDC_ISSUER_URI: http://127.0.0.1:9080/realms/apollo PLAYWRIGHT_WORKERS: 2 run: | cd e2e/portal-e2e npm ci npx playwright install chromium npm run test:e2e:auth-matrix:ci - name: Dump auth provider logs on failure if: failure() run: | docker logs apollo-e2e-ldap > /tmp/apollo-e2e-ldap.log 2>&1 || true docker logs apollo-e2e-keycloak > /tmp/apollo-e2e-keycloak.log 2>&1 || true - name: Upload auth matrix artifacts on failure if: failure() uses: actions/upload-artifact@v4 with: name: portal-login-e2e-artifacts-${{ matrix.mode }} path: | e2e/portal-e2e/playwright-report e2e/portal-e2e/test-results /tmp/apollo-assembly-run.log /tmp/apollo-e2e-ldap.log /tmp/apollo-e2e-keycloak.log - name: Stop Apollo assembly if: always() run: | if [ -f /tmp/apollo-assembly.pid ]; then kill "$(cat /tmp/apollo-assembly.pid)" || true fi - name: Teardown auth provider if: always() run: ./e2e/portal-e2e/scripts/auth/teardown-auth.sh ================================================ FILE: .github/workflows/portal-ui-e2e.yml ================================================ # # Copyright 2026 Apollo Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # name: portal-ui-e2e on: pull_request: branches: [ master ] paths: - 'apollo-portal/**' - 'apollo-assembly/**' - 'e2e/portal-e2e/**' - 'scripts/sql/**' - '.github/workflows/portal-ui-e2e.yml' jobs: portal-ui-e2e: name: portal-ui-e2e runs-on: ubuntu-latest timeout-minutes: 90 steps: - uses: actions/checkout@v4 - name: Set up JDK 17 uses: actions/setup-java@v4 with: distribution: temurin java-version: 17 cache: maven - name: Set up Node.js 20 uses: actions/setup-node@v4 with: node-version: 20 - name: Cache Playwright browsers uses: actions/cache@v4 with: path: ~/.cache/ms-playwright key: ${{ runner.os }}-playwright-${{ hashFiles('e2e/portal-e2e/package-lock.json') }} restore-keys: | ${{ runner.os }}-playwright- - name: Build Apollo assembly run: ./mvnw -B -pl apollo-assembly -am -DskipTests package - name: Start Apollo assembly run: | JAR="" for candidate in apollo-assembly/target/apollo-assembly-*.jar; do if [[ "$candidate" == *"-sources.jar" || "$candidate" == *"-javadoc.jar" ]]; then continue fi if [[ -f "$candidate" ]]; then JAR="$candidate" break fi done if [[ -z "$JAR" ]]; then echo "No runnable apollo-assembly jar found in apollo-assembly/target" >&2 exit 1 fi SPRING_PROFILES_ACTIVE="github,database-discovery,auth" \ SPRING_SQL_CONFIG_INIT_MODE="always" \ SPRING_SQL_PORTAL_INIT_MODE="always" \ SPRING_CONFIG_DATASOURCE_URL="jdbc:h2:mem:apollo-config-db;mode=mysql;DB_CLOSE_ON_EXIT=FALSE;DB_CLOSE_DELAY=-1;BUILTIN_ALIAS_OVERRIDE=TRUE;DATABASE_TO_UPPER=FALSE" \ SPRING_PORTAL_DATASOURCE_URL="jdbc:h2:mem:apollo-portal-db;mode=mysql;DB_CLOSE_ON_EXIT=FALSE;DB_CLOSE_DELAY=-1;BUILTIN_ALIAS_OVERRIDE=TRUE;DATABASE_TO_UPPER=FALSE" \ java -jar "$JAR" > /tmp/apollo-assembly-run.log 2>&1 & echo $! > /tmp/apollo-assembly.pid - name: Wait for Apollo readiness run: ./e2e/portal-e2e/scripts/wait-for-ready.sh - name: Run Playwright UI tests env: BASE_URL: http://127.0.0.1:8070 PLAYWRIGHT_WORKERS: 2 run: | cd e2e/portal-e2e npm ci npx playwright install chromium npm run test:e2e:ci - name: Upload e2e test artifacts on failure if: failure() uses: actions/upload-artifact@v4 with: name: portal-ui-e2e-artifacts path: | e2e/portal-e2e/playwright-report e2e/portal-e2e/test-results /tmp/apollo-assembly-run.log - name: Stop Apollo assembly if: always() run: | if [ -f /tmp/apollo-assembly.pid ]; then kill "$(cat /tmp/apollo-assembly.pid)" || true fi ================================================ FILE: .github/workflows/release-packages.yml ================================================ # # Copyright 2024 Apollo Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # name: Release Packages on: workflow_dispatch: inputs: release_tag: description: 'release tag, e.g. v2.5.0' required: true skip_version_check: description: 'skip release_tag and pom revision consistency check' required: false default: 'false' permissions: contents: write jobs: package: runs-on: ubuntu-latest timeout-minutes: 90 steps: - name: Checkout uses: actions/checkout@v4 with: ref: ${{ inputs.release_tag }} - name: Set up JDK 17 uses: actions/setup-java@v4 with: distribution: temurin java-version: 17 - name: Cache Maven packages uses: actions/cache@v4 with: path: ~/.m2/repository key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} restore-keys: | ${{ runner.os }}-maven- - name: Build package run: ./scripts/build.sh - name: Resolve project version from pom revision id: project-version run: | set -euo pipefail VERSION=$(mvn -q -N help:evaluate -Dexpression=revision -DforceStdout | tail -n 1 | tr -d '\r') if [[ -z "${VERSION}" ]]; then echo "Failed to resolve revision from pom.xml" exit 1 fi echo "version=${VERSION}" >> "${GITHUB_OUTPUT}" - name: Generate checksums env: VERSION: ${{ steps.project-version.outputs.version }} run: | set -euo pipefail (cd "apollo-configservice/target" && shasum "apollo-configservice-${VERSION}-github.zip") > "apollo-configservice/target/apollo-configservice-${VERSION}-github.zip.sha1" (cd "apollo-adminservice/target" && shasum "apollo-adminservice-${VERSION}-github.zip") > "apollo-adminservice/target/apollo-adminservice-${VERSION}-github.zip.sha1" (cd "apollo-portal/target" && shasum "apollo-portal-${VERSION}-github.zip") > "apollo-portal/target/apollo-portal-${VERSION}-github.zip.sha1" - name: Verify release and artifacts env: GH_TOKEN: ${{ github.token }} VERSION: ${{ steps.project-version.outputs.version }} RELEASE_TAG: ${{ inputs.release_tag }} SKIP_VERSION_CHECK: ${{ inputs.skip_version_check }} run: | set -euo pipefail if [[ "${SKIP_VERSION_CHECK,,}" != "true" ]]; then TAG_VERSION="${RELEASE_TAG#v}" if [[ "${TAG_VERSION}" != "${VERSION}" ]]; then echo "Release tag (${RELEASE_TAG}) does not match pom revision (${VERSION})" exit 1 fi else echo "Skip version consistency check by input: skip_version_check=${SKIP_VERSION_CHECK}" fi gh release view "${RELEASE_TAG}" --repo apolloconfig/apollo >/dev/null test -f "apollo-configservice/target/apollo-configservice-${VERSION}-github.zip" test -f "apollo-configservice/target/apollo-configservice-${VERSION}-github.zip.sha1" test -f "apollo-adminservice/target/apollo-adminservice-${VERSION}-github.zip" test -f "apollo-adminservice/target/apollo-adminservice-${VERSION}-github.zip.sha1" test -f "apollo-portal/target/apollo-portal-${VERSION}-github.zip" test -f "apollo-portal/target/apollo-portal-${VERSION}-github.zip.sha1" - name: Upload release assets env: GH_TOKEN: ${{ github.token }} VERSION: ${{ steps.project-version.outputs.version }} RELEASE_TAG: ${{ inputs.release_tag }} run: | set -euo pipefail gh release upload "${RELEASE_TAG}" \ "apollo-configservice/target/apollo-configservice-${VERSION}-github.zip" \ "apollo-configservice/target/apollo-configservice-${VERSION}-github.zip.sha1" \ "apollo-adminservice/target/apollo-adminservice-${VERSION}-github.zip" \ "apollo-adminservice/target/apollo-adminservice-${VERSION}-github.zip.sha1" \ "apollo-portal/target/apollo-portal-${VERSION}-github.zip" \ "apollo-portal/target/apollo-portal-${VERSION}-github.zip.sha1" \ --repo apolloconfig/apollo \ --clobber ================================================ FILE: .gitignore ================================================ *.class .DS_Store application.pid # Mobile Tools for Java (J2ME) .mtj.tmp/ # Package Files # *.jar *.war *.ear # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* # Eclipse .classpath .project target .settings # Idea .idea *.iml # git *.orig .flattened-pom.xml ================================================ FILE: .licenserc.yaml ================================================ # # Copyright 2024 Apollo Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # header: license: spdx-id: Apache-2.0 copyright-owner: Apollo Authors paths-ignore: - '.github/**' - '**/.gitignore' - '**/.helmignore' - '.gitmodules' - '.gitattributes' - '.mvn' - '**/*.md' - '**/*.svg' - '**/*.json' - '**/*.conf' - '**/*.ftl' - '**/*.iml' - '**/*.tpl' - '**/*.factories' - '**/*.handlers' - '**/*.schemas' - '**/*.nojekyll' - 'mvnw' - 'mvnw.cmd' - '**/target/**' - 'LICENSE' - 'NOTICE' - 'CNAME' - '**/resources/META-INF/services/**' - '**/vendor/**' - 'apollo-buildtools/src/main/resources/google_checks.xml' - 'apollo-core/src/test/resources/META-INF/app-with-utf8bom.properties' - 'apollo-core/src/test/resources/properties/server-with-utf8bom.properties' - 'apollo-audit/apollo-audit-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports' comment: on-failure license-location-threshold: 130 ================================================ FILE: .mergify.yml ================================================ # # Copyright 2026 Apollo Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # merge_queue: max_parallel_checks: 1 queue_rules: - name: single-commit autoqueue: true batch_size: 1 merge_method: rebase queue_conditions: &single_commit_conditions - "base = master" - "-draft" - "-closed" - "-conflict" - "#approved-reviews-by >= 1" - "#changes-requested-reviews-by = 0" - "#commits = 1" - "check-success = build (21)" - "check-success = build (17)" - "check-success = code-style-check" - "check-success = license" - "check-success = CLAssistant" merge_conditions: *single_commit_conditions - name: multi-commit autoqueue: true batch_size: 1 merge_method: squash queue_conditions: &multi_commit_conditions - "base = master" - "-draft" - "-closed" - "-conflict" - "#approved-reviews-by >= 1" - "#changes-requested-reviews-by = 0" - "#commits > 1" - "check-success = build (21)" - "check-success = build (17)" - "check-success = code-style-check" - "check-success = license" - "check-success = CLAssistant" merge_conditions: *multi_commit_conditions pull_request_rules: - name: notify author when PR has conflicts conditions: - "conflict" - "-closed" actions: comment: message: "@{{author}} This pull request has conflicts with the target branch. Please resolve them and update the branch before merging." ================================================ FILE: .mvn/wrapper/MavenWrapperDownloader.java ================================================ /* Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ import java.net.*; import java.io.*; import java.nio.channels.*; import java.util.Properties; public class MavenWrapperDownloader { /** * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. */ private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar"; /** * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to * use instead of the default one. */ private static final String MAVEN_WRAPPER_PROPERTIES_PATH = ".mvn/wrapper/maven-wrapper.properties"; /** * Path where the maven-wrapper.jar will be saved to. */ private static final String MAVEN_WRAPPER_JAR_PATH = ".mvn/wrapper/maven-wrapper.jar"; /** * Name of the property which should be used to override the default download url for the wrapper. */ private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; public static void main(String args[]) { System.out.println("- Downloader started"); File baseDirectory = new File(args[0]); System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); // If the maven-wrapper.properties exists, read it and check if it contains a custom // wrapperUrl parameter. File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); String url = DEFAULT_DOWNLOAD_URL; if(mavenWrapperPropertyFile.exists()) { FileInputStream mavenWrapperPropertyFileInputStream = null; try { mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); Properties mavenWrapperProperties = new Properties(); mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); } catch (IOException e) { System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); } finally { try { if(mavenWrapperPropertyFileInputStream != null) { mavenWrapperPropertyFileInputStream.close(); } } catch (IOException e) { // Ignore ... } } } System.out.println("- Downloading from: : " + url); File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); if(!outputFile.getParentFile().exists()) { if(!outputFile.getParentFile().mkdirs()) { System.out.println( "- ERROR creating output direcrory '" + outputFile.getParentFile().getAbsolutePath() + "'"); } } System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); try { downloadFileFromURL(url, outputFile); System.out.println("Done"); System.exit(0); } catch (Throwable e) { System.out.println("- Error downloading"); e.printStackTrace(); System.exit(1); } } private static void downloadFileFromURL(String urlString, File destination) throws Exception { URL website = new URL(urlString); ReadableByteChannel rbc; rbc = Channels.newChannel(website.openStream()); FileOutputStream fos = new FileOutputStream(destination); fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); fos.close(); rbc.close(); } } ================================================ FILE: .mvn/wrapper/maven-wrapper.properties ================================================ distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.5.4/apache-maven-3.5.4-bin.zip ================================================ FILE: AGENTS.md ================================================ # Repository Guidelines ## Project Structure & Module Organization - Multi-module Maven repo with root `pom.xml` and service modules like `apollo-configservice`, `apollo-adminservice`, and `apollo-portal`, shared libs in `apollo-common`, and packaging in `apollo-assembly`. - `apollo-biz` is a shared business-logic module used by services like config/admin (not a standalone service). - Build and release tooling lives in `scripts/` (e.g., `scripts/build.sh`) and `apollo-buildtools/` (code style configs). - Database and schema assets are under `scripts/sql` and module `src/main/resources` folders. - Tests follow standard Maven layout: `*/src/test/java` and `*/src/test/resources` inside each module. - Documentation is in `docs/`. ## Build, Test, and Development Commands - `./mvnw -DskipTests package` builds all modules. - `./mvnw test` runs the full test suite via Surefire (may log warnings if local meta/admin services are not running). - `./mvnw -pl apollo-configservice -am test` runs tests for a specific module and its dependencies. - `./mvnw spotless:apply` formats code and must be run before opening a PR. - `./scripts/build.sh` generates distributable packages (used for deployment workflows). ## Coding Style & Naming Conventions - Follow Google Java Style Guide; 2-space indentation and standard Java conventions apply. - Use the IDE configs in `apollo-buildtools/style/` (IntelliJ/Eclipse). - New Java classes should include a short Javadoc describing the class purpose. - Use standard Java naming: packages `lower.case`, classes `UpperCamelCase`, tests `*Test`. ## OpenAPI Contract Workflow (apollo-portal) - Treat OpenAPI as contract-first: update spec in `apolloconfig/apollo-openapi` before (or together with) portal implementation changes. - `apollo-portal/pom.xml` uses `apollo.openapi.spec.url` + `openapi-generator-maven-plugin`; generated sources under `target/generated-sources/openapi/src/main/java` are added to compile path via `build-helper-maven-plugin`. - For new/changed OpenAPI endpoints, prefer implementing generated `*ManagementApi` interfaces and generated models; avoid introducing hand-written DTO/controller contracts that bypass the spec pipeline. - In PR review, verify contract alignment explicitly: endpoint path, request/response model, and permissions in `apollo` should match the spec in `apollo-openapi`. ## Testing Guidelines - JUnit 5 is the default, with Vintage enabled for legacy JUnit 4 tests. - Put new tests under the module’s `src/test/java` with `*Test` suffix. - Add unit tests for new features or important bug fixes. ## Commit & Pull Request Guidelines - Use Conventional Commits format (e.g., `feat:`, `fix:`). - If a commit fixes an issue, append `Fixes #123` in the commit message. - Commit only on feature branches; never commit directly to `master` or `main`. - `CHANGES.md` entries must use a PR URL in Markdown link format; if the PR URL is not available yet, open the PR first, then add/update `CHANGES.md` in a follow-up commit. - Rebase onto `master` and squash feature work into a single commit before merge. - When merging a PR on GitHub: if it has a single commit, use rebase and merge; if it has multiple commits, use squash and merge. - Non-trivial contributions require signing the CLA. - Open a feature branch for your change and submit a PR using `.github/PULL_REQUEST_TEMPLATE.md`. - PRs should include a clear description, tests run, and any relevant screenshots/logs; use GitHub issues for tracking. - The PR checklist expects `mvn clean test`, `mvn spotless:apply`, and an update to `CHANGES.md`. - For upstream contributions, open PRs against `apolloconfig/apollo` and fill the template with real content (not the raw template text). ## Security & Configuration Notes - Apollo supports H2 in-memory for local development and MySQL for production; prefer H2 locally unless you need a real database. - Review `scripts/sql` and environment config when using MySQL. - Do not commit secrets or environment-specific credentials; use local overrides instead. - Follow `SECURITY.md` for vulnerability reporting. ================================================ FILE: CHANGES.md ================================================ Changes by Version ================== Release Notes. Apollo 3.0.0 ------------------ * [Fix: include super admin in hasAnyPermission semantics](https://github.com/apolloconfig/apollo/pull/5568) ------------------ All issues and pull requests are [here](https://github.com/apolloconfig/apollo/milestone/18?closed=1) ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at apollo-config@googlegroups.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] [homepage]: http://contributor-covenant.org [version]: http://contributor-covenant.org/version/1/4/ ================================================ FILE: CONTRIBUTING.md ================================================ ## Contributing to apollo Apollo is released under the non-restrictive Apache 2.0 license, and follows a very standard GitHub development process, using GitHub tracker for issues and merging pull requests into master. If you want to contribute even something trivial please do not hesitate, but follow the guidelines below. ### Sign the Contributor License Agreement Before we accept a non-trivial patch or pull request we will need you to sign the Contributor License Agreement. Signing the contributor’s agreement does not grant anyone commit rights to the main repository, but it does mean that we can accept your contributions, and you will get an author credit if we do. Active contributors might be asked to join the core team, and given the ability to merge pull requests. ### Code Conventions Our code style is in line with [Google Java Style Guide](https://google.github.io/styleguide/javaguide.html). We provide template files [intellij-java-google-style.xml](https://github.com/ctripcorp/apollo/blob/master/apollo-buildtools/style/intellij-java-google-style.xml) for IntelliJ IDEA and [eclipse-java-google-style.xml](https://github.com/ctripcorp/apollo/blob/master/apollo-buildtools/style/eclipse-java-google-style.xml) for Eclipse. If you use other IDEs, then you may config manually by referencing the template files. * Make sure all new .java files have a simple Javadoc class comment on what the class is for. * Add some Javadocs and, if you change the namespace, some XSD doc elements. * A few unit tests should be added for a new feature or an important bug fix. * If no-one else is using your branch, please rebase it against the current master (or other target branch in the main project). * Normally, we would squash commits for one feature into one commit. There are 2 ways to do this: 1. To rebase and squash based on the remote branch * `git rebase -i /master` * merge commits via `fixup`, etc 2. Create a new branch and merge these commits into one * `git checkout -b /master` * `git merge --squash ` * For commits, we adhere to the conventional commits format. For more details, refer to [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/). * When crafting commit messages, please adhere to the following conventions: if your commit addresses an existing issue, append "Fixes #XXX" to the end of the commit message (where XXX is the issue number). * Before submitting a pull request, you need to run `mvn spotless:apply` locally to format the code; this is important for maintaining good coding style in the project. ================================================ FILE: GOVERNANCE.md ================================================ # Overview Apollo is a meritocratic, consensus-based community project. Anyone with an interest in the project can join the community, contribute to the project design and participate in the decision-making process. This document describes how that participation takes place and how to set about earning merit within the project community. # Roles and Responsibilities Apollo community is composed of and operated by the following roles: - Users - Contributors - Committers - Project Management Committee (PMC) ## Users Users are community members who have a need for the project. They are the most important members of the community and without them the project would have no purpose. Anyone can be a user and there are no special requirements. ## Contributors Contributors are community members who contribute in concrete ways to the project. ### How to become a Contributor - merged at least 1 pull request You are also encouraged to participate in the projects in the following ways: - Actively answer technical questions raised by community users in GitHub issues. - Help test the projects - Help review the pull requests (PRs) submitted by others - Help improve technical documents - Submit valuable issues - Report or fix known and unknown bugs - Write articles about source code analysis and usage cases for a project. - Give representations of Apollo topic in conferences. - Take part in our discussions of features, enhancements, etc. ## Members Members are active contributors who have made multiple contributions to the project or community. They will be invited as a member of the [apollo organization](https://github.com/apolloconfig). ### How to become a Member - Enabled [two-factor authentication](https://help.github.com/articles/about-two-factor-authentication) on their GitHub account - Have made multiple contributions to the project or community, e.g. authoring or reviewing PRs, commenting on issues/discussions, etc - [Submit a member request](https://github.com/apolloconfig/apollo-community/issues/new?template=membership-request.md&title=REQUEST%3A+New+membership+for+%3Cyour-github-username%3E) in the [apollo-community](https://github.com/apolloconfig/apollo-community) repo - Sponsored by two PMC members, i.e. replying `+1` to confirm sponsorship in the member request ### Privileges and responsibilities - Have the [Triage role](https://docs.github.com/en/enterprise-cloud@latest/organizations/managing-peoples-access-to-your-organization-with-roles/managing-custom-repository-roles-for-an-organization#about-the-inherited-role) of the apollo community - Responsive to issues and PRs assigned to them - Active owner of code contributed by them, e.g. Addresses bugs or issues discovered after code is accepted ## Committers Committers are contributors who have shown that they are committed to the continued development of the project through ongoing engagement with the community and recognized by PMCs for their outstanding contributions. ### How to become a Committer A Committer must have accomplished one or more of the following items: - Demonstrated a good sense of responsibility in PR reviews. - Demonstrated deep understanding of Apollo components by contributing significantly as: - Finished 2 or more tasks of Medium difficulty - Fixed 1 or more tasks of Hard difficulty - Nominated by one PMC member and gained more +1 than -1. ### Privileges and responsibilities - Have the [Write role](https://docs.github.com/en/enterprise-cloud@latest/organizations/managing-peoples-access-to-your-organization-with-roles/managing-custom-repository-roles-for-an-organization#about-the-inherited-role) of the apollo community - Control overall code quality of projects - Guide Contributors to contribute to the community continuously - Participate in design discussions ## Project Management Committee The PMC(Project Management Committee) functions as the core management team that oversees the Apollo community. The PMC has additional responsibilities over and above those of Committers. These responsibilities ensure the smooth running of the project. ### How to become a PMC member - Membership of the PMC is by invitation from the existing PMC members. - A nomination will result in discussion and then a vote by the existing PMC members. - PMC membership votes are subject to consensus approval of the current PMC members. ### Privileges and responsibilities - Handle reported security issues (CVE, etc.) - Nominate new committers and PMC members - Vote on new committers and new PMC members - Make major decisions for the future with respect to Apollo, such as project-level governance policies, management of sub-structures, security processes and so on - Make decisions when community consensus cannot be reached # Decision-making and voting Proposals and ideas can be submitted for agreement via a GitHub issue, PR, or GitHub Discussion. Major changes such as feature proposals and organization or process changes should be brought to the PMC. For the change to happen, the change must earn more +1 than -1. # Conflict resolution In general, we prefer that technical issues and other disputes upon which consensus can't be reached are amicably worked out between the persons involved. If a dispute cannot be decided independently, the PMC can be called in to resolve the issue by voting. The same PR can be used, or a separate PR can be opened for voting. # Changes in Governance Any change in this Governance document, or similar nature of changes to other governance related documents, shall go through the voting process as described in [Decision-making and voting](#decision-making-and-voting). # Credits The contents of this document are based on [Meritocratic Governance Model](http://oss-watch.ac.uk/resources/meritocraticgovernancemodel), [TiDB Governance](https://github.com/pingcap/community/blob/master/GOVERNANCE.md) and [Dapr Community Membership](https://github.com/dapr/community/blob/master/community-membership.md). ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright {yyyy} {name of copyright owner} Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: README.md ================================================ apollo-logo English | [中文](https://www.apolloconfig.com/#/zh/README) # Apollo - A reliable configuration management system [![Build Status](https://github.com/apolloconfig/apollo/workflows/build/badge.svg)](https://github.com/apolloconfig/apollo/actions) [![GitHub Release](https://img.shields.io/github/release/apolloconfig/apollo.svg)](https://github.com/apolloconfig/apollo/releases) [![Maven Central Repo](https://img.shields.io/maven-central/v/com.ctrip.framework.apollo/apollo-client.svg)](https://mvnrepository.com/artifact/com.ctrip.framework.apollo/apollo-client) [![codecov.io](https://codecov.io/github/apolloconfig/apollo/coverage.svg?branch=master)](https://codecov.io/github/apolloconfig/apollo?branch=master) [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) [Ask DeepWiki.com](https://deepwiki.com/apolloconfig/apollo) Apollo is a reliable configuration management system. It can centrally manage the configurations of different applications and different clusters. It is suitable for microservice configuration management scenarios. The server side is developed based on Spring Boot and Spring Cloud, which can simply run without the need to install additional application containers such as Tomcat. The Java SDK does not rely on any framework and can run in all Java runtime environments. It also has good support for Spring/Spring Boot environments. The .Net SDK does not rely on any framework and can run in all .Net runtime environments. For more details of the product introduction, please refer [Introduction to Apollo Configuration Center](https://www.apolloconfig.com/#/zh/design/apollo-introduction). For local demo purpose, please refer [Quick Start](https://www.apolloconfig.com/#/zh/deployment/quick-start). Demo Environment: - [http://81.68.181.139](http://81.68.181.139/) - User/Password: apollo/admin # Screenshots ![Screenshot](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/docs/en/images/apollo-home-screenshot.jpg) # Features * **Unified management of the configurations of different environments and different clusters** * Apollo provides a unified interface to centrally manage the configurations of different environments, different clusters, and different namespaces * The same codebase could have different configurations when deployed in different clusters * With the namespace concept, it is easy to support multiple applications to share the same configurations, while also allowing them to customize the configurations * Multiple languages is provided in user interface(currently Chinese and English) * **Configuration changes takes effect in real time (hot release)** * After the user modified the configuration and released it in Apollo, the sdk will receive the latest configurations in real time (1 second) and notify the application * **Release version management** * Every configuration releases are versioned, which is friendly to support configuration rollback * **Grayscale release** * Support grayscale configuration release, for example, after clicking release, it will only take effect for some application instances. After a period of observation, we could push the configurations to all application instances if there is no problem * **Global Search Configuration Items** * A fuzzy search of the key and value of a configuration item finds in which application, environment, cluster, namespace the configuration item with the corresponding value is used * It is easy for administrators and SRE roles to quickly and easily find and change the configuration values of resources by highlighting, paging and jumping through configurations * **Authorization management, release approval and operation audit** * Great authorization mechanism is designed for applications and configurations management, and the management of configurations is divided into two operations: editing and publishing, therefore greatly reducing human errors * All operations have audit logs for easy tracking of problems * **Client side configuration information monitoring** * It's very easy to see which instances are using the configurations and what versions they are using * **Rich SDKs available** * Provides native sdks of Java and .Net to facilitate application integration * Support Spring Placeholder, Annotation and Spring Boot ConfigurationProperties for easy application use (requires Spring 3.1.1+) * Http APIs are provided, so non-Java and .Net applications can integrate conveniently * Rich third party sdks are also available, e.g. Golang, Python, NodeJS, PHP, C, etc * **Open platform API** * Apollo itself provides a unified configuration management interface, which supports features such as multi-environment, multi-data center configuration management, permissions, and process governance * However, for the sake of versatility, Apollo will not put too many restrictions on the modification of the configuration, as long as it conforms to the basic format, it can be saved. * In our research, we found that for some users, their configurations may have more complicated formats, such as xml, json, and the format needs to be verified * There are also some users such as DAL, which not only have a specific format, but also need to verify the entered value before saving, such as checking whether the database, username and password match * For this type of application, Apollo allows the application to modify and release configurations through open APIs, which has great authorization and permission control mechanism built in * **Simple deployment** * As an infrastructure service, the configuration center has very high availability requirements, which forces Apollo to rely on external dependencies as little as possible * Currently, the only external dependency is MySQL, so the deployment is very simple. Apollo can run as long as Java and MySQL are installed * Apollo also provides a packaging script, which can generate all required installation packages with just one click, and supports customization of runtime parameters # Usage * [Apollo User Guide](https://www.apolloconfig.com/#/zh/portal/apollo-user-guide) * [Apollo Open APIs](https://www.apolloconfig.com/#/zh/portal/apollo-open-api-platform) * [Apollo Use Cases](https://github.com/apolloconfig/apollo-use-cases) * [Apollo User Practices](https://www.apolloconfig.com/#/zh/portal/apollo-user-practices) * [Apollo Security Best Practices](https://www.apolloconfig.com/#/zh/portal/apollo-user-guide?id=_71-%e5%ae%89%e5%85%a8%e7%9b%b8%e5%85%b3) # SDK * [Java SDK User Guide](https://www.apolloconfig.com/#/zh/client/java-sdk-user-guide) * [.Net SDK user Guide](https://www.apolloconfig.com/#/zh/client/dotnet-sdk-user-guide) * [Golang SDK User Guide](https://www.apolloconfig.com/#/zh/client/golang-sdks-user-guide) * [Python SDK User Guide](https://www.apolloconfig.com/#/zh/client/python-sdks-user-guide) * [NodeJS SDK User Guide](https://www.apolloconfig.com/#/zh/client/nodejs-sdks-user-guide) * [PHP SDK User Guide](https://www.apolloconfig.com/#/zh/client/php-sdks-user-guide) * [C SDK User Guide](https://www.apolloconfig.com/#/zh/client/c-sdks-user-guide) * [Rust SDK User Guide](https://www.apolloconfig.com/#/zh/client/rust-sdks-user-guide) * [HTTP API Guide](https://www.apolloconfig.com/#/zh/client/other-language-client-user-guide) # Design * [Apollo Design](https://www.apolloconfig.com/#/zh/design/apollo-design) * [Apollo Core Concept - Namespace](https://www.apolloconfig.com/#/zh/design/apollo-core-concept-namespace) * [Apollo Architecture Analysis](https://mp.weixin.qq.com/s/-hUaQPzfsl9Lm3IqQW3VDQ) * [Apollo Source Code Explanation](http://www.iocoder.cn/categories/Apollo/) # Development * [Apollo Development Guide](https://www.apolloconfig.com/#/zh/contribution/apollo-development-guide) * Code Styles * [Eclipse Code Style](https://github.com/apolloconfig/apollo/blob/master/apollo-buildtools/style/eclipse-java-google-style.xml) * [Intellij Code Style](https://github.com/apolloconfig/apollo/blob/master/apollo-buildtools/style/intellij-java-google-style.xml) # Deployment * [Quick Start](https://www.apolloconfig.com/#/zh/deployment/quick-start) * [Distributed Deployment Guide](https://www.apolloconfig.com/#/zh/deployment/distributed-deployment-guide) # Release Notes * [Releases](https://github.com/apolloconfig/apollo/releases) # FAQ * [FAQ](https://www.apolloconfig.com/#/zh/faq/faq) * [Common Issues in Deployment & Development Phase](https://www.apolloconfig.com/#/zh/faq/common-issues-in-deployment-and-development-phase) # Presentation * [Design and Implementation Details of Apollo](http://www.itdks.com/dakalive/detail/3420) * [Slides](https://github.com/apolloconfig/apollo-community/blob/master/slides/design-and-implementation-of-apollo.pdf) * [Configuration Center Makes Microservices Smart](https://2018.qconshanghai.com/presentation/799) * [Slides](https://github.com/apolloconfig/apollo-community/blob/master/slides/configuration-center-makes-microservices-smart.pdf) # Publication * [Design and Implementation Details of Apollo](https://www.infoq.cn/article/open-source-configuration-center-apollo) * [Configuration Center Makes Microservices Smart](https://mp.weixin.qq.com/s/iDmYJre_ULEIxuliu1EbIQ) # Community * [Apollo Team](https://www.apolloconfig.com/#/en/community/team) * [Community Governance](https://github.com/apolloconfig/apollo/blob/master/GOVERNANCE.md) * [Contributing Guide](https://github.com/apolloconfig/apollo/blob/master/CONTRIBUTING.md) # License The project is licensed under the [Apache 2 license](https://github.com/apolloconfig/apollo/blob/master/LICENSE). # Known Users > Sorted by registration order,users are welcome to register in [https://github.com/apolloconfig/apollo/issues/451](https://github.com/apolloconfig/apollo/issues/451) (reference purpose only for the community)
携程 青石证券 沙绿 航旅纵横 58转转
蜂助手 海南航空 CVTE 明博教育 麻袋理财
美行科技 首展科技 易微行 人才加 凯京集团
乐刻运动 大疆 快看漫画 我来贷 虚实软件
网易严选 视觉中国 资产360 亿咖通 5173
沪江 网易云基础服务 现金巴士 锤子科技 头等仓
吉祥航空 263移动通信 投投金融 每天健康 麦芽金服
蜂向科技 即科金融 贝壳网 有赞 云集汇通
犀牛瀚海科技 农信互联 蘑菇租房 狐狸金服 漫道集团
怪兽充电 南瓜租房 石投金融 土巴兔 平安银行
新新贷 中国华戎科技集团 涂鸦智能 立创商城 乐赚金服
开心汽车 乐赚金服 普元信息 医帮管家 付啦信用卡管家
悠哉网 梧桐诚选 拍拍贷 信用飞 丁香园
国槐科技 亲宝宝 华为视频直播 微播易 欧飞
迷说 一下科技 DaoCloud 汽摩交易所 好未来教育集团
猎户星空 卓健科技 银江股份 途虎养车 河姆渡
新网银行 中旅安信云贷 美柚 震坤行 万谷盛世
铂涛旅行 乐心 亿投传媒 股先生 财学堂
4399 汽车之家 面包财经 虎扑 搜狐汽车
量富征信 卖好车 中移物联网 易车网 一药网
小影 彩贝壳 YEELIGHT 积目 极致医疗
金汇金融 久柏易游 小麦铺 搜款网 米庄理财
贝吉塔网络科技 微盟 网易卡搭 股书 聚贸
广联达bimface 环球易购 浙江执御 二维火 上品
浪潮集团 纳里健康 橙红科技 龙腾出行 荔枝
汇通达 云融金科 天生掌柜 容联光辉 云天励飞
嘉云数据 中泰证券网络金融部 网易易盾 享物说 申通
金和网络 二三四五 恒天财富 沐雪微信 温州医科大学附属眼视光医院
联通支付 杉数科技 分利宝 核桃编程 小红书
幸福西饼 跨越速运 OYO 叮咚买菜 智道网联
雪球 车通云 哒哒英语 小E微店 达令家
人力窝 嘉美在线 极易付 智慧开源 车仕库
太美医疗科技 亿联百汇 舟谱数据 芙蓉兴盛 野兽派
凯叔讲故事 好大夫在线 云幂信息技术 兑吧 九机网
随手科技 万谷盛世 云账房 浙江远图互联 青客公寓
东方财富 极客修 美市科技 中通快递 易流科技
实习僧 达令家 寺库 连连支付 众安保险
360金融 中航服商旅 贝壳 Yeahmobi易点天下 北京登云美业网络科技有限公司
金和网络 中移(杭州)信息技术有限公司 北森 合肥维天运通 北京蜜步科技有限公司
术康 富力集团 天府行 八商山 中原地产
智科云达 中原730 百果园 波罗蜜 Xignite
杭州有云科技有限公司 成都书声科技有限公司 斯维登集团 广东快乐种子科技有限公司 上海盈翼文化传播有限公司
上海尚诚消费金融股份有限公司 自如网 京东 兔展智能 竹贝
iMile(中东) 哈罗出行 智联招聘 阿卡索 妙知旅
程多多 上汽通用五菱 乐言科技 樊登读书 找一找教程网
中油碧辟石油有限公司 四川商旅无忧科技服务有限公司 懿鸢网络科技(上海)有限公司 稿定科技 搵樓 - 利嘉閣
南京领行科技股份有限公司 北京希瑞亚斯科技有限公司 印彩虹印刷公司 Million Tech 果果科技
昆明航空 我爱我家 国金证券 不亦乐乎 惠农网
成都道壳 澳优乳业 河南有态度信息科技有限公司 智阳第一人力 上海保险交易所
万顺叫车 收钱吧 宝尊电商 喜百年供应链 南京观为智慧软件科技有限公司
在途商旅 哗啦啦 优信二手车 每刻科技 杭州蛮牛
翼支付 魔筷科技 畅唐网络 准时达 早道网校
万店掌 推文科技 Lemonbox 保利票务 芯翼科技
浙商银行 易企银科技 上海云盾 苏州盖雅信息技术有限公司 爱库存
极豆车联网 伴鱼少儿英语 锐达科技 新东方在线 金康高科
soul 驿氪 慧聪 中塑在线 甄云科技
追溯科技 玩吧 广州卡桑信息技术有限公司 水滴 酷我音乐
小米 今典 签宝科技 广州趣米网络科技有限公司 More...
# Awards The most popular Chinese open source software in 2018 # Stargazers over time [![Stargazers over time](https://api.star-history.com/svg?repos=apolloconfig/apollo&type=Date)](https://star-history.com/#apolloconfig/apollo&Date) ================================================ FILE: SECURITY.md ================================================ # Security Policy ## Reporting a Vulnerability If you have apprehensions regarding Apollo's security or you discover vulnerability or potential threat, don’t hesitate to get in touch with us by dropping a mail at apollo-config@googlegroups.com. In the mail, specify the description of the issue or potential threat. You are also urged to recommend the way to reproduce and replicate the issue. The Apollo community will get back to you after assessing and analysing the findings. PLEASE PAY ATTENTION to report the security issue on the security email before disclosing it on public domain. ================================================ FILE: apollo-adminservice/pom.xml ================================================ com.ctrip.framework.apollo apollo ${revision} ../pom.xml 4.0.0 apollo-adminservice Apollo AdminService ${project.artifactId} com.ctrip.framework.apollo apollo-biz com.ctrip.framework.apollo apollo-audit-spring-boot-starter org.springframework.cloud spring-cloud-starter-netflix-eureka-server test spring-cloud-starter-netflix-archaius org.springframework.cloud spring-cloud-starter-netflix-ribbon org.springframework.cloud ribbon-eureka com.netflix.ribbon aws-java-sdk-core com.amazonaws aws-java-sdk-ec2 com.amazonaws aws-java-sdk-autoscaling com.amazonaws aws-java-sdk-sts com.amazonaws aws-java-sdk-route53 com.amazonaws com.sun.jersey.contribs jersey-apache-client4 test jakarta.xml.bind jakarta.xml.bind-api org.glassfish.jaxb jaxb-runtime jakarta.activation jakarta.activation-api org.javassist javassist org.springframework.boot spring-boot-maven-plugin maven-assembly-plugin package single ${project.artifactId}-${project.version}-${package.environment} false src/assembly/assembly-descriptor.xml com.spotify docker-maven-plugin 1.2.2 apolloconfig/${project.artifactId} ${project.version} latest ${project.basedir}/src/main/docker docker-hub ${project.version} / ${project.build.directory} *.zip nacos-discovery com.alibaba.boot nacos-discovery-spring-boot-starter com.alibaba fastjson ================================================ FILE: apollo-adminservice/src/assembly/assembly-descriptor.xml ================================================ apollo-assembly zip false src/main/scripts scripts *.sh 0755 unix target/classes / apollo-adminservice.conf unix target/classes /config application-github.properties application.properties target / ${project.artifactId}-*.jar 0755 ================================================ FILE: apollo-adminservice/src/main/docker/Dockerfile ================================================ # # Copyright 2024 Apollo Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # Dockerfile for apollo-adminservice # 1. ./scripts/build.sh # 2. Build with: mvn docker:build -pl apollo-adminservice # 3. Run with: docker run -p 8090:8090 -e SPRING_DATASOURCE_URL="jdbc:mysql://fill-in-the-correct-server:3306/ApolloConfigDB?characterEncoding=utf8" -e SPRING_DATASOURCE_USERNAME=FillInCorrectUser -e SPRING_DATASOURCE_PASSWORD=FillInCorrectPassword -d -v /tmp/logs:/opt/logs --name apollo-adminservice apolloconfig/apollo-adminservice FROM alpine:3.15.5 ARG VERSION ENV VERSION $VERSION COPY apollo-adminservice-${VERSION}-github.zip /apollo-adminservice/apollo-adminservice-${VERSION}-github.zip RUN unzip /apollo-adminservice/apollo-adminservice-${VERSION}-github.zip -d /apollo-adminservice \ && rm -rf /apollo-adminservice/apollo-adminservice-${VERSION}-github.zip \ && chmod +x /apollo-adminservice/scripts/startup.sh FROM eclipse-temurin:17-jre-jammy LABEL maintainer="g632104866@gmail.com;finchcn@gmail.com;ameizi" ENV APOLLO_RUN_MODE "Docker" ENV SERVER_PORT 8090 RUN DEBIAN_FRONTEND=noninteractive apt-get update \ && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends procps curl bash tzdata \ && rm -rf /var/lib/apt/lists/* \ && ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ && echo "Asia/Shanghai" > /etc/timezone COPY --from=0 /apollo-adminservice /apollo-adminservice EXPOSE $SERVER_PORT CMD ["/apollo-adminservice/scripts/startup.sh"] ================================================ FILE: apollo-adminservice/src/main/java/com/ctrip/framework/apollo/adminservice/AdminServiceApplication.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.adminservice; import com.ctrip.framework.apollo.biz.ApolloBizConfig; import com.ctrip.framework.apollo.common.ApolloCommonConfig; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration; import org.springframework.boot.autoconfigure.session.SessionAutoConfiguration; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.EnableAspectJAutoProxy; import org.springframework.context.annotation.PropertySource; import org.springframework.transaction.annotation.EnableTransactionManagement; @EnableAspectJAutoProxy @Configuration @PropertySource(value = {"classpath:adminservice.properties"}) @EnableAutoConfiguration( exclude = {UserDetailsServiceAutoConfiguration.class, SessionAutoConfiguration.class}) @EnableTransactionManagement @ComponentScan(basePackageClasses = {ApolloCommonConfig.class, ApolloBizConfig.class, AdminServiceApplication.class}) public class AdminServiceApplication { public static void main(String[] args) { SpringApplication.run(AdminServiceApplication.class, args); } } ================================================ FILE: apollo-adminservice/src/main/java/com/ctrip/framework/apollo/adminservice/AdminServiceAssemblyConfiguration.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.adminservice; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; import org.springframework.core.annotation.Order; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.web.SecurityFilterChain; @Profile("assembly") @Configuration public class AdminServiceAssemblyConfiguration { @Bean @Order(101) public SecurityFilterChain adminServiceAssemblySecurityFilterChain(HttpSecurity http) throws Exception { http.csrf(csrf -> csrf.disable()); http.httpBasic(Customizer.withDefaults()); return http.build(); } } ================================================ FILE: apollo-adminservice/src/main/java/com/ctrip/framework/apollo/adminservice/AdminServiceAutoConfiguration.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.adminservice; import com.ctrip.framework.apollo.adminservice.filter.AdminServiceAuthenticationFilter; import com.ctrip.framework.apollo.biz.config.BizConfig; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class AdminServiceAutoConfiguration { private final BizConfig bizConfig; public AdminServiceAutoConfiguration(final BizConfig bizConfig) { this.bizConfig = bizConfig; } @Bean public FilterRegistrationBean adminServiceAuthenticationFilter() { FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean<>(); filterRegistrationBean.setFilter(new AdminServiceAuthenticationFilter(bizConfig)); filterRegistrationBean.addUrlPatterns("/apollo/audit/*"); filterRegistrationBean.addUrlPatterns("/apps/*"); filterRegistrationBean.addUrlPatterns("/appnamespaces/*"); filterRegistrationBean.addUrlPatterns("/instances/*"); filterRegistrationBean.addUrlPatterns("/items/*"); filterRegistrationBean.addUrlPatterns("/items-search/*"); filterRegistrationBean.addUrlPatterns("/namespaces/*"); filterRegistrationBean.addUrlPatterns("/releases/*"); filterRegistrationBean.addUrlPatterns("/server/*"); return filterRegistrationBean; } } ================================================ FILE: apollo-adminservice/src/main/java/com/ctrip/framework/apollo/adminservice/AdminServiceHealthIndicator.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.adminservice; import com.ctrip.framework.apollo.biz.service.AppService; import org.springframework.boot.actuate.health.Health; import org.springframework.boot.actuate.health.HealthIndicator; import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Component; @Component public class AdminServiceHealthIndicator implements HealthIndicator { private final AppService appService; public AdminServiceHealthIndicator(final AppService appService) { this.appService = appService; } @Override public Health health() { check(); return Health.up().build(); } private void check() { PageRequest pageable = PageRequest.of(0, 1); appService.findAll(pageable); } } ================================================ FILE: apollo-adminservice/src/main/java/com/ctrip/framework/apollo/adminservice/ServletInitializer.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.adminservice; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; /** * Entry point for traditional web app * * @author Jason Song(song_s@ctrip.com) */ public class ServletInitializer extends SpringBootServletInitializer { @Override protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { return application.sources(AdminServiceApplication.class); } } ================================================ FILE: apollo-adminservice/src/main/java/com/ctrip/framework/apollo/adminservice/aop/NamespaceAcquireLockAspect.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.adminservice.aop; import com.ctrip.framework.apollo.biz.config.BizConfig; import com.ctrip.framework.apollo.biz.entity.Item; import com.ctrip.framework.apollo.biz.entity.Namespace; import com.ctrip.framework.apollo.biz.entity.NamespaceLock; import com.ctrip.framework.apollo.biz.service.ItemService; import com.ctrip.framework.apollo.biz.service.NamespaceLockService; import com.ctrip.framework.apollo.biz.service.NamespaceService; import com.ctrip.framework.apollo.common.dto.ItemChangeSets; import com.ctrip.framework.apollo.common.dto.ItemDTO; import com.ctrip.framework.apollo.common.exception.BadRequestException; import com.ctrip.framework.apollo.common.exception.ServiceException; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.stereotype.Component; /** * 一个namespace在一次发布中只能允许一个人修改配置 * 通过数据库lock表来实现 */ @Aspect @Component public class NamespaceAcquireLockAspect { private static final Logger logger = LoggerFactory.getLogger(NamespaceAcquireLockAspect.class); private final NamespaceLockService namespaceLockService; private final NamespaceService namespaceService; private final ItemService itemService; private final BizConfig bizConfig; public NamespaceAcquireLockAspect(final NamespaceLockService namespaceLockService, final NamespaceService namespaceService, final ItemService itemService, final BizConfig bizConfig) { this.namespaceLockService = namespaceLockService; this.namespaceService = namespaceService; this.itemService = itemService; this.bizConfig = bizConfig; } // create item @Before( value = "@annotation(PreAcquireNamespaceLock) && args(appId, clusterName, namespaceName, item, ..)", argNames = "appId,clusterName,namespaceName,item") public void requireLockAdvice(String appId, String clusterName, String namespaceName, ItemDTO item) { acquireLock(appId, clusterName, namespaceName, item.getDataChangeLastModifiedBy()); } // update item @Before( value = "@annotation(PreAcquireNamespaceLock) && args(appId, clusterName, namespaceName, itemId, item, ..)", argNames = "appId,clusterName,namespaceName,itemId,item") public void requireLockAdvice(String appId, String clusterName, String namespaceName, long itemId, ItemDTO item) { acquireLock(appId, clusterName, namespaceName, item.getDataChangeLastModifiedBy()); } // update by change set @Before( value = "@annotation(PreAcquireNamespaceLock) && args(appId, clusterName, namespaceName, changeSet, ..)", argNames = "appId,clusterName,namespaceName,changeSet") public void requireLockAdvice(String appId, String clusterName, String namespaceName, ItemChangeSets changeSet) { acquireLock(appId, clusterName, namespaceName, changeSet.getDataChangeLastModifiedBy()); } // delete item @Before(value = "@annotation(PreAcquireNamespaceLock) && args(itemId, operator, ..)", argNames = "itemId,operator") public void requireLockAdvice(long itemId, String operator) { Item item = itemService.findOne(itemId); if (item == null) { throw BadRequestException.itemNotExists(itemId); } acquireLock(item.getNamespaceId(), operator); } void acquireLock(String appId, String clusterName, String namespaceName, String currentUser) { if (bizConfig.isNamespaceLockSwitchOff()) { return; } Namespace namespace = namespaceService.findOne(appId, clusterName, namespaceName); acquireLock(namespace, currentUser); } void acquireLock(long namespaceId, String currentUser) { if (bizConfig.isNamespaceLockSwitchOff()) { return; } Namespace namespace = namespaceService.findOne(namespaceId); acquireLock(namespace, currentUser); } private void acquireLock(Namespace namespace, String currentUser) { if (namespace == null) { throw BadRequestException.namespaceNotExists(); } long namespaceId = namespace.getId(); NamespaceLock namespaceLock = namespaceLockService.findLock(namespaceId); if (namespaceLock == null) { try { tryLock(namespaceId, currentUser); // lock success } catch (DataIntegrityViolationException e) { // lock fail namespaceLock = namespaceLockService.findLock(namespaceId); checkLock(namespace, namespaceLock, currentUser); } catch (Exception e) { logger.error("try lock error", e); throw e; } } else { // check lock owner is current user checkLock(namespace, namespaceLock, currentUser); } } private void tryLock(long namespaceId, String user) { NamespaceLock lock = new NamespaceLock(); lock.setNamespaceId(namespaceId); lock.setDataChangeCreatedBy(user); lock.setDataChangeLastModifiedBy(user); namespaceLockService.tryLock(lock); } private void checkLock(Namespace namespace, NamespaceLock namespaceLock, String currentUser) { if (namespaceLock == null) { throw new ServiceException( String.format("Check lock for %s failed, please retry.", namespace.getNamespaceName())); } String lockOwner = namespaceLock.getDataChangeCreatedBy(); if (!lockOwner.equals(currentUser)) { throw new BadRequestException( "namespace:" + namespace.getNamespaceName() + " is modified by " + lockOwner); } } } ================================================ FILE: apollo-adminservice/src/main/java/com/ctrip/framework/apollo/adminservice/aop/NamespaceUnlockAspect.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.adminservice.aop; import com.ctrip.framework.apollo.biz.config.BizConfig; import com.ctrip.framework.apollo.biz.entity.Item; import com.ctrip.framework.apollo.biz.entity.Namespace; import com.ctrip.framework.apollo.biz.entity.Release; import com.ctrip.framework.apollo.biz.service.ItemService; import com.ctrip.framework.apollo.biz.service.NamespaceLockService; import com.ctrip.framework.apollo.biz.service.NamespaceService; import com.ctrip.framework.apollo.biz.service.ReleaseService; import com.ctrip.framework.apollo.common.constants.GsonType; import com.ctrip.framework.apollo.common.dto.ItemChangeSets; import com.ctrip.framework.apollo.common.dto.ItemDTO; import com.ctrip.framework.apollo.common.exception.BadRequestException; import com.ctrip.framework.apollo.core.utils.StringUtils; import com.google.common.collect.MapDifference; import com.google.common.collect.Maps; import com.google.gson.Gson; import org.aspectj.lang.annotation.After; import org.aspectj.lang.annotation.Aspect; import org.springframework.stereotype.Component; import java.util.List; import java.util.Map; /** * unlock namespace if is redo operation. * -------------------------------------------- * For example: If namespace has a item K1 = v1 * -------------------------------------------- * First operate: change k1 = v2 (lock namespace) * Second operate: change k1 = v1 (unlock namespace) */ @Aspect @Component public class NamespaceUnlockAspect { private static final Gson GSON = new Gson(); private final NamespaceLockService namespaceLockService; private final NamespaceService namespaceService; private final ItemService itemService; private final ReleaseService releaseService; private final BizConfig bizConfig; public NamespaceUnlockAspect(final NamespaceLockService namespaceLockService, final NamespaceService namespaceService, final ItemService itemService, final ReleaseService releaseService, final BizConfig bizConfig) { this.namespaceLockService = namespaceLockService; this.namespaceService = namespaceService; this.itemService = itemService; this.releaseService = releaseService; this.bizConfig = bizConfig; } // create item @After( value = "@annotation(PreAcquireNamespaceLock) && args(appId, clusterName, namespaceName, item, ..)", argNames = "appId,clusterName,namespaceName,item") public void requireLockAdvice(String appId, String clusterName, String namespaceName, ItemDTO item) { tryUnlock(namespaceService.findOne(appId, clusterName, namespaceName)); } // update item @After( value = "@annotation(PreAcquireNamespaceLock) && args(appId, clusterName, namespaceName, itemId, item, ..)", argNames = "appId,clusterName,namespaceName,itemId,item") public void requireLockAdvice(String appId, String clusterName, String namespaceName, long itemId, ItemDTO item) { tryUnlock(namespaceService.findOne(appId, clusterName, namespaceName)); } // update by change set @After( value = "@annotation(PreAcquireNamespaceLock) && args(appId, clusterName, namespaceName, changeSet, ..)", argNames = "appId,clusterName,namespaceName,changeSet") public void requireLockAdvice(String appId, String clusterName, String namespaceName, ItemChangeSets changeSet) { tryUnlock(namespaceService.findOne(appId, clusterName, namespaceName)); } // delete item @After(value = "@annotation(PreAcquireNamespaceLock) && args(itemId, operator, ..)", argNames = "itemId,operator") public void requireLockAdvice(long itemId, String operator) { Item item = itemService.findOne(itemId); if (item == null) { throw BadRequestException.itemNotExists(itemId); } tryUnlock(namespaceService.findOne(item.getNamespaceId())); } private void tryUnlock(Namespace namespace) { if (bizConfig.isNamespaceLockSwitchOff()) { return; } if (!isModified(namespace)) { namespaceLockService.unlock(namespace.getId()); } } boolean isModified(Namespace namespace) { Release release = releaseService.findLatestActiveRelease(namespace); List items = itemService.findItemsWithoutOrdered(namespace.getId()); if (release == null) { return hasNormalItems(items); } Map releasedConfiguration = GSON.fromJson(release.getConfigurations(), GsonType.CONFIG); Map configurationFromItems = generateConfigurationFromItems(namespace, items); MapDifference difference = Maps.difference(releasedConfiguration, configurationFromItems); return !difference.areEqual(); } private boolean hasNormalItems(List items) { for (Item item : items) { if (!StringUtils.isEmpty(item.getKey())) { return true; } } return false; } private Map generateConfigurationFromItems(Namespace namespace, List namespaceItems) { Map configurationFromItems = Maps.newHashMap(); Namespace parentNamespace = namespaceService.findParentNamespace(namespace); // parent namespace if (parentNamespace == null) { generateMapFromItems(namespaceItems, configurationFromItems); } else {// child namespace Release parentRelease = releaseService.findLatestActiveRelease(parentNamespace); if (parentRelease != null) { configurationFromItems = GSON.fromJson(parentRelease.getConfigurations(), GsonType.CONFIG); } generateMapFromItems(namespaceItems, configurationFromItems); } return configurationFromItems; } private Map generateMapFromItems(List items, Map configurationFromItems) { for (Item item : items) { String key = item.getKey(); if (StringUtils.isBlank(key)) { continue; } configurationFromItems.put(key, item.getValue()); } return configurationFromItems; } } ================================================ FILE: apollo-adminservice/src/main/java/com/ctrip/framework/apollo/adminservice/aop/PreAcquireNamespaceLock.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.adminservice.aop; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * 标识方法需要获取到namespace的lock才能执行 */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface PreAcquireNamespaceLock { } ================================================ FILE: apollo-adminservice/src/main/java/com/ctrip/framework/apollo/adminservice/controller/AccessKeyController.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.adminservice.controller; import static com.ctrip.framework.apollo.common.constants.AccessKeyMode.FILTER; import com.ctrip.framework.apollo.biz.entity.AccessKey; import com.ctrip.framework.apollo.biz.service.AccessKeyService; import com.ctrip.framework.apollo.common.dto.AccessKeyDTO; import com.ctrip.framework.apollo.common.utils.BeanUtils; import java.util.List; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; /** * @author nisiyong */ @RestController public class AccessKeyController { private final AccessKeyService accessKeyService; public AccessKeyController(AccessKeyService accessKeyService) { this.accessKeyService = accessKeyService; } @PostMapping(value = "/apps/{appId}/accesskeys") public AccessKeyDTO create(@PathVariable String appId, @RequestBody AccessKeyDTO dto) { AccessKey entity = BeanUtils.transform(AccessKey.class, dto); entity = accessKeyService.create(appId, entity); return BeanUtils.transform(AccessKeyDTO.class, entity); } @GetMapping(value = "/apps/{appId}/accesskeys") public List findByAppId(@PathVariable String appId) { List accessKeyList = accessKeyService.findByAppId(appId); return BeanUtils.batchTransform(AccessKeyDTO.class, accessKeyList); } @DeleteMapping(value = "/apps/{appId}/accesskeys/{id}") public void delete(@PathVariable String appId, @PathVariable long id, String operator) { accessKeyService.delete(appId, id, operator); } @PutMapping(value = "/apps/{appId}/accesskeys/{id}/enable") public void enable(@PathVariable String appId, @PathVariable long id, @RequestParam(required = false, defaultValue = "" + FILTER) int mode, String operator) { AccessKey entity = new AccessKey(); entity.setId(id); entity.setMode(mode); entity.setEnabled(true); entity.setDataChangeLastModifiedBy(operator); accessKeyService.update(appId, entity); } @PutMapping(value = "/apps/{appId}/accesskeys/{id}/disable") public void disable(@PathVariable String appId, @PathVariable long id, String operator) { AccessKey entity = new AccessKey(); entity.setId(id); entity.setMode(FILTER); entity.setEnabled(false); entity.setDataChangeLastModifiedBy(operator); accessKeyService.update(appId, entity); } } ================================================ FILE: apollo-adminservice/src/main/java/com/ctrip/framework/apollo/adminservice/controller/AppController.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.adminservice.controller; import com.ctrip.framework.apollo.biz.service.AdminService; import com.ctrip.framework.apollo.biz.service.AppService; import com.ctrip.framework.apollo.common.dto.AppDTO; import com.ctrip.framework.apollo.common.entity.App; import com.ctrip.framework.apollo.common.exception.BadRequestException; import com.ctrip.framework.apollo.common.exception.NotFoundException; import com.ctrip.framework.apollo.common.utils.BeanUtils; import com.ctrip.framework.apollo.core.utils.StringUtils; import org.springframework.data.domain.Pageable; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import jakarta.validation.Valid; import java.util.List; import java.util.Objects; @RestController public class AppController { private final AppService appService; private final AdminService adminService; public AppController(final AppService appService, final AdminService adminService) { this.appService = appService; this.adminService = adminService; } @PostMapping("/apps") public AppDTO create(@Valid @RequestBody AppDTO dto) { App entity = BeanUtils.transform(App.class, dto); App managedEntity = appService.findOne(entity.getAppId()); if (managedEntity != null) { throw BadRequestException.appAlreadyExists(entity.getAppId()); } entity = adminService.createNewApp(entity); return BeanUtils.transform(AppDTO.class, entity); } @DeleteMapping("/apps/{appId:.+}") public void delete(@PathVariable("appId") String appId, @RequestParam String operator) { App entity = appService.findOne(appId); if (entity == null) { throw NotFoundException.appNotFound(appId); } adminService.deleteApp(entity, operator); } @PutMapping("/apps/{appId:.+}") public void update(@PathVariable String appId, @RequestBody App app) { if (!Objects.equals(appId, app.getAppId())) { throw new BadRequestException("The App Id of path variable and request body is different"); } appService.update(app); } @GetMapping("/apps") public List find(@RequestParam(value = "name", required = false) String name, Pageable pageable) { List app; if (StringUtils.isBlank(name)) { app = appService.findAll(pageable); } else { app = appService.findByName(name); } return BeanUtils.batchTransform(AppDTO.class, app); } @GetMapping("/apps/{appId:.+}") public AppDTO get(@PathVariable("appId") String appId) { App app = appService.findOne(appId); if (app == null) { throw NotFoundException.appNotFound(appId); } return BeanUtils.transform(AppDTO.class, app); } @GetMapping("/apps/{appId}/unique") public boolean isAppIdUnique(@PathVariable("appId") String appId) { return appService.isAppIdUnique(appId); } } ================================================ FILE: apollo-adminservice/src/main/java/com/ctrip/framework/apollo/adminservice/controller/AppNamespaceController.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.adminservice.controller; import com.ctrip.framework.apollo.biz.entity.Namespace; import com.ctrip.framework.apollo.biz.service.AppNamespaceService; import com.ctrip.framework.apollo.biz.service.NamespaceService; import com.ctrip.framework.apollo.common.dto.AppNamespaceDTO; import com.ctrip.framework.apollo.common.dto.NamespaceDTO; import com.ctrip.framework.apollo.common.entity.AppNamespace; import com.ctrip.framework.apollo.common.exception.BadRequestException; import com.ctrip.framework.apollo.common.utils.BeanUtils; import com.ctrip.framework.apollo.core.enums.ConfigFileFormat; import com.ctrip.framework.apollo.core.utils.StringUtils; import org.springframework.data.domain.Pageable; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.util.List; @RestController public class AppNamespaceController { private final AppNamespaceService appNamespaceService; private final NamespaceService namespaceService; public AppNamespaceController(final AppNamespaceService appNamespaceService, final NamespaceService namespaceService) { this.appNamespaceService = appNamespaceService; this.namespaceService = namespaceService; } @PostMapping("/apps/{appId}/appnamespaces") public AppNamespaceDTO create(@RequestBody AppNamespaceDTO appNamespace, @RequestParam(defaultValue = "false") boolean silentCreation) { AppNamespace entity = BeanUtils.transform(AppNamespace.class, appNamespace); AppNamespace managedEntity = appNamespaceService.findOne(entity.getAppId(), entity.getName()); if (managedEntity == null) { if (StringUtils.isEmpty(entity.getFormat())) { entity.setFormat(ConfigFileFormat.Properties.getValue()); } entity = appNamespaceService.createAppNamespace(entity); } else if (silentCreation) { appNamespaceService.createNamespaceForAppNamespaceInAllCluster(appNamespace.getAppId(), appNamespace.getName(), appNamespace.getDataChangeCreatedBy()); entity = managedEntity; } else { throw BadRequestException.appNamespaceAlreadyExists(entity.getAppId(), entity.getName()); } return BeanUtils.transform(AppNamespaceDTO.class, entity); } @DeleteMapping("/apps/{appId}/appnamespaces/{namespaceName:.+}") public void delete(@PathVariable("appId") String appId, @PathVariable("namespaceName") String namespaceName, @RequestParam String operator) { AppNamespace entity = appNamespaceService.findOne(appId, namespaceName); if (entity == null) { throw BadRequestException.appNamespaceNotExists(appId, namespaceName); } appNamespaceService.deleteAppNamespace(entity, operator); } @GetMapping("/appnamespaces/{publicNamespaceName}/namespaces") public List findPublicAppNamespaceAllNamespaces( @PathVariable String publicNamespaceName, Pageable pageable) { List namespaces = namespaceService.findPublicAppNamespaceAllNamespaces(publicNamespaceName, pageable); return BeanUtils.batchTransform(NamespaceDTO.class, namespaces); } @GetMapping("/appnamespaces/{publicNamespaceName}/associated-namespaces/count") public int countPublicAppNamespaceAssociatedNamespaces(@PathVariable String publicNamespaceName) { return namespaceService.countPublicAppNamespaceAssociatedNamespaces(publicNamespaceName); } @GetMapping("/apps/{appId}/appnamespaces") public List getAppNamespaces(@PathVariable("appId") String appId) { List appNamespaces = appNamespaceService.findByAppId(appId); return BeanUtils.batchTransform(AppNamespaceDTO.class, appNamespaces); } } ================================================ FILE: apollo-adminservice/src/main/java/com/ctrip/framework/apollo/adminservice/controller/ClusterController.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.adminservice.controller; import com.ctrip.framework.apollo.biz.entity.Cluster; import com.ctrip.framework.apollo.biz.service.ClusterService; import com.ctrip.framework.apollo.common.dto.ClusterDTO; import com.ctrip.framework.apollo.common.exception.BadRequestException; import com.ctrip.framework.apollo.common.exception.NotFoundException; import com.ctrip.framework.apollo.common.utils.BeanUtils; import com.ctrip.framework.apollo.core.ConfigConsts; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import jakarta.validation.Valid; import java.util.List; @RestController public class ClusterController { private final ClusterService clusterService; public ClusterController(final ClusterService clusterService) { this.clusterService = clusterService; } @PostMapping("/apps/{appId}/clusters") public ClusterDTO create(@PathVariable("appId") String appId, @RequestParam(value = "autoCreatePrivateNamespace", defaultValue = "true") boolean autoCreatePrivateNamespace, @Valid @RequestBody ClusterDTO dto) { Cluster entity = BeanUtils.transform(Cluster.class, dto); Cluster managedEntity = clusterService.findOne(appId, entity.getName()); if (managedEntity != null) { throw BadRequestException.clusterAlreadyExists(entity.getName()); } if (autoCreatePrivateNamespace) { entity = clusterService.saveWithInstanceOfAppNamespaces(entity); } else { entity = clusterService.saveWithoutInstanceOfAppNamespaces(entity); } return BeanUtils.transform(ClusterDTO.class, entity); } @DeleteMapping("/apps/{appId}/clusters/{clusterName:.+}") public void delete(@PathVariable("appId") String appId, @PathVariable("clusterName") String clusterName, @RequestParam String operator) { Cluster entity = clusterService.findOne(appId, clusterName); if (entity == null) { throw NotFoundException.clusterNotFound(appId, clusterName); } if (ConfigConsts.CLUSTER_NAME_DEFAULT.equals(entity.getName())) { throw new BadRequestException("can not delete default cluster!"); } clusterService.delete(entity.getId(), operator); } @GetMapping("/apps/{appId}/clusters") public List find(@PathVariable("appId") String appId) { List clusters = clusterService.findParentClusters(appId); return BeanUtils.batchTransform(ClusterDTO.class, clusters); } @GetMapping("/apps/{appId}/clusters/{clusterName:.+}") public ClusterDTO get(@PathVariable("appId") String appId, @PathVariable("clusterName") String clusterName) { Cluster cluster = clusterService.findOne(appId, clusterName); if (cluster == null) { throw NotFoundException.clusterNotFound(appId, clusterName); } return BeanUtils.transform(ClusterDTO.class, cluster); } @GetMapping("/apps/{appId}/cluster/{clusterName}/unique") public boolean isAppIdUnique(@PathVariable("appId") String appId, @PathVariable("clusterName") String clusterName) { return clusterService.isClusterNameUnique(appId, clusterName); } } ================================================ FILE: apollo-adminservice/src/main/java/com/ctrip/framework/apollo/adminservice/controller/CommitController.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.adminservice.controller; import com.ctrip.framework.apollo.biz.entity.Commit; import com.ctrip.framework.apollo.biz.service.CommitService; import com.ctrip.framework.apollo.common.dto.CommitDTO; import com.ctrip.framework.apollo.common.utils.BeanUtils; import com.ctrip.framework.apollo.core.utils.StringUtils; import org.springframework.data.domain.Pageable; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.util.List; @RestController public class CommitController { private final CommitService commitService; public CommitController(final CommitService commitService) { this.commitService = commitService; } @GetMapping("/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/commit") public List find(@PathVariable String appId, @PathVariable String clusterName, @PathVariable String namespaceName, @RequestParam(required = false) String key, Pageable pageable) { List commits; if (StringUtils.isEmpty(key)) { commits = commitService.find(appId, clusterName, namespaceName, pageable); } else { commits = commitService.findByKey(appId, clusterName, namespaceName, key, pageable); } return BeanUtils.batchTransform(CommitDTO.class, commits); } } ================================================ FILE: apollo-adminservice/src/main/java/com/ctrip/framework/apollo/adminservice/controller/IndexController.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.adminservice.controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping(path = "/") public class IndexController { @GetMapping public String index() { return "apollo-adminservice"; } } ================================================ FILE: apollo-adminservice/src/main/java/com/ctrip/framework/apollo/adminservice/controller/InstanceConfigController.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.adminservice.controller; import com.ctrip.framework.apollo.biz.entity.Instance; import com.ctrip.framework.apollo.biz.entity.InstanceConfig; import com.ctrip.framework.apollo.biz.entity.Release; import com.ctrip.framework.apollo.biz.service.InstanceService; import com.ctrip.framework.apollo.biz.service.ReleaseService; import com.ctrip.framework.apollo.common.dto.InstanceConfigDTO; import com.ctrip.framework.apollo.common.dto.InstanceDTO; import com.ctrip.framework.apollo.common.dto.PageDTO; import com.ctrip.framework.apollo.common.dto.ReleaseDTO; import com.ctrip.framework.apollo.common.exception.NotFoundException; import com.ctrip.framework.apollo.common.utils.BeanUtils; import com.google.common.base.Splitter; import com.google.common.base.Strings; import com.google.common.collect.HashMultimap; import com.google.common.collect.Maps; import com.google.common.collect.Multimap; import com.google.common.collect.Sets; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.util.CollectionUtils; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; /** * @author Jason Song(song_s@ctrip.com) */ @RestController @RequestMapping("/instances") public class InstanceConfigController { private static final Splitter RELEASES_SPLITTER = Splitter.on(",").omitEmptyStrings().trimResults(); private final ReleaseService releaseService; private final InstanceService instanceService; public InstanceConfigController(final ReleaseService releaseService, final InstanceService instanceService) { this.releaseService = releaseService; this.instanceService = instanceService; } @GetMapping("/by-release") public PageDTO getByRelease(@RequestParam("releaseId") long releaseId, Pageable pageable) { Release release = releaseService.findOne(releaseId); if (release == null) { throw NotFoundException.releaseNotFound(releaseId); } Page instanceConfigsPage = instanceService.findActiveInstanceConfigsByReleaseKey(release.getReleaseKey(), pageable); List instanceDTOs = Collections.emptyList(); if (instanceConfigsPage.hasContent()) { Multimap instanceConfigMap = HashMultimap.create(); Set otherReleaseKeys = Sets.newHashSet(); for (InstanceConfig instanceConfig : instanceConfigsPage.getContent()) { instanceConfigMap.put(instanceConfig.getInstanceId(), instanceConfig); otherReleaseKeys.add(instanceConfig.getReleaseKey()); } Set instanceIds = instanceConfigMap.keySet(); List instances = instanceService.findInstancesByIds(instanceIds); if (!CollectionUtils.isEmpty(instances)) { instanceDTOs = BeanUtils.batchTransform(InstanceDTO.class, instances); } for (InstanceDTO instanceDTO : instanceDTOs) { Collection configs = instanceConfigMap.get(instanceDTO.getId()); List configDTOs = configs.stream().map(instanceConfig -> { InstanceConfigDTO instanceConfigDTO = new InstanceConfigDTO(); // to save some space instanceConfigDTO.setRelease(null); instanceConfigDTO.setReleaseDeliveryTime(instanceConfig.getReleaseDeliveryTime()); instanceConfigDTO .setDataChangeLastModifiedTime(instanceConfig.getDataChangeLastModifiedTime()); return instanceConfigDTO; }).collect(Collectors.toList()); instanceDTO.setConfigs(configDTOs); } } return new PageDTO<>(instanceDTOs, pageable, instanceConfigsPage.getTotalElements()); } @GetMapping("/by-namespace-and-releases-not-in") public List getByReleasesNotIn(@RequestParam("appId") String appId, @RequestParam("clusterName") String clusterName, @RequestParam("namespaceName") String namespaceName, @RequestParam("releaseIds") String releaseIds) { Set releaseIdSet = RELEASES_SPLITTER.splitToList(releaseIds).stream().map(Long::parseLong) .collect(Collectors.toSet()); List releases = releaseService.findByReleaseIds(releaseIdSet); if (CollectionUtils.isEmpty(releases)) { throw NotFoundException.releaseNotFound(releaseIds); } Set releaseKeys = releases.stream().map(Release::getReleaseKey).collect(Collectors.toSet()); List instanceConfigs = instanceService.findInstanceConfigsByNamespaceWithReleaseKeysNotIn(appId, clusterName, namespaceName, releaseKeys); Multimap instanceConfigMap = HashMultimap.create(); Set otherReleaseKeys = Sets.newHashSet(); for (InstanceConfig instanceConfig : instanceConfigs) { instanceConfigMap.put(instanceConfig.getInstanceId(), instanceConfig); otherReleaseKeys.add(instanceConfig.getReleaseKey()); } List instances = instanceService.findInstancesByIds(instanceConfigMap.keySet()); if (CollectionUtils.isEmpty(instances)) { return Collections.emptyList(); } List instanceDTOs = BeanUtils.batchTransform(InstanceDTO.class, instances); List otherReleases = releaseService.findByReleaseKeys(otherReleaseKeys); Map releaseMap = Maps.newHashMap(); for (Release release : otherReleases) { // unset configurations to save space release.setConfigurations(null); ReleaseDTO releaseDTO = BeanUtils.transform(ReleaseDTO.class, release); releaseMap.put(release.getReleaseKey(), releaseDTO); } for (InstanceDTO instanceDTO : instanceDTOs) { Collection configs = instanceConfigMap.get(instanceDTO.getId()); List configDTOs = configs.stream().map(instanceConfig -> { InstanceConfigDTO instanceConfigDTO = new InstanceConfigDTO(); instanceConfigDTO.setRelease(releaseMap.get(instanceConfig.getReleaseKey())); instanceConfigDTO.setReleaseDeliveryTime(instanceConfig.getReleaseDeliveryTime()); instanceConfigDTO .setDataChangeLastModifiedTime(instanceConfig.getDataChangeLastModifiedTime()); return instanceConfigDTO; }).collect(Collectors.toList()); instanceDTO.setConfigs(configDTOs); } return instanceDTOs; } @GetMapping("/by-namespace") public PageDTO getInstancesByNamespace(@RequestParam("appId") String appId, @RequestParam("clusterName") String clusterName, @RequestParam("namespaceName") String namespaceName, @RequestParam(value = "instanceAppId", required = false) String instanceAppId, Pageable pageable) { Page instances; if (Strings.isNullOrEmpty(instanceAppId)) { instances = instanceService.findInstancesByNamespace(appId, clusterName, namespaceName, pageable); } else { instances = instanceService.findInstancesByNamespaceAndInstanceAppId(instanceAppId, appId, clusterName, namespaceName, pageable); } List instanceDTOs = BeanUtils.batchTransform(InstanceDTO.class, instances.getContent()); return new PageDTO<>(instanceDTOs, pageable, instances.getTotalElements()); } @GetMapping("/by-namespace/count") public long getInstancesCountByNamespace(@RequestParam("appId") String appId, @RequestParam("clusterName") String clusterName, @RequestParam("namespaceName") String namespaceName) { Page instances = instanceService.findInstancesByNamespace(appId, clusterName, namespaceName, PageRequest.of(0, 1)); return instances.getTotalElements(); } } ================================================ FILE: apollo-adminservice/src/main/java/com/ctrip/framework/apollo/adminservice/controller/ItemController.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.adminservice.controller; import com.ctrip.framework.apollo.adminservice.aop.PreAcquireNamespaceLock; import com.ctrip.framework.apollo.biz.config.BizConfig; import com.ctrip.framework.apollo.biz.entity.Commit; import com.ctrip.framework.apollo.biz.entity.Item; import com.ctrip.framework.apollo.biz.entity.Namespace; import com.ctrip.framework.apollo.biz.entity.Release; import com.ctrip.framework.apollo.biz.service.CommitService; import com.ctrip.framework.apollo.biz.service.ItemService; import com.ctrip.framework.apollo.biz.service.NamespaceService; import com.ctrip.framework.apollo.biz.service.ReleaseService; import com.ctrip.framework.apollo.biz.utils.ConfigChangeContentBuilder; import com.ctrip.framework.apollo.common.dto.ItemDTO; import com.ctrip.framework.apollo.common.dto.ItemInfoDTO; import com.ctrip.framework.apollo.common.dto.PageDTO; import com.ctrip.framework.apollo.common.exception.BadRequestException; import com.ctrip.framework.apollo.common.exception.NotFoundException; import com.ctrip.framework.apollo.common.utils.BeanUtils; import com.ctrip.framework.apollo.core.utils.StringUtils; import java.nio.charset.StandardCharsets; import java.util.Base64; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController public class ItemController { private final ItemService itemService; private final NamespaceService namespaceService; private final CommitService commitService; private final ReleaseService releaseService; private final BizConfig bizConfig; public ItemController(final ItemService itemService, final NamespaceService namespaceService, final CommitService commitService, final ReleaseService releaseService, final BizConfig bizConfig) { this.itemService = itemService; this.namespaceService = namespaceService; this.commitService = commitService; this.releaseService = releaseService; this.bizConfig = bizConfig; } @PreAcquireNamespaceLock @PostMapping("/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/items") public ItemDTO create(@PathVariable("appId") String appId, @PathVariable("clusterName") String clusterName, @PathVariable("namespaceName") String namespaceName, @RequestBody ItemDTO dto) { Item entity = BeanUtils.transform(Item.class, dto); Item managedEntity = itemService.findOne(appId, clusterName, namespaceName, entity.getKey()); if (managedEntity != null) { throw BadRequestException.itemAlreadyExists(entity.getKey()); } if (bizConfig.isItemNumLimitEnabled()) { int itemCount = itemService.findNonEmptyItemCount(entity.getNamespaceId()); if (itemCount >= bizConfig.itemNumLimit()) { throw new BadRequestException("The maximum number of items (" + bizConfig.itemNumLimit() + ") for this namespace has been reached. Current item count is " + itemCount + "."); } } entity = itemService.save(entity); dto = BeanUtils.transform(ItemDTO.class, entity); commitService.createCommit(appId, clusterName, namespaceName, new ConfigChangeContentBuilder().createItem(entity).build(), dto.getDataChangeLastModifiedBy()); return dto; } @PostMapping("/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/comment_items") public ItemDTO createComment(@PathVariable("appId") String appId, @PathVariable("clusterName") String clusterName, @PathVariable("namespaceName") String namespaceName, @RequestBody ItemDTO dto) { if (!StringUtils.isBlank(dto.getKey()) || !StringUtils.isBlank(dto.getValue())) { throw new BadRequestException("Comment item's key or value should be blank."); } if (StringUtils.isBlank(dto.getComment())) { throw new BadRequestException("Comment item's comment should not be blank."); } // check if comment existed List allItems = itemService.findItemsWithOrdered(appId, clusterName, namespaceName); for (Item item : allItems) { if (StringUtils.isBlank(item.getKey()) && StringUtils.isBlank(item.getValue()) && Objects.equals(item.getComment(), dto.getComment())) { return BeanUtils.transform(ItemDTO.class, item); } } Item entity = BeanUtils.transform(Item.class, dto); entity = itemService.saveComment(entity); return BeanUtils.transform(ItemDTO.class, entity); } @PreAcquireNamespaceLock @PutMapping("/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/items/{itemId}") public ItemDTO update(@PathVariable("appId") String appId, @PathVariable("clusterName") String clusterName, @PathVariable("namespaceName") String namespaceName, @PathVariable("itemId") long itemId, @RequestBody ItemDTO itemDTO) { Item managedEntity = itemService.findOne(itemId); if (managedEntity == null) { throw NotFoundException.itemNotFound(appId, clusterName, namespaceName, itemId); } Namespace namespace = namespaceService.findOne(appId, clusterName, namespaceName); // In case someone constructs an attack scenario if (namespace == null || namespace.getId() != managedEntity.getNamespaceId()) { throw BadRequestException.namespaceNotMatch(); } Item entity = BeanUtils.transform(Item.class, itemDTO); ConfigChangeContentBuilder builder = new ConfigChangeContentBuilder(); Item beforeUpdateItem = BeanUtils.transform(Item.class, managedEntity); // protect. only value,type,comment,lastModifiedBy can be modified managedEntity.setType(entity.getType()); managedEntity.setValue(entity.getValue()); managedEntity.setComment(entity.getComment()); managedEntity.setDataChangeLastModifiedBy(entity.getDataChangeLastModifiedBy()); entity = itemService.update(managedEntity); builder.updateItem(beforeUpdateItem, entity); itemDTO = BeanUtils.transform(ItemDTO.class, entity); if (builder.hasContent()) { commitService.createCommit(appId, clusterName, namespaceName, builder.build(), itemDTO.getDataChangeLastModifiedBy()); } return itemDTO; } @PreAcquireNamespaceLock @DeleteMapping("/items/{itemId}") public void delete(@PathVariable("itemId") long itemId, @RequestParam String operator) { Item entity = itemService.findOne(itemId); if (entity == null) { throw NotFoundException.itemNotFound(itemId); } itemService.delete(entity.getId(), operator); Namespace namespace = namespaceService.findOne(entity.getNamespaceId()); commitService.createCommit(namespace.getAppId(), namespace.getClusterName(), namespace.getNamespaceName(), new ConfigChangeContentBuilder().deleteItem(entity).build(), operator); } @GetMapping("/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/items") public List findItems(@PathVariable("appId") String appId, @PathVariable("clusterName") String clusterName, @PathVariable("namespaceName") String namespaceName) { return BeanUtils.batchTransform(ItemDTO.class, itemService.findItemsWithOrdered(appId, clusterName, namespaceName)); } @GetMapping("/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/items/deleted") public List findDeletedItems(@PathVariable("appId") String appId, @PathVariable("clusterName") String clusterName, @PathVariable("namespaceName") String namespaceName) { // get latest release time Release latestActiveRelease = releaseService.findLatestActiveRelease(appId, clusterName, namespaceName); List commits; if (Objects.nonNull(latestActiveRelease)) { commits = commitService.find(appId, clusterName, namespaceName, latestActiveRelease.getDataChangeCreatedTime(), null); } else { commits = commitService.find(appId, clusterName, namespaceName, null); } if (Objects.nonNull(commits)) { List deletedItems = commits .stream().map(item -> ConfigChangeContentBuilder .convertJsonString(item.getChangeSets()).getDeleteItems()) .flatMap(Collection::stream).collect(Collectors.toList()); return BeanUtils.batchTransform(ItemDTO.class, deletedItems); } return Collections.emptyList(); } @GetMapping("/items-search/key-and-value") public PageDTO getItemInfoBySearch( @RequestParam(value = "key", required = false) String key, @RequestParam(value = "value", required = false) String value, Pageable limit) { Page pageItemInfoDTO = itemService.getItemInfoBySearch(key, value, limit); return new PageDTO<>(pageItemInfoDTO.getContent(), limit, pageItemInfoDTO.getTotalElements()); } @GetMapping("/items/{itemId}") public ItemDTO get(@PathVariable("itemId") long itemId) { Item item = itemService.findOne(itemId); if (item == null) { throw NotFoundException.itemNotFound(itemId); } return BeanUtils.transform(ItemDTO.class, item); } @GetMapping("/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/items/{key:.+}") public ItemDTO get(@PathVariable("appId") String appId, @PathVariable("clusterName") String clusterName, @PathVariable("namespaceName") String namespaceName, @PathVariable("key") String key) { Item item = itemService.findOne(appId, clusterName, namespaceName, key); if (item == null) { throw NotFoundException.itemNotFound(appId, clusterName, namespaceName, key); } return BeanUtils.transform(ItemDTO.class, item); } @GetMapping("/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/encodedItems/{key:.+}") public ItemDTO getByEncodedKey(@PathVariable("appId") String appId, @PathVariable("clusterName") String clusterName, @PathVariable("namespaceName") String namespaceName, @PathVariable("key") String key) { return this.get(appId, clusterName, namespaceName, new String(Base64.getUrlDecoder().decode(key.getBytes(StandardCharsets.UTF_8)))); } @GetMapping( value = "/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/items-with-page") public PageDTO findItemsByNamespace(@PathVariable("appId") String appId, @PathVariable("clusterName") String clusterName, @PathVariable("namespaceName") String namespaceName, Pageable pageable) { Page itemPage = itemService.findItemsByNamespace(appId, clusterName, namespaceName, pageable); List itemDTOS = BeanUtils.batchTransform(ItemDTO.class, itemPage.getContent()); return new PageDTO<>(itemDTOS, pageable, itemPage.getTotalElements()); } } ================================================ FILE: apollo-adminservice/src/main/java/com/ctrip/framework/apollo/adminservice/controller/ItemSetController.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.adminservice.controller; import com.ctrip.framework.apollo.adminservice.aop.PreAcquireNamespaceLock; import com.ctrip.framework.apollo.biz.service.ItemSetService; import com.ctrip.framework.apollo.common.dto.ItemChangeSets; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; @RestController public class ItemSetController { private final ItemSetService itemSetService; public ItemSetController(final ItemSetService itemSetService) { this.itemSetService = itemSetService; } @PreAcquireNamespaceLock @PostMapping("/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/itemset") public ResponseEntity create(@PathVariable String appId, @PathVariable String clusterName, @PathVariable String namespaceName, @RequestBody ItemChangeSets changeSet) { itemSetService.updateSet(appId, clusterName, namespaceName, changeSet); return ResponseEntity.status(HttpStatus.OK).build(); } } ================================================ FILE: apollo-adminservice/src/main/java/com/ctrip/framework/apollo/adminservice/controller/NamespaceBranchController.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.adminservice.controller; import com.ctrip.framework.apollo.biz.entity.GrayReleaseRule; import com.ctrip.framework.apollo.biz.entity.Namespace; import com.ctrip.framework.apollo.biz.message.MessageSender; import com.ctrip.framework.apollo.biz.message.Topics; import com.ctrip.framework.apollo.biz.service.NamespaceBranchService; import com.ctrip.framework.apollo.biz.service.NamespaceService; import com.ctrip.framework.apollo.biz.utils.ReleaseMessageKeyGenerator; import com.ctrip.framework.apollo.common.constants.NamespaceBranchStatus; import com.ctrip.framework.apollo.common.dto.GrayReleaseRuleDTO; import com.ctrip.framework.apollo.common.dto.NamespaceDTO; import com.ctrip.framework.apollo.common.exception.BadRequestException; import com.ctrip.framework.apollo.common.utils.BeanUtils; import com.ctrip.framework.apollo.common.utils.GrayReleaseRuleItemTransformer; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController public class NamespaceBranchController { private final MessageSender messageSender; private final NamespaceBranchService namespaceBranchService; private final NamespaceService namespaceService; public NamespaceBranchController(final MessageSender messageSender, final NamespaceBranchService namespaceBranchService, final NamespaceService namespaceService) { this.messageSender = messageSender; this.namespaceBranchService = namespaceBranchService; this.namespaceService = namespaceService; } @PostMapping("/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/branches") public NamespaceDTO createBranch(@PathVariable String appId, @PathVariable String clusterName, @PathVariable String namespaceName, @RequestParam("operator") String operator) { checkNamespace(appId, clusterName, namespaceName); Namespace createdBranch = namespaceBranchService.createBranch(appId, clusterName, namespaceName, operator); return BeanUtils.transform(NamespaceDTO.class, createdBranch); } @GetMapping("/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/branches/{branchName}/rules") public GrayReleaseRuleDTO findBranchGrayRules(@PathVariable String appId, @PathVariable String clusterName, @PathVariable String namespaceName, @PathVariable String branchName) { checkBranch(appId, clusterName, namespaceName, branchName); GrayReleaseRule rules = namespaceBranchService.findBranchGrayRules(appId, clusterName, namespaceName, branchName); if (rules == null) { return null; } GrayReleaseRuleDTO ruleDTO = new GrayReleaseRuleDTO(rules.getAppId(), rules.getClusterName(), rules.getNamespaceName(), rules.getBranchName()); ruleDTO.setReleaseId(rules.getReleaseId()); ruleDTO.setRuleItems(GrayReleaseRuleItemTransformer.batchTransformFromJSON(rules.getRules())); return ruleDTO; } @Transactional @PutMapping("/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/branches/{branchName}/rules") public void updateBranchGrayRules(@PathVariable String appId, @PathVariable String clusterName, @PathVariable String namespaceName, @PathVariable String branchName, @RequestBody GrayReleaseRuleDTO newRuleDto) { checkBranch(appId, clusterName, namespaceName, branchName); GrayReleaseRule newRules = BeanUtils.transform(GrayReleaseRule.class, newRuleDto); newRules .setRules(GrayReleaseRuleItemTransformer.batchTransformToJSON(newRuleDto.getRuleItems())); newRules.setBranchStatus(NamespaceBranchStatus.ACTIVE); namespaceBranchService.updateBranchGrayRules(appId, clusterName, namespaceName, branchName, newRules); messageSender.sendMessage( ReleaseMessageKeyGenerator.generate(appId, clusterName, namespaceName), Topics.APOLLO_RELEASE_TOPIC); } @Transactional @DeleteMapping("/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/branches/{branchName}") public void deleteBranch(@PathVariable String appId, @PathVariable String clusterName, @PathVariable String namespaceName, @PathVariable String branchName, @RequestParam("operator") String operator) { checkBranch(appId, clusterName, namespaceName, branchName); namespaceBranchService.deleteBranch(appId, clusterName, namespaceName, branchName, NamespaceBranchStatus.DELETED, operator); messageSender.sendMessage( ReleaseMessageKeyGenerator.generate(appId, clusterName, namespaceName), Topics.APOLLO_RELEASE_TOPIC); } @GetMapping("/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/branches") public NamespaceDTO loadNamespaceBranch(@PathVariable String appId, @PathVariable String clusterName, @PathVariable String namespaceName) { checkNamespace(appId, clusterName, namespaceName); Namespace childNamespace = namespaceBranchService.findBranch(appId, clusterName, namespaceName); if (childNamespace == null) { return null; } return BeanUtils.transform(NamespaceDTO.class, childNamespace); } private void checkBranch(String appId, String clusterName, String namespaceName, String branchName) { // 1. check parent namespace checkNamespace(appId, clusterName, namespaceName); // 2. check child namespace Namespace childNamespace = namespaceService.findOne(appId, branchName, namespaceName); if (childNamespace == null) { throw new BadRequestException( "Namespace's branch not exist. AppId = %s, ClusterName = %s, NamespaceName = %s, BranchName = %s", appId, clusterName, namespaceName, branchName); } } private void checkNamespace(String appId, String clusterName, String namespaceName) { Namespace parentNamespace = namespaceService.findOne(appId, clusterName, namespaceName); if (parentNamespace == null) { throw BadRequestException.namespaceNotExists(appId, clusterName, namespaceName); } } } ================================================ FILE: apollo-adminservice/src/main/java/com/ctrip/framework/apollo/adminservice/controller/NamespaceController.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.adminservice.controller; import com.ctrip.framework.apollo.biz.entity.Namespace; import com.ctrip.framework.apollo.biz.service.NamespaceService; import com.ctrip.framework.apollo.common.dto.NamespaceDTO; import com.ctrip.framework.apollo.common.dto.PageDTO; import com.ctrip.framework.apollo.common.exception.BadRequestException; import com.ctrip.framework.apollo.common.exception.NotFoundException; import com.ctrip.framework.apollo.common.utils.BeanUtils; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import jakarta.validation.Valid; import java.util.List; import java.util.Map; @RestController public class NamespaceController { private final NamespaceService namespaceService; public NamespaceController(final NamespaceService namespaceService) { this.namespaceService = namespaceService; } @PostMapping("/apps/{appId}/clusters/{clusterName}/namespaces") public NamespaceDTO create(@PathVariable("appId") String appId, @PathVariable("clusterName") String clusterName, @Valid @RequestBody NamespaceDTO dto) { Namespace entity = BeanUtils.transform(Namespace.class, dto); Namespace managedEntity = namespaceService.findOne(appId, clusterName, entity.getNamespaceName()); if (managedEntity != null) { throw BadRequestException.namespaceAlreadyExists(entity.getNamespaceName()); } entity = namespaceService.save(entity); return BeanUtils.transform(NamespaceDTO.class, entity); } @DeleteMapping("/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName:.+}") public void delete(@PathVariable("appId") String appId, @PathVariable("clusterName") String clusterName, @PathVariable("namespaceName") String namespaceName, @RequestParam String operator) { Namespace entity = namespaceService.findOne(appId, clusterName, namespaceName); if (entity == null) { throw NotFoundException.namespaceNotFound(appId, clusterName, namespaceName); } namespaceService.deleteNamespace(entity, operator); } @GetMapping("/apps/{appId}/clusters/{clusterName}/namespaces") public List find(@PathVariable("appId") String appId, @PathVariable("clusterName") String clusterName) { List groups = namespaceService.findNamespaces(appId, clusterName); return BeanUtils.batchTransform(NamespaceDTO.class, groups); } @GetMapping("/namespaces/{namespaceId}") public NamespaceDTO get(@PathVariable("namespaceId") Long namespaceId) { Namespace namespace = namespaceService.findOne(namespaceId); if (namespace == null) { throw NotFoundException.itemNotFound(namespaceId); } return BeanUtils.transform(NamespaceDTO.class, namespace); } /** * the returned content's size is not fixed. so please carefully used. */ @GetMapping("/namespaces/find-by-item") public PageDTO findByItem(@RequestParam String itemKey, Pageable pageable) { Page namespacePage = namespaceService.findByItem(itemKey, pageable); List namespaceDTOS = BeanUtils.batchTransform(NamespaceDTO.class, namespacePage.getContent()); return new PageDTO<>(namespaceDTOS, pageable, namespacePage.getTotalElements()); } @GetMapping("/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName:.+}") public NamespaceDTO get(@PathVariable("appId") String appId, @PathVariable("clusterName") String clusterName, @PathVariable("namespaceName") String namespaceName) { Namespace namespace = namespaceService.findOne(appId, clusterName, namespaceName); if (namespace == null) { throw NotFoundException.namespaceNotFound(appId, clusterName, namespaceName); } return BeanUtils.transform(NamespaceDTO.class, namespace); } @GetMapping("/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/associated-public-namespace") public NamespaceDTO findPublicNamespaceForAssociatedNamespace(@PathVariable String appId, @PathVariable String clusterName, @PathVariable String namespaceName) { Namespace namespace = namespaceService.findPublicNamespaceForAssociatedNamespace(clusterName, namespaceName); if (namespace == null) { throw new NotFoundException("public namespace not found. namespace:%s", namespaceName); } return BeanUtils.transform(NamespaceDTO.class, namespace); } /** * cluster -> cluster has not published namespaces? */ @GetMapping("/apps/{appId}/namespaces/publish_info") public Map namespacePublishInfo(@PathVariable String appId) { return namespaceService.namespacePublishInfo(appId); } } ================================================ FILE: apollo-adminservice/src/main/java/com/ctrip/framework/apollo/adminservice/controller/NamespaceLockController.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.adminservice.controller; import com.ctrip.framework.apollo.biz.config.BizConfig; import com.ctrip.framework.apollo.biz.entity.Namespace; import com.ctrip.framework.apollo.biz.entity.NamespaceLock; import com.ctrip.framework.apollo.biz.service.NamespaceLockService; import com.ctrip.framework.apollo.biz.service.NamespaceService; import com.ctrip.framework.apollo.common.dto.NamespaceLockDTO; import com.ctrip.framework.apollo.common.exception.BadRequestException; import com.ctrip.framework.apollo.common.utils.BeanUtils; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; @RestController public class NamespaceLockController { private final NamespaceLockService namespaceLockService; private final NamespaceService namespaceService; private final BizConfig bizConfig; public NamespaceLockController(final NamespaceLockService namespaceLockService, final NamespaceService namespaceService, final BizConfig bizConfig) { this.namespaceLockService = namespaceLockService; this.namespaceService = namespaceService; this.bizConfig = bizConfig; } @GetMapping("/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/lock") public NamespaceLockDTO getNamespaceLockOwner(@PathVariable String appId, @PathVariable String clusterName, @PathVariable String namespaceName) { Namespace namespace = namespaceService.findOne(appId, clusterName, namespaceName); if (namespace == null) { throw BadRequestException.namespaceNotExists(appId, clusterName, namespaceName); } if (bizConfig.isNamespaceLockSwitchOff()) { return null; } NamespaceLock lock = namespaceLockService.findLock(namespace.getId()); if (lock == null) { return null; } return BeanUtils.transform(NamespaceLockDTO.class, lock); } } ================================================ FILE: apollo-adminservice/src/main/java/com/ctrip/framework/apollo/adminservice/controller/ReleaseController.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.adminservice.controller; import com.ctrip.framework.apollo.biz.entity.Namespace; import com.ctrip.framework.apollo.biz.entity.Release; import com.ctrip.framework.apollo.biz.message.MessageSender; import com.ctrip.framework.apollo.biz.message.Topics; import com.ctrip.framework.apollo.biz.service.NamespaceBranchService; import com.ctrip.framework.apollo.biz.service.NamespaceService; import com.ctrip.framework.apollo.biz.service.ReleaseService; import com.ctrip.framework.apollo.biz.utils.ReleaseMessageKeyGenerator; import com.ctrip.framework.apollo.common.constants.NamespaceBranchStatus; import com.ctrip.framework.apollo.common.dto.ItemChangeSets; import com.ctrip.framework.apollo.common.dto.ReleaseDTO; import com.ctrip.framework.apollo.common.exception.NotFoundException; import com.ctrip.framework.apollo.common.utils.BeanUtils; import com.google.common.base.Splitter; import org.springframework.data.domain.Pageable; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.util.List; import java.util.Set; import java.util.stream.Collectors; @RestController public class ReleaseController { private static final Splitter RELEASES_SPLITTER = Splitter.on(",").omitEmptyStrings().trimResults(); private final ReleaseService releaseService; private final NamespaceService namespaceService; private final MessageSender messageSender; private final NamespaceBranchService namespaceBranchService; public ReleaseController(final ReleaseService releaseService, final NamespaceService namespaceService, final MessageSender messageSender, final NamespaceBranchService namespaceBranchService) { this.releaseService = releaseService; this.namespaceService = namespaceService; this.messageSender = messageSender; this.namespaceBranchService = namespaceBranchService; } @GetMapping("/releases/{releaseId}") public ReleaseDTO get(@PathVariable("releaseId") long releaseId) { Release release = releaseService.findOne(releaseId); if (release == null) { throw NotFoundException.releaseNotFound(releaseId); } return BeanUtils.transform(ReleaseDTO.class, release); } @GetMapping("/releases") public List findReleaseByIds(@RequestParam("releaseIds") String releaseIds) { Set releaseIdSet = RELEASES_SPLITTER.splitToList(releaseIds).stream().map(Long::parseLong) .collect(Collectors.toSet()); List releases = releaseService.findByReleaseIds(releaseIdSet); return BeanUtils.batchTransform(ReleaseDTO.class, releases); } @GetMapping("/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/releases/all") public List findAllReleases(@PathVariable("appId") String appId, @PathVariable("clusterName") String clusterName, @PathVariable("namespaceName") String namespaceName, Pageable page) { List releases = releaseService.findAllReleases(appId, clusterName, namespaceName, page); return BeanUtils.batchTransform(ReleaseDTO.class, releases); } @GetMapping("/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/releases/active") public List findActiveReleases(@PathVariable("appId") String appId, @PathVariable("clusterName") String clusterName, @PathVariable("namespaceName") String namespaceName, Pageable page) { List releases = releaseService.findActiveReleases(appId, clusterName, namespaceName, page); return BeanUtils.batchTransform(ReleaseDTO.class, releases); } @GetMapping("/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/releases/latest") public ReleaseDTO getLatest(@PathVariable("appId") String appId, @PathVariable("clusterName") String clusterName, @PathVariable("namespaceName") String namespaceName) { Release release = releaseService.findLatestActiveRelease(appId, clusterName, namespaceName); return BeanUtils.transform(ReleaseDTO.class, release); } @Transactional @PostMapping("/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/releases") public ReleaseDTO publish(@PathVariable("appId") String appId, @PathVariable("clusterName") String clusterName, @PathVariable("namespaceName") String namespaceName, @RequestParam("name") String releaseName, @RequestParam(name = "comment", required = false) String releaseComment, @RequestParam("operator") String operator, @RequestParam(name = "isEmergencyPublish", defaultValue = "false") boolean isEmergencyPublish) { Namespace namespace = namespaceService.findOne(appId, clusterName, namespaceName); if (namespace == null) { throw NotFoundException.namespaceNotFound(appId, clusterName, namespaceName); } Release release = releaseService.publish(namespace, releaseName, releaseComment, operator, isEmergencyPublish); // send release message Namespace parentNamespace = namespaceService.findParentNamespace(namespace); String messageCluster; if (parentNamespace != null) { messageCluster = parentNamespace.getClusterName(); } else { messageCluster = clusterName; } messageSender.sendMessage( ReleaseMessageKeyGenerator.generate(appId, messageCluster, namespaceName), Topics.APOLLO_RELEASE_TOPIC); return BeanUtils.transform(ReleaseDTO.class, release); } /** * merge branch items to master and publish master * * @return published result */ @Transactional @PostMapping("/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/updateAndPublish") public ReleaseDTO updateAndPublish(@PathVariable("appId") String appId, @PathVariable("clusterName") String clusterName, @PathVariable("namespaceName") String namespaceName, @RequestParam("releaseName") String releaseName, @RequestParam("branchName") String branchName, @RequestParam(value = "deleteBranch", defaultValue = "true") boolean deleteBranch, @RequestParam(name = "releaseComment", required = false) String releaseComment, @RequestParam(name = "isEmergencyPublish", defaultValue = "false") boolean isEmergencyPublish, @RequestBody ItemChangeSets changeSets) { Namespace namespace = namespaceService.findOne(appId, clusterName, namespaceName); if (namespace == null) { throw NotFoundException.namespaceNotFound(appId, clusterName, namespaceName); } Release release = releaseService.mergeBranchChangeSetsAndRelease(namespace, branchName, releaseName, releaseComment, isEmergencyPublish, changeSets); if (deleteBranch) { namespaceBranchService.deleteBranch(appId, clusterName, namespaceName, branchName, NamespaceBranchStatus.MERGED, changeSets.getDataChangeLastModifiedBy()); } messageSender.sendMessage( ReleaseMessageKeyGenerator.generate(appId, clusterName, namespaceName), Topics.APOLLO_RELEASE_TOPIC); return BeanUtils.transform(ReleaseDTO.class, release); } @Transactional @PutMapping("/releases/{releaseId}/rollback") public void rollback(@PathVariable("releaseId") long releaseId, @RequestParam(name = "toReleaseId", defaultValue = "-1") long toReleaseId, @RequestParam("operator") String operator) { Release release; if (toReleaseId > -1) { release = releaseService.rollbackTo(releaseId, toReleaseId, operator); } else { release = releaseService.rollback(releaseId, operator); } String appId = release.getAppId(); String clusterName = release.getClusterName(); String namespaceName = release.getNamespaceName(); // send release message messageSender.sendMessage( ReleaseMessageKeyGenerator.generate(appId, clusterName, namespaceName), Topics.APOLLO_RELEASE_TOPIC); } @Transactional @PostMapping("/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/gray-del-releases") public ReleaseDTO publish(@PathVariable("appId") String appId, @PathVariable("clusterName") String clusterName, @PathVariable("namespaceName") String namespaceName, @RequestParam("operator") String operator, @RequestParam("releaseName") String releaseName, @RequestParam(name = "comment", required = false) String releaseComment, @RequestParam(name = "isEmergencyPublish", defaultValue = "false") boolean isEmergencyPublish, @RequestParam(name = "grayDelKeys") Set grayDelKeys) { Namespace namespace = namespaceService.findOne(appId, clusterName, namespaceName); if (namespace == null) { throw NotFoundException.namespaceNotFound(appId, clusterName, namespaceName); } Release release = releaseService.grayDeletionPublish(namespace, releaseName, releaseComment, operator, isEmergencyPublish, grayDelKeys); // send release message Namespace parentNamespace = namespaceService.findParentNamespace(namespace); String messageCluster; if (parentNamespace != null) { messageCluster = parentNamespace.getClusterName(); } else { messageCluster = clusterName; } messageSender.sendMessage( ReleaseMessageKeyGenerator.generate(appId, messageCluster, namespaceName), Topics.APOLLO_RELEASE_TOPIC); return BeanUtils.transform(ReleaseDTO.class, release); } } ================================================ FILE: apollo-adminservice/src/main/java/com/ctrip/framework/apollo/adminservice/controller/ReleaseHistoryController.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.adminservice.controller; import com.ctrip.framework.apollo.biz.entity.ReleaseHistory; import com.ctrip.framework.apollo.biz.service.ReleaseHistoryService; import com.ctrip.framework.apollo.common.dto.PageDTO; import com.ctrip.framework.apollo.common.dto.ReleaseHistoryDTO; import com.ctrip.framework.apollo.common.utils.BeanUtils; import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.List; import java.util.Map; /** * @author Jason Song(song_s@ctrip.com) */ @RestController public class ReleaseHistoryController { private static final Gson GSON = new Gson(); private final Type configurationTypeReference = new TypeToken>() {}.getType(); private final ReleaseHistoryService releaseHistoryService; public ReleaseHistoryController(final ReleaseHistoryService releaseHistoryService) { this.releaseHistoryService = releaseHistoryService; } @GetMapping("/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/releases/histories") public PageDTO findReleaseHistoriesByNamespace(@PathVariable String appId, @PathVariable String clusterName, @PathVariable String namespaceName, Pageable pageable) { Page result = releaseHistoryService.findReleaseHistoriesByNamespace(appId, clusterName, namespaceName, pageable); return transform2PageDTO(result, pageable); } @GetMapping("/releases/histories/by_release_id_and_operation") public PageDTO findReleaseHistoryByReleaseIdAndOperation( @RequestParam("releaseId") long releaseId, @RequestParam("operation") int operation, Pageable pageable) { Page result = releaseHistoryService.findByReleaseIdAndOperation(releaseId, operation, pageable); return transform2PageDTO(result, pageable); } @GetMapping("/releases/histories/by_previous_release_id_and_operation") public PageDTO findReleaseHistoryByPreviousReleaseIdAndOperation( @RequestParam("previousReleaseId") long previousReleaseId, @RequestParam("operation") int operation, Pageable pageable) { Page result = releaseHistoryService .findByPreviousReleaseIdAndOperation(previousReleaseId, operation, pageable); return transform2PageDTO(result, pageable); } private PageDTO transform2PageDTO(Page releaseHistoriesPage, Pageable pageable) { if (!releaseHistoriesPage.hasContent()) { return null; } List releaseHistories = releaseHistoriesPage.getContent(); List releaseHistoryDTOs = new ArrayList<>(releaseHistories.size()); for (ReleaseHistory releaseHistory : releaseHistories) { releaseHistoryDTOs.add(transformReleaseHistory2DTO(releaseHistory)); } return new PageDTO<>(releaseHistoryDTOs, pageable, releaseHistoriesPage.getTotalElements()); } private ReleaseHistoryDTO transformReleaseHistory2DTO(ReleaseHistory releaseHistory) { ReleaseHistoryDTO dto = new ReleaseHistoryDTO(); BeanUtils.copyProperties(releaseHistory, dto, "operationContext"); dto.setOperationContext( GSON.fromJson(releaseHistory.getOperationContext(), configurationTypeReference)); return dto; } } ================================================ FILE: apollo-adminservice/src/main/java/com/ctrip/framework/apollo/adminservice/controller/ServerConfigController.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.adminservice.controller; import com.ctrip.framework.apollo.biz.entity.ServerConfig; import com.ctrip.framework.apollo.biz.service.ServerConfigService; import java.util.List; import jakarta.validation.Valid; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; /** * @author kl (http://kailing.pub) * @since 2022/12/13 */ @RestController public class ServerConfigController { private final ServerConfigService serverConfigService; public ServerConfigController(ServerConfigService serverConfigService) { this.serverConfigService = serverConfigService; } @GetMapping("/server/config/find-all-config") public List findAllServerConfig() { return serverConfigService.findAll(); } @PostMapping("/server/config") public ServerConfig createOrUpdatePortalDBConfig(@Valid @RequestBody ServerConfig serverConfig) { return serverConfigService.createOrUpdateConfig(serverConfig); } } ================================================ FILE: apollo-adminservice/src/main/java/com/ctrip/framework/apollo/adminservice/filter/AdminServiceAuthenticationFilter.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.adminservice.filter; import com.ctrip.framework.apollo.biz.config.BizConfig; import com.google.common.base.Splitter; import com.google.common.base.Strings; import java.io.IOException; import java.util.List; import jakarta.servlet.Filter; import jakarta.servlet.FilterChain; import jakarta.servlet.FilterConfig; import jakarta.servlet.ServletException; import jakarta.servlet.ServletRequest; import jakarta.servlet.ServletResponse; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpHeaders; public class AdminServiceAuthenticationFilter implements Filter { private static final Logger logger = LoggerFactory.getLogger(AdminServiceAuthenticationFilter.class); private static final Splitter ACCESS_TOKEN_SPLITTER = Splitter.on(",").omitEmptyStrings().trimResults(); private final BizConfig bizConfig; private volatile String lastAccessTokens; private volatile List accessTokenList; public AdminServiceAuthenticationFilter(BizConfig bizConfig) { this.bizConfig = bizConfig; } @Override public void init(FilterConfig filterConfig) throws ServletException { } @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { if (bizConfig.isAdminServiceAccessControlEnabled()) { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; String token = request.getHeader(HttpHeaders.AUTHORIZATION); if (!checkAccessToken(token)) { logger.warn("Invalid access token: {} for uri: {}", token, request.getRequestURI()); response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized"); return; } } chain.doFilter(req, resp); } private boolean checkAccessToken(String token) { String accessTokens = bizConfig.getAdminServiceAccessTokens(); // if user forget to configure access tokens, then default to pass if (Strings.isNullOrEmpty(accessTokens)) { return true; } // no need to check if (Strings.isNullOrEmpty(token)) { return false; } // update cache if (!accessTokens.equals(lastAccessTokens)) { synchronized (this) { accessTokenList = ACCESS_TOKEN_SPLITTER.splitToList(accessTokens); lastAccessTokens = accessTokens; } } return accessTokenList.contains(token); } @Override public void destroy() { } } ================================================ FILE: apollo-adminservice/src/main/resources/adminservice.properties ================================================ # # Copyright 2024 Apollo Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # #Used for apollo-assembly spring.application.name= apollo-adminservice server.port= 8090 logging.file.name= /opt/logs/apollo-adminservice.log spring.jmx.default-domain = apollo-adminservice ================================================ FILE: apollo-adminservice/src/main/resources/apollo-adminservice.conf ================================================ MODE=service PID_FOLDER=. # console appender log file folder LOG_FOLDER=/opt/logs/ # console appender log file name LOG_FILENAME=apollo-adminservice.console.log # write application logs only to file appender export LOG_APPENDERS=FILE ================================================ FILE: apollo-adminservice/src/main/resources/application-consul-discovery.properties ================================================ # # Copyright 2024 Apollo Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # eureka.client.enabled=false #consul enabled spring.cloud.consul.enabled=true spring.cloud.consul.discovery.enabled=true spring.cloud.consul.service-registry.enabled=true spring.cloud.consul.discovery.heartbeat.enabled=true spring.cloud.consul.discovery.instance-id=apollo-adminservice ================================================ FILE: apollo-adminservice/src/main/resources/application-custom-defined-discovery.properties ================================================ # # Copyright 2024 Apollo Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # eureka.client.enabled=false spring.cloud.discovery.enabled=false ================================================ FILE: apollo-adminservice/src/main/resources/application-database-discovery.properties ================================================ # # Copyright 2024 Apollo Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # eureka.client.enabled=false spring.cloud.discovery.enabled=false apollo.service.registry.enabled=true apollo.service.registry.cluster=default apollo.service.registry.heartbeat-interval-in-second=10 ================================================ FILE: apollo-adminservice/src/main/resources/application-github.properties ================================================ # # Copyright 2024 Apollo Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # DataSource spring.datasource.url = ${spring_datasource_url} spring.datasource.username = ${spring_datasource_username} spring.datasource.password = ${spring_datasource_password} ================================================ FILE: apollo-adminservice/src/main/resources/application-kubernetes.properties ================================================ # # Copyright 2024 Apollo Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # eureka.client.enabled=false spring.cloud.discovery.enabled=false ================================================ FILE: apollo-adminservice/src/main/resources/application-nacos-discovery.properties ================================================ # # Copyright 2024 Apollo Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # eureka.client.enabled=false spring.cloud.discovery.enabled=false #nacos enabled nacos.discovery.register.enabled=true nacos.discovery.auto-register=true nacos.discovery.register.service-name=apollo-adminservice ================================================ FILE: apollo-adminservice/src/main/resources/application-zookeeper-discovery.properties ================================================ # # Copyright 2024 Apollo Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # eureka.client.enabled=false #zookeeper enabled spring.cloud.zookeeper.enabled=true spring.cloud.zookeeper.discovery.enabled=true spring.cloud.zookeeper.discovery.register=true spring.cloud.zookeeper.discovery.instance-id=${spring.cloud.client.ip-address}:${server.port} ================================================ FILE: apollo-adminservice/src/main/resources/application.properties ================================================ # # Copyright 2024 Apollo Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # You may uncomment the following config to activate different spring profiles #spring.profiles.active=github,consul-discovery #spring.profiles.active=github,zookeeper-discovery #spring.profiles.active=github,custom-defined-discovery #spring.profiles.active=github,database-discovery # You may change the following config to activate different database profiles like h2/postgres spring.profiles.group.github = mysql # true: enabled the new feature of audit log # false/missing: disable it apollo.audit.log.enabled = true ================================================ FILE: apollo-adminservice/src/main/resources/application.yml ================================================ # # Copyright 2024 Apollo Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # spring: application: name: apollo-adminservice profiles: active: ${apollo_profile} cloud: consul: enabled: false zookeeper: enabled: false jpa: properties: hibernate: metadata_builder_contributor: com.ctrip.framework.apollo.common.jpa.SqlFunctionsMetadataBuilderContributor lifecycle: timeout-per-shutdown-phase: ${GRACEFUL_SHUTDOWN_TIMEOUT:10s} server: port: 8090 shutdown: graceful logging: file: name: /opt/logs/apollo-adminservice.log eureka: instance: hostname: ${hostname:localhost} prefer-ip-address: true status-page-url-path: /info health-check-url-path: /health client: service-url: # This setting will be overridden by eureka.service.url setting from ApolloConfigDB.ServerConfig or System Property # see com.ctrip.framework.apollo.biz.eureka.ApolloEurekaClientConfig defaultZone: http://${eureka.instance.hostname}:8080/eureka/ healthcheck: enabled: true eureka-service-url-poll-interval-seconds: 60 management: health: status: order: DOWN, OUT_OF_SERVICE, UNKNOWN, UP ================================================ FILE: apollo-adminservice/src/main/resources/logback.xml ================================================ propertyContains("LOG_APPENDERS", "FILE") && !propertyContains("LOG_APPENDERS", "CONSOLE") propertyContains("LOG_APPENDERS", "CONSOLE") && !propertyContains("LOG_APPENDERS", "FILE") ================================================ FILE: apollo-adminservice/src/main/scripts/shutdown.sh ================================================ #!/bin/bash # # Copyright 2024 Apollo Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # SERVICE_NAME=apollo-adminservice export APP_NAME=$SERVICE_NAME if [[ -z "$JAVA_HOME" && -d /usr/java/latest/ ]]; then export JAVA_HOME=/usr/java/latest/ fi cd `dirname $0`/.. if [[ ! -f $SERVICE_NAME".jar" && -d current ]]; then cd current fi if [[ -f $SERVICE_NAME".jar" ]]; then chmod a+x $SERVICE_NAME".jar" ./$SERVICE_NAME".jar" stop fi ================================================ FILE: apollo-adminservice/src/main/scripts/startup.sh ================================================ #!/bin/bash # # Copyright 2024 Apollo Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # SERVICE_NAME=apollo-adminservice ## Adjust log dir if necessary LOG_DIR=${LOG_DIR:=/opt/logs} ## Adjust server port if necessary SERVER_PORT=${SERVER_PORT:=8090} ## Adjust context path if necessary CONTEXT_PATH=${CONTEXT_PATH:=/} ## Create log directory if not existed because JDK 8+ won't do that mkdir -p $LOG_DIR # Create directory of -XX:HeapDumpPath mkdir -p $LOG_DIR/HeapDumpOnOutOfMemoryError/ ## Adjust memory settings if necessary #export JAVA_OPTS="-Xms2560m -Xmx2560m -Xss256k -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=384m -XX:NewSize=1536m -XX:MaxNewSize=1536m -XX:SurvivorRatio=8" ## Only uncomment the following when you are using server jvm #export JAVA_OPTS="$JAVA_OPTS -server -XX:-ReduceInitialCardMarks" ########### The following is the same for configservice, adminservice, portal ########### export JAVA_OPTS="$JAVA_OPTS -XX:ParallelGCThreads=4 -XX:MaxTenuringThreshold=9 -XX:+DisableExplicitGC -XX:+ScavengeBeforeFullGC -XX:SoftRefLRUPolicyMSPerMB=0 -XX:+ExplicitGCInvokesConcurrent -XX:+HeapDumpOnOutOfMemoryError -XX:-OmitStackTraceInFastThrow -Duser.timezone=Asia/Shanghai -Dclient.encoding.override=UTF-8 -Dfile.encoding=UTF-8 -Djava.security.egd=file:/dev/./urandom" # DS_URL, DS_USERNAME, DS_PASSWORD are deprecated, please use SPRING_DATASOURCE_URL, SPRING_DATASOURCE_USERNAME, SPRING_DATASOURCE_PASSWORD instead # DataSource URL USERNAME PASSWORD if [ "$DS_URL"x != x ] then export SPRING_DATASOURCE_URL=$DS_URL export SPRING_DATASOURCE_USERNAME=$DS_USERNAME export SPRING_DATASOURCE_PASSWORD=$DS_PASSWORD fi export JAVA_OPTS="$JAVA_OPTS -Dserver.port=$SERVER_PORT -Dlogging.file.name=$LOG_DIR/$SERVICE_NAME.log -XX:HeapDumpPath=$LOG_DIR/HeapDumpOnOutOfMemoryError/" export APP_NAME=$SERVICE_NAME PATH_TO_JAR=$SERVICE_NAME".jar" CONTEXT_PATH=$(echo "$CONTEXT_PATH" | sed 's/^\/*//; s/\/*$//') SERVER_URL="http://localhost:${SERVER_PORT}${CONTEXT_PATH:+/$CONTEXT_PATH}" function getPid() { pgrep -f $SERVICE_NAME } function checkPidAlive() { for i in `ls -t $APP_NAME/$APP_NAME.pid 2>/dev/null` do read pid < $i result=$(ps -p "$pid") if [ "$?" -eq 0 ]; then return 0 else printf "\npid - $pid just quit unexpectedly, please check logs under $LOG_DIR and /tmp for more information!\n" exit 1; fi done printf "\nNo pid file found, startup may failed. Please check logs under $LOG_DIR and /tmp for more information!\n" exit 1; } function existProcessUsePort() { if [ "$(curl -X GET --silent --connect-timeout 1 --max-time 2 --head $SERVER_URL | grep "HTTP")" != "" ]; then true else false fi } function isServiceRunning() { if [ "$(curl -X GET --silent --connect-timeout 1 --max-time 2 $SERVER_URL/health | grep "UP")" != "" ]; then true else false fi } if [ "$(uname)" == "Darwin" ]; then windows="0" elif [ "$(expr substr $(uname -s) 1 5)" == "Linux" ]; then windows="0" elif [ "$(expr substr $(uname -s) 1 5)" == "MINGW" ]; then windows="1" else windows="0" fi # for Windows if [ "$windows" == "1" ] && [[ -n "$JAVA_HOME" ]] && [[ -x "$JAVA_HOME/bin/java" ]]; then tmp_java_home=`cygpath -sw "$JAVA_HOME"` export JAVA_HOME=`cygpath -u $tmp_java_home` echo "Windows new JAVA_HOME is: $JAVA_HOME" fi # Find Java if [[ -n "$JAVA_HOME" ]] && [[ -x "$JAVA_HOME/bin/java" ]]; then javaexe="$JAVA_HOME/bin/java" elif type -p java > /dev/null 2>&1; then javaexe=$(type -p java) elif [[ -x "/usr/bin/java" ]]; then javaexe="/usr/bin/java" else echo "Unable to find Java" exit 1 fi if [[ "$javaexe" ]]; then version=$("$javaexe" -version 2>&1 | awk -F '"' '/version/ {print $2}') version=$(echo "$version" | awk -F. '{printf("%03d%03d",$1,$2);}') # now version is of format 009003 (9.3.x) if [ $version -ge 011000 ]; then JAVA_OPTS="$JAVA_OPTS -Xlog:gc*:$LOG_DIR/$SERVICE_NAME.gc.log:time,level,tags -Xlog:safepoint -Xlog:gc+heap=trace" elif [ $version -ge 010000 ]; then JAVA_OPTS="$JAVA_OPTS -Xlog:gc*:$LOG_DIR/$SERVICE_NAME.gc.log:time,level,tags -Xlog:safepoint -Xlog:gc+heap=trace" elif [ $version -ge 009000 ]; then JAVA_OPTS="$JAVA_OPTS -Xlog:gc*:$LOG_DIR/$SERVICE_NAME.gc.log:time,level,tags -Xlog:safepoint -Xlog:gc+heap=trace" else JAVA_OPTS="$JAVA_OPTS -XX:+UseParNewGC" JAVA_OPTS="$JAVA_OPTS -Xloggc:$LOG_DIR/$SERVICE_NAME.gc.log -XX:+PrintGCDetails" JAVA_OPTS="$JAVA_OPTS -XX:+UseConcMarkSweepGC -XX:+UseCMSCompactAtFullCollection -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=60 -XX:+CMSClassUnloadingEnabled -XX:+CMSParallelRemarkEnabled -XX:CMSFullGCsBeforeCompaction=9 -XX:+CMSClassUnloadingEnabled -XX:+PrintGCDateStamps -XX:+PrintGCApplicationConcurrentTime -XX:+PrintHeapAtGC -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=5M" fi fi cd `dirname $0`/.. for i in `ls $SERVICE_NAME-*.jar 2>/dev/null` do if [[ ! $i == *"-sources.jar" ]] then PATH_TO_JAR=$i break fi done if [[ ! -f $PATH_TO_JAR && -d current ]]; then cd current for i in `ls $SERVICE_NAME-*.jar 2>/dev/null` do if [[ ! $i == *"-sources.jar" ]] then PATH_TO_JAR=$i break fi done fi # For Docker environment, start in foreground mode if [[ -n "$APOLLO_RUN_MODE" ]] && [[ "$APOLLO_RUN_MODE" == "Docker" ]]; then exec $javaexe -Dsun.misc.URLClassPath.disableJarChecking=true $JAVA_OPTS -jar $PATH_TO_JAR else # before running check there is another process use port or not if existProcessUsePort; then if isServiceRunning; then echo "$(date) ==== $SERVICE_NAME is running already with port $SERVER_PORT, pid $(getPid)" exit 0 else echo "$(date) ==== $SERVICE_NAME failed to start. The port $SERVER_PORT already be in use by another process" echo "maybe you can figure out which process use port $SERVER_PORT by following ways:" echo "1. access http://change-to-this-machine-ip:$SERVER_PORT by browser" echo "2. run command 'curl $SERVER_URL'" echo "3. run command 'sudo netstat -tunlp | grep :$SERVER_PORT'" echo "4. run command 'sudo lsof -nP -iTCP:$SERVER_PORT -sTCP:LISTEN'" exit 1 fi fi printf "$(date) ==== $SERVICE_NAME Starting ==== \n" if [[ -f $SERVICE_NAME".jar" ]]; then rm -rf $SERVICE_NAME".jar" fi ln $PATH_TO_JAR $SERVICE_NAME".jar" chmod a+x $SERVICE_NAME".jar" ./$SERVICE_NAME".jar" start rc=$?; if [[ $rc != 0 ]]; then echo "$(date) Failed to start $SERVICE_NAME.jar, return code: $rc" exit $rc; fi declare -i counter=0 declare -i max_counter=48 # 48*5=240s declare -i total_time=0 printf "Waiting for server startup" until [[ (( counter -ge max_counter )) ]]; do printf "." sleep 5 counter+=1 total_time=$((counter*5)) checkPidAlive if isServiceRunning; then printf "\n$(date) Server started in $total_time seconds!\n" exit 0; fi done printf "\n$(date) Server failed to start in $total_time seconds!\n" exit 1; fi ================================================ FILE: apollo-adminservice/src/test/java/com/ctrip/framework/apollo/AdminServiceTestConfiguration.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo; import com.ctrip.framework.apollo.adminservice.AdminServiceApplication; import com.ctrip.framework.apollo.common.controller.HttpMessageConverterConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.ComponentScan.Filter; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.FilterType; @Configuration @ComponentScan(excludeFilters = {@Filter(type = FilterType.ASSIGNABLE_TYPE, value = {LocalAdminServiceApplication.class, AdminServiceApplication.class, HttpMessageConverterConfiguration.class})}) @EnableAutoConfiguration public class AdminServiceTestConfiguration { } ================================================ FILE: apollo-adminservice/src/test/java/com/ctrip/framework/apollo/LocalAdminServiceApplication.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer; @SpringBootApplication @EnableEurekaServer public class LocalAdminServiceApplication { public static void main(String[] args) { new SpringApplicationBuilder(LocalAdminServiceApplication.class).run(args); } } ================================================ FILE: apollo-adminservice/src/test/java/com/ctrip/framework/apollo/adminservice/GracefulShutdownConfigurationTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.adminservice; import com.ctrip.framework.apollo.AdminServiceTestConfiguration; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; /** * Configuration validation test for graceful shutdown feature. * * This test verifies that the graceful shutdown configuration is properly loaded * from application.yml by checking ServerProperties and the web server lifecycle. * * Note: This test does NOT verify the actual behavior of graceful shutdown * (e.g., waiting for in-flight requests). Full behavioral testing requires: * - Integration tests with real HTTP requests during shutdown * - Manual testing in staging/production environments * - Monitoring of shutdown metrics and logs */ @RunWith(SpringJUnit4ClassRunner.class) @SpringBootTest(classes = AdminServiceTestConfiguration.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class GracefulShutdownConfigurationTest { @Autowired private ServletWebServerApplicationContext webServerAppContext; @Autowired private ServerProperties serverProperties; @Test public void testGracefulShutdownIsConfigured() { assertNotNull("WebServer should be available", webServerAppContext); assertTrue("Server should be running", webServerAppContext.getWebServer().getPort() > 0); // Verify graceful shutdown is enabled in application.yml assertEquals("Graceful shutdown should be enabled in application.yml", "graceful", serverProperties.getShutdown().name().toLowerCase()); // Verify the lifecycle processor exists (indicates graceful shutdown is enabled) assertNotNull("Lifecycle processor should be present for graceful shutdown", webServerAppContext.getBean("lifecycleProcessor")); } } ================================================ FILE: apollo-adminservice/src/test/java/com/ctrip/framework/apollo/adminservice/aop/NamespaceLockTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.adminservice.aop; import com.ctrip.framework.apollo.biz.config.BizConfig; import com.ctrip.framework.apollo.biz.entity.Namespace; import com.ctrip.framework.apollo.biz.entity.NamespaceLock; import com.ctrip.framework.apollo.biz.service.ItemService; import com.ctrip.framework.apollo.biz.service.NamespaceLockService; import com.ctrip.framework.apollo.biz.service.NamespaceService; import com.ctrip.framework.apollo.common.exception.BadRequestException; import com.ctrip.framework.apollo.common.exception.ServiceException; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import org.springframework.dao.DataIntegrityViolationException; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @RunWith(MockitoJUnitRunner.class) public class NamespaceLockTest { private static final String APP = "app-test"; private static final String CLUSTER = "cluster-test"; private static final String NAMESPACE = "namespace-test"; private static final String CURRENT_USER = "user-test"; private static final String ANOTHER_USER = "user-test2"; private static final long NAMESPACE_ID = 100; @Mock private NamespaceLockService namespaceLockService; @Mock private NamespaceService namespaceService; @Mock private ItemService itemService; @Mock private BizConfig bizConfig; @InjectMocks NamespaceAcquireLockAspect namespaceLockAspect; @Test public void acquireLockWithNotLockedAndSwitchON() { when(bizConfig.isNamespaceLockSwitchOff()).thenReturn(true); namespaceLockAspect.acquireLock(APP, CLUSTER, NAMESPACE, CURRENT_USER); verify(namespaceService, times(0)).findOne(APP, CLUSTER, NAMESPACE); } @Test public void acquireLockWithNotLockedAndSwitchOFF() { when(bizConfig.isNamespaceLockSwitchOff()).thenReturn(false); when(namespaceService.findOne(APP, CLUSTER, NAMESPACE)).thenReturn(mockNamespace()); when(namespaceLockService.findLock(anyLong())).thenReturn(null); namespaceLockAspect.acquireLock(APP, CLUSTER, NAMESPACE, CURRENT_USER); verify(bizConfig).isNamespaceLockSwitchOff(); verify(namespaceService).findOne(APP, CLUSTER, NAMESPACE); verify(namespaceLockService).findLock(anyLong()); verify(namespaceLockService).tryLock(any()); } @Test(expected = BadRequestException.class) public void acquireLockWithAlreadyLockedByOtherGuy() { when(bizConfig.isNamespaceLockSwitchOff()).thenReturn(false); when(namespaceService.findOne(APP, CLUSTER, NAMESPACE)).thenReturn(mockNamespace()); when(namespaceLockService.findLock(NAMESPACE_ID)).thenReturn(mockNamespaceLock(ANOTHER_USER)); namespaceLockAspect.acquireLock(APP, CLUSTER, NAMESPACE, CURRENT_USER); verify(bizConfig).isNamespaceLockSwitchOff(); verify(namespaceService).findOne(APP, CLUSTER, NAMESPACE); verify(namespaceLockService).findLock(NAMESPACE_ID); } @Test public void acquireLockWithAlreadyLockedBySelf() { when(bizConfig.isNamespaceLockSwitchOff()).thenReturn(false); when(namespaceService.findOne(APP, CLUSTER, NAMESPACE)).thenReturn(mockNamespace()); when(namespaceLockService.findLock(NAMESPACE_ID)).thenReturn(mockNamespaceLock(CURRENT_USER)); namespaceLockAspect.acquireLock(APP, CLUSTER, NAMESPACE, CURRENT_USER); verify(bizConfig).isNamespaceLockSwitchOff(); verify(namespaceService).findOne(APP, CLUSTER, NAMESPACE); verify(namespaceLockService).findLock(NAMESPACE_ID); } @Test public void acquireLockWithNamespaceIdSwitchOn(){ when(bizConfig.isNamespaceLockSwitchOff()).thenReturn(false); when(namespaceService.findOne(NAMESPACE_ID)).thenReturn(mockNamespace()); when(namespaceLockService.findLock(NAMESPACE_ID)).thenReturn(null); namespaceLockAspect.acquireLock(NAMESPACE_ID, CURRENT_USER); verify(bizConfig).isNamespaceLockSwitchOff(); verify(namespaceService).findOne(NAMESPACE_ID); verify(namespaceLockService).findLock(NAMESPACE_ID); verify(namespaceLockService).tryLock(any()); } @Test(expected = ServiceException.class) public void testDuplicateLock(){ when(bizConfig.isNamespaceLockSwitchOff()).thenReturn(false); when(namespaceService.findOne(NAMESPACE_ID)).thenReturn(mockNamespace()); when(namespaceLockService.findLock(NAMESPACE_ID)).thenReturn(null); when(namespaceLockService.tryLock(any())).thenThrow(DataIntegrityViolationException.class); namespaceLockAspect.acquireLock(NAMESPACE_ID, CURRENT_USER); verify(bizConfig).isNamespaceLockSwitchOff(); verify(namespaceService).findOne(NAMESPACE_ID); verify(namespaceLockService, times(2)).findLock(NAMESPACE_ID); verify(namespaceLockService).tryLock(any()); } private Namespace mockNamespace() { Namespace namespace = new Namespace(); namespace.setId(NAMESPACE_ID); namespace.setAppId(APP); namespace.setClusterName(CLUSTER); namespace.setNamespaceName(NAMESPACE); return namespace; } private NamespaceLock mockNamespaceLock(String locedUser) { NamespaceLock lock = new NamespaceLock(); lock.setNamespaceId(NAMESPACE_ID); lock.setDataChangeCreatedBy(locedUser); return lock; } } ================================================ FILE: apollo-adminservice/src/test/java/com/ctrip/framework/apollo/adminservice/aop/NamespaceUnlockAspectTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.adminservice.aop; import com.ctrip.framework.apollo.biz.entity.Item; import com.ctrip.framework.apollo.biz.entity.Namespace; import com.ctrip.framework.apollo.biz.entity.Release; import com.ctrip.framework.apollo.biz.service.ItemService; import com.ctrip.framework.apollo.biz.service.NamespaceService; import com.ctrip.framework.apollo.biz.service.ReleaseService; import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import java.util.Arrays; import java.util.Collections; import java.util.List; import static org.mockito.Mockito.when; @RunWith(MockitoJUnitRunner.class) public class NamespaceUnlockAspectTest { @Mock private ReleaseService releaseService; @Mock private ItemService itemService; @Mock private NamespaceService namespaceService; @InjectMocks private NamespaceUnlockAspect namespaceUnlockAspect; @Test public void testNamespaceHasNoNormalItemsAndRelease() { long namespaceId = 1; Namespace namespace = createNamespace(namespaceId); when(releaseService.findLatestActiveRelease(namespace)).thenReturn(null); when(itemService.findItemsWithoutOrdered(namespaceId)) .thenReturn(Collections.singletonList(createItem("", ""))); boolean isModified = namespaceUnlockAspect.isModified(namespace); Assert.assertFalse(isModified); } @Test public void testNamespaceAddItem() { long namespaceId = 1; Namespace namespace = createNamespace(namespaceId); Release release = createRelease("{\"k1\":\"v1\"}"); List items = Arrays.asList(createItem("k1", "v1"), createItem("k2", "v2")); when(releaseService.findLatestActiveRelease(namespace)).thenReturn(release); when(itemService.findItemsWithoutOrdered(namespaceId)).thenReturn(items); when(namespaceService.findParentNamespace(namespace)).thenReturn(null); boolean isModified = namespaceUnlockAspect.isModified(namespace); Assert.assertTrue(isModified); } @Test public void testNamespaceModifyItem() { long namespaceId = 1; Namespace namespace = createNamespace(namespaceId); Release release = createRelease("{\"k1\":\"v1\"}"); List items = Collections.singletonList(createItem("k1", "v2")); when(releaseService.findLatestActiveRelease(namespace)).thenReturn(release); when(itemService.findItemsWithoutOrdered(namespaceId)).thenReturn(items); when(namespaceService.findParentNamespace(namespace)).thenReturn(null); boolean isModified = namespaceUnlockAspect.isModified(namespace); Assert.assertTrue(isModified); } @Test public void testNamespaceDeleteItem() { long namespaceId = 1; Namespace namespace = createNamespace(namespaceId); Release release = createRelease("{\"k1\":\"v1\"}"); List items = Collections.singletonList(createItem("k2", "v2")); when(releaseService.findLatestActiveRelease(namespace)).thenReturn(release); when(itemService.findItemsWithoutOrdered(namespaceId)).thenReturn(items); when(namespaceService.findParentNamespace(namespace)).thenReturn(null); boolean isModified = namespaceUnlockAspect.isModified(namespace); Assert.assertTrue(isModified); } @Test public void testChildNamespaceModified() { long childNamespaceId = 1, parentNamespaceId = 2; Namespace childNamespace = createNamespace(childNamespaceId); Namespace parentNamespace = createNamespace(parentNamespaceId); Release childRelease = createRelease("{\"k1\":\"v1\", \"k2\":\"v2\"}"); List childItems = Collections.singletonList(createItem("k1", "v3")); Release parentRelease = createRelease("{\"k1\":\"v1\", \"k2\":\"v2\"}"); when(releaseService.findLatestActiveRelease(childNamespace)).thenReturn(childRelease); when(releaseService.findLatestActiveRelease(parentNamespace)).thenReturn(parentRelease); when(itemService.findItemsWithoutOrdered(childNamespaceId)).thenReturn(childItems); when(namespaceService.findParentNamespace(childNamespace)).thenReturn(parentNamespace); boolean isModified = namespaceUnlockAspect.isModified(childNamespace); Assert.assertTrue(isModified); } @Test public void testChildNamespaceNotModified() { long childNamespaceId = 1, parentNamespaceId = 2; Namespace childNamespace = createNamespace(childNamespaceId); Namespace parentNamespace = createNamespace(parentNamespaceId); Release childRelease = createRelease("{\"k1\":\"v3\", \"k2\":\"v2\"}"); List childItems = Collections.singletonList(createItem("k1", "v3")); Release parentRelease = createRelease("{\"k1\":\"v1\", \"k2\":\"v2\"}"); when(releaseService.findLatestActiveRelease(childNamespace)).thenReturn(childRelease); when(releaseService.findLatestActiveRelease(parentNamespace)).thenReturn(parentRelease); when(itemService.findItemsWithoutOrdered(childNamespaceId)).thenReturn(childItems); when(namespaceService.findParentNamespace(childNamespace)).thenReturn(parentNamespace); boolean isModified = namespaceUnlockAspect.isModified(childNamespace); Assert.assertFalse(isModified); } @Test public void testParentNamespaceNotReleased() { long childNamespaceId = 1, parentNamespaceId = 2; Namespace childNamespace = createNamespace(childNamespaceId); Namespace parentNamespace = createNamespace(parentNamespaceId); Release childRelease = createRelease("{\"k1\":\"v3\", \"k2\":\"v2\"}"); List childItems = Arrays.asList(createItem("k1", "v2"), createItem("k2", "v2")); when(releaseService.findLatestActiveRelease(childNamespace)).thenReturn(childRelease); when(releaseService.findLatestActiveRelease(parentNamespace)).thenReturn(null); when(itemService.findItemsWithoutOrdered(childNamespaceId)).thenReturn(childItems); when(namespaceService.findParentNamespace(childNamespace)).thenReturn(parentNamespace); boolean isModified = namespaceUnlockAspect.isModified(childNamespace); Assert.assertTrue(isModified); } private Namespace createNamespace(long namespaceId) { Namespace namespace = new Namespace(); namespace.setId(namespaceId); return namespace; } private Item createItem(String key, String value) { Item item = new Item(); item.setKey(key); item.setValue(value); return item; } private Release createRelease(String configuration) { Release release = new Release(); release.setConfigurations(configuration); return release; } } ================================================ FILE: apollo-adminservice/src/test/java/com/ctrip/framework/apollo/adminservice/controller/AbstractControllerTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.adminservice.controller; import com.ctrip.framework.apollo.AdminServiceTestConfiguration; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.http.HttpMessageConverters; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.boot.test.web.client.TestRestTemplate; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.web.client.DefaultResponseErrorHandler; import org.springframework.web.client.RestTemplate; import jakarta.annotation.PostConstruct; @RunWith(SpringJUnit4ClassRunner.class) @SpringBootTest(classes = AdminServiceTestConfiguration.class, webEnvironment = WebEnvironment.RANDOM_PORT) public abstract class AbstractControllerTest { @Autowired private HttpMessageConverters httpMessageConverters; protected RestTemplate restTemplate = (new TestRestTemplate()).getRestTemplate(); @PostConstruct private void postConstruct() { restTemplate.setErrorHandler(new DefaultResponseErrorHandler()); restTemplate.setMessageConverters(httpMessageConverters.getConverters()); } @Value("${local.server.port}") protected int port; protected String url(String path) { return "http://localhost:" + port + path; } protected String namespaceBaseUrl() { return url("/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName:.+}"); } protected String appBaseUrl() { return url("/apps/{appId}"); } protected String clusterBaseUrl() { return url("/apps/{appId}/clusters/{clusterName}"); } protected String itemBaseUrl() { return url("/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/items"); } } ================================================ FILE: apollo-adminservice/src/test/java/com/ctrip/framework/apollo/adminservice/controller/AppControllerTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.adminservice.controller; import com.ctrip.framework.apollo.biz.repository.AppRepository; import com.ctrip.framework.apollo.common.dto.AppDTO; import com.ctrip.framework.apollo.common.entity.App; import com.ctrip.framework.apollo.common.utils.BeanUtils; import com.ctrip.framework.apollo.common.utils.InputValidator; import org.junit.Assert; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.test.context.jdbc.Sql; import org.springframework.test.context.jdbc.Sql.ExecutionPhase; import org.springframework.web.client.HttpClientErrorException; import static org.hamcrest.Matchers.containsString; public class AppControllerTest extends AbstractControllerTest { @Autowired AppRepository appRepository; private String getBaseAppUrl() { return url("/apps"); } @Test @Sql(scripts = "/controller/cleanup.sql", executionPhase = ExecutionPhase.AFTER_TEST_METHOD) public void testCheckIfAppIdUnique() { AppDTO dto = generateSampleDTOData(); ResponseEntity response = restTemplate.postForEntity(getBaseAppUrl(), dto, AppDTO.class); AppDTO result = response.getBody(); Assert.assertEquals(HttpStatus.OK, response.getStatusCode()); Assert.assertEquals(dto.getAppId(), result.getAppId()); Assert.assertTrue(result.getId() > 0); Boolean falseUnique = restTemplate .getForObject(getBaseAppUrl() + "/" + dto.getAppId() + "/unique", Boolean.class); Assert.assertFalse(falseUnique); Boolean trueUnique = restTemplate .getForObject(getBaseAppUrl() + "/" + dto.getAppId() + "true" + "/unique", Boolean.class); Assert.assertTrue(trueUnique); } @Test @Sql(scripts = "/controller/cleanup.sql", executionPhase = ExecutionPhase.AFTER_TEST_METHOD) public void testCreate() { AppDTO dto = generateSampleDTOData(); ResponseEntity response = restTemplate.postForEntity(getBaseAppUrl(), dto, AppDTO.class); AppDTO result = response.getBody(); Assert.assertEquals(HttpStatus.OK, response.getStatusCode()); Assert.assertEquals(dto.getAppId(), result.getAppId()); Assert.assertTrue(result.getId() > 0); App savedApp = appRepository.findById(result.getId()).orElse(null); Assert.assertEquals(dto.getAppId(), savedApp.getAppId()); Assert.assertNotNull(savedApp.getDataChangeCreatedTime()); } @Test @Sql(scripts = "/controller/cleanup.sql", executionPhase = ExecutionPhase.AFTER_TEST_METHOD) public void testCreateTwice() { AppDTO dto = generateSampleDTOData(); ResponseEntity response = restTemplate.postForEntity(getBaseAppUrl(), dto, AppDTO.class); AppDTO first = response.getBody(); Assert.assertEquals(HttpStatus.OK, response.getStatusCode()); Assert.assertEquals(dto.getAppId(), first.getAppId()); Assert.assertTrue(first.getId() > 0); App savedApp = appRepository.findById(first.getId()).orElse(null); Assert.assertEquals(dto.getAppId(), savedApp.getAppId()); Assert.assertNotNull(savedApp.getDataChangeCreatedTime()); try { restTemplate.postForEntity(getBaseAppUrl(), dto, AppDTO.class); } catch (HttpClientErrorException e) { Assert.assertEquals(HttpStatus.BAD_REQUEST, e.getStatusCode()); } } @Test @Sql(scripts = "/controller/cleanup.sql", executionPhase = ExecutionPhase.AFTER_TEST_METHOD) public void testFind() { AppDTO dto = generateSampleDTOData(); App app = BeanUtils.transform(App.class, dto); app = appRepository.save(app); AppDTO result = restTemplate.getForObject(getBaseAppUrl() + "/" + dto.getAppId(), AppDTO.class); Assert.assertEquals(dto.getAppId(), result.getAppId()); Assert.assertEquals(dto.getName(), result.getName()); } @Test(expected = HttpClientErrorException.class) @Sql(scripts = "/controller/cleanup.sql", executionPhase = ExecutionPhase.AFTER_TEST_METHOD) public void testFindNotExist() { restTemplate.getForEntity(getBaseAppUrl() + "/notExists", AppDTO.class); } @Test @Sql(scripts = "/controller/cleanup.sql", executionPhase = ExecutionPhase.AFTER_TEST_METHOD) public void testDelete() { AppDTO dto = generateSampleDTOData(); App app = BeanUtils.transform(App.class, dto); app = appRepository.save(app); restTemplate.delete(url("/apps/{appId}?operator={operator}"), app.getAppId(), "test"); App deletedApp = appRepository.findById(app.getId()).orElse(null); Assert.assertNull(deletedApp); } @Test @Sql(scripts = "/controller/cleanup.sql", executionPhase = ExecutionPhase.AFTER_TEST_METHOD) public void shouldFailedWhenAppIdIsInvalid() { AppDTO dto = generateSampleDTOData(); dto.setAppId("invalid app id"); try { restTemplate.postForEntity(getBaseAppUrl(), dto, String.class); Assert.fail("Should throw"); } catch (HttpClientErrorException e) { Assert.assertEquals(HttpStatus.BAD_REQUEST, e.getStatusCode()); Assert.assertThat(new String(e.getResponseBodyAsByteArray()), containsString(InputValidator.INVALID_CLUSTER_NAMESPACE_MESSAGE)); } } private AppDTO generateSampleDTOData() { AppDTO dto = new AppDTO(); dto.setAppId("someAppId"); dto.setName("someName"); dto.setOwnerName("someOwner"); dto.setOwnerEmail("someOwner@ctrip.com"); dto.setDataChangeCreatedBy("apollo"); dto.setDataChangeLastModifiedBy("apollo"); return dto; } } ================================================ FILE: apollo-adminservice/src/test/java/com/ctrip/framework/apollo/adminservice/controller/AppNamespaceControllerTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.adminservice.controller; import com.ctrip.framework.apollo.biz.repository.AppNamespaceRepository; import com.ctrip.framework.apollo.common.dto.AppNamespaceDTO; import com.ctrip.framework.apollo.common.entity.AppNamespace; import org.junit.Assert; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.jdbc.Sql; public class AppNamespaceControllerTest extends AbstractControllerTest { @Autowired private AppNamespaceRepository namespaceRepository; @Test @Sql(scripts = "/controller/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testCreate() { String appId = "6666"; String name = "testnamespace"; String comment = "comment"; AppNamespaceDTO dto = new AppNamespaceDTO(); dto.setAppId(appId); dto.setName(name); dto.setComment(comment); dto.setDataChangeCreatedBy("apollo"); AppNamespaceDTO resultDto = restTemplate .postForEntity(url("/apps/{appId}/appnamespaces"), dto, AppNamespaceDTO.class, appId) .getBody(); Assert.assertNotNull(resultDto); Assert.assertEquals(appId, resultDto.getAppId()); Assert.assertTrue(resultDto.getId() > 0); AppNamespace savedAppNs = namespaceRepository.findByAppIdAndName(appId, name); Assert.assertNotNull(savedAppNs); Assert.assertNotNull(savedAppNs.getDataChangeCreatedTime()); Assert.assertNotNull(savedAppNs.getDataChangeLastModifiedTime()); Assert.assertNotNull(savedAppNs.getDataChangeLastModifiedBy()); Assert.assertNotNull(savedAppNs.getDataChangeCreatedBy()); } } ================================================ FILE: apollo-adminservice/src/test/java/com/ctrip/framework/apollo/adminservice/controller/ClusterControllerTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.adminservice.controller; import com.ctrip.framework.apollo.biz.entity.Cluster; import com.ctrip.framework.apollo.biz.service.ClusterService; import com.ctrip.framework.apollo.common.dto.ClusterDTO; import com.ctrip.framework.apollo.common.exception.BadRequestException; import com.ctrip.framework.apollo.common.utils.InputValidator; import com.ctrip.framework.apollo.core.ConfigConsts; import org.junit.Assert; import org.junit.Test; import org.mockito.InjectMocks; import org.mockito.Mock; import org.springframework.http.ResponseEntity; import org.springframework.web.client.HttpClientErrorException; import static org.hamcrest.Matchers.containsString; import static org.mockito.Mockito.*; public class ClusterControllerTest extends AbstractControllerTest { @InjectMocks private ClusterController clusterController; @Mock private ClusterService clusterService; @Test(expected = BadRequestException.class) public void testDeleteDefaultFail() { Cluster cluster = new Cluster(); cluster.setName(ConfigConsts.CLUSTER_NAME_DEFAULT); when(clusterService.findOne(any(String.class), any(String.class))).thenReturn(cluster); clusterController.delete("1", "2", "d"); } @Test public void testDeleteSuccess() { Cluster cluster = new Cluster(); when(clusterService.findOne(any(String.class), any(String.class))).thenReturn(cluster); clusterController.delete("1", "2", "d"); verify(clusterService, times(1)).findOne("1", "2"); } @Test public void shouldFailWhenRequestBodyInvalid() { ClusterDTO cluster = new ClusterDTO(); cluster.setAppId("valid"); cluster.setName("notBlank"); ResponseEntity response = restTemplate.postForEntity(url("/apps/{appId}/clusters"), cluster, ClusterDTO.class, cluster.getAppId()); ClusterDTO createdCluster = response.getBody(); Assert.assertNotNull(createdCluster); Assert.assertEquals(cluster.getAppId(), createdCluster.getAppId()); Assert.assertEquals(cluster.getName(), createdCluster.getName()); cluster.setName("invalid app name"); try { restTemplate.postForEntity(url("/apps/{appId}/clusters"), cluster, ClusterDTO.class, cluster.getAppId()); Assert.fail("Should throw"); } catch (HttpClientErrorException e) { Assert.assertThat(new String(e.getResponseBodyAsByteArray()), containsString(InputValidator.INVALID_CLUSTER_NAMESPACE_MESSAGE)); } } } ================================================ FILE: apollo-adminservice/src/test/java/com/ctrip/framework/apollo/adminservice/controller/ControllerExceptionTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.adminservice.controller; import com.ctrip.framework.apollo.biz.service.AdminService; import com.ctrip.framework.apollo.biz.service.AppService; import com.ctrip.framework.apollo.common.dto.AppDTO; import com.ctrip.framework.apollo.common.entity.App; import com.ctrip.framework.apollo.common.exception.NotFoundException; import com.ctrip.framework.apollo.common.exception.ServiceException; import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import java.util.ArrayList; import java.util.List; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; @RunWith(MockitoJUnitRunner.class) public class ControllerExceptionTest { @InjectMocks private AppController appController; @Mock private AppService appService; @Mock private AdminService adminService; @Test(expected = NotFoundException.class) public void testFindNotExists() { when(appService.findOne(any(String.class))).thenReturn(null); appController.get("unexist"); } @Test(expected = NotFoundException.class) public void testDeleteNotExists() { when(appService.findOne(any(String.class))).thenReturn(null); appController.delete("unexist", null); } @Test public void testFindEmpty() { when(appService.findAll(any(Pageable.class))).thenReturn(new ArrayList<>()); Pageable pageable = PageRequest.of(0, 10); List appDTOs = appController.find(null, pageable); Assert.assertNotNull(appDTOs); Assert.assertEquals(0, appDTOs.size()); appDTOs = appController.find("", pageable); Assert.assertNotNull(appDTOs); Assert.assertEquals(0, appDTOs.size()); } @Test public void testFindByName() { Pageable pageable = PageRequest.of(0, 10); List appDTOs = appController.find("unexist", pageable); Assert.assertNotNull(appDTOs); Assert.assertEquals(0, appDTOs.size()); } @Test(expected = ServiceException.class) public void createFailed() { AppDTO dto = generateSampleDTOData(); when(appService.findOne(any(String.class))).thenReturn(null); when(adminService.createNewApp(any(App.class))) .thenThrow(new ServiceException("create app failed")); appController.create(dto); } private AppDTO generateSampleDTOData() { AppDTO dto = new AppDTO(); dto.setAppId("someAppId"); dto.setName("someName"); dto.setOwnerName("someOwner"); dto.setOwnerEmail("someOwner@ctrip.com"); dto.setDataChangeLastModifiedBy("test"); dto.setDataChangeCreatedBy("test"); return dto; } } ================================================ FILE: apollo-adminservice/src/test/java/com/ctrip/framework/apollo/adminservice/controller/ControllerIntegrationExceptionTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.adminservice.controller; import com.google.gson.Gson; import com.ctrip.framework.apollo.biz.service.AdminService; import com.ctrip.framework.apollo.biz.service.AppService; import com.ctrip.framework.apollo.common.dto.AppDTO; import com.ctrip.framework.apollo.common.entity.App; import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.mockito.Mock; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.jdbc.Sql; import org.springframework.test.context.jdbc.Sql.ExecutionPhase; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.web.client.HttpStatusCodeException; import java.util.Map; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; public class ControllerIntegrationExceptionTest extends AbstractControllerTest { @Autowired AppController appController; @Mock AdminService adminService; private Object realAdminService; @Autowired AppService appService; private static final Gson GSON = new Gson(); @Before public void setUp() { realAdminService = ReflectionTestUtils.getField(appController, "adminService"); ReflectionTestUtils.setField(appController, "adminService", adminService); } @After public void tearDown() throws Exception { ReflectionTestUtils.setField(appController, "adminService", realAdminService); } @Test @Sql(scripts = "/controller/cleanup.sql", executionPhase = ExecutionPhase.AFTER_TEST_METHOD) public void testCreateFailed() { AppDTO dto = generateSampleDTOData(); when(adminService.createNewApp(any(App.class))).thenThrow(new RuntimeException("save failed")); try { restTemplate.postForEntity(url("/apps"), dto, AppDTO.class); } catch (HttpStatusCodeException e) { @SuppressWarnings("unchecked") Map attr = GSON.fromJson(e.getResponseBodyAsString(), Map.class); Assert.assertEquals("save failed", attr.get("message")); } App savedApp = appService.findOne(dto.getAppId()); Assert.assertNull(savedApp); } private AppDTO generateSampleDTOData() { AppDTO dto = new AppDTO(); dto.setAppId("someAppId"); dto.setName("someName"); dto.setOwnerName("someOwner"); dto.setOwnerEmail("someOwner@ctrip.com"); return dto; } } ================================================ FILE: apollo-adminservice/src/test/java/com/ctrip/framework/apollo/adminservice/controller/InstanceConfigControllerTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.adminservice.controller; import com.ctrip.framework.apollo.biz.entity.Instance; import com.ctrip.framework.apollo.biz.entity.InstanceConfig; import com.ctrip.framework.apollo.biz.entity.Release; import com.ctrip.framework.apollo.biz.service.InstanceService; import com.ctrip.framework.apollo.biz.service.ReleaseService; import com.ctrip.framework.apollo.common.dto.InstanceDTO; import com.ctrip.framework.apollo.common.dto.PageDTO; import com.ctrip.framework.apollo.common.exception.NotFoundException; import com.google.common.base.Joiner; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Set; import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; /** * @author Jason Song(song_s@ctrip.com) */ @RunWith(MockitoJUnitRunner.class) public class InstanceConfigControllerTest { private InstanceConfigController instanceConfigController; @Mock private ReleaseService releaseService; @Mock private InstanceService instanceService; private Pageable pageable; @Before public void setUp() throws Exception { instanceConfigController = new InstanceConfigController(releaseService, instanceService); pageable = PageRequest.of(0, 2); } @Test public void getByRelease() throws Exception { long someReleaseId = 1; long someInstanceId = 1; long anotherInstanceId = 2; String someReleaseKey = "someKey"; Release someRelease = new Release(); someRelease.setReleaseKey(someReleaseKey); String someAppId = "someAppId"; String anotherAppId = "anotherAppId"; String someCluster = "someCluster"; String someDataCenter = "someDC"; String someConfigAppId = "someConfigAppId"; String someConfigNamespace = "someNamespace"; String someIp = "someIp"; Date someReleaseDeliveryTime = new Date(); Date anotherReleaseDeliveryTime = new Date(); when(releaseService.findOne(someReleaseId)).thenReturn(someRelease); InstanceConfig someInstanceConfig = assembleInstanceConfig(someInstanceId, someConfigAppId, someConfigNamespace, someReleaseKey, someReleaseDeliveryTime); InstanceConfig anotherInstanceConfig = assembleInstanceConfig(anotherInstanceId, someConfigAppId, someConfigNamespace, someReleaseKey, anotherReleaseDeliveryTime); List instanceConfigs = Lists.newArrayList(someInstanceConfig, anotherInstanceConfig); Page instanceConfigPage = new PageImpl<>(instanceConfigs, pageable, instanceConfigs.size()); when(instanceService.findActiveInstanceConfigsByReleaseKey(someReleaseKey, pageable)) .thenReturn(instanceConfigPage); Instance someInstance = assembleInstance(someInstanceId, someAppId, someCluster, someDataCenter, someIp); Instance anotherInstance = assembleInstance(anotherInstanceId, anotherAppId, someCluster, someDataCenter, someIp); List instances = Lists.newArrayList(someInstance, anotherInstance); Set instanceIds = Sets.newHashSet(someInstanceId, anotherInstanceId); when(instanceService.findInstancesByIds(instanceIds)).thenReturn(instances); PageDTO result = instanceConfigController.getByRelease(someReleaseId, pageable); assertEquals(2, result.getContent().size()); InstanceDTO someInstanceDto = null; InstanceDTO anotherInstanceDto = null; for (InstanceDTO instanceDTO : result.getContent()) { if (instanceDTO.getId() == someInstanceId) { someInstanceDto = instanceDTO; } else if (instanceDTO.getId() == anotherInstanceId) { anotherInstanceDto = instanceDTO; } } verifyInstance(someInstance, someInstanceDto); verifyInstance(anotherInstance, anotherInstanceDto); assertEquals(1, someInstanceDto.getConfigs().size()); assertEquals(someReleaseDeliveryTime, someInstanceDto.getConfigs().get(0).getReleaseDeliveryTime()); assertEquals(1, anotherInstanceDto.getConfigs().size()); assertEquals(anotherReleaseDeliveryTime, anotherInstanceDto.getConfigs().get(0).getReleaseDeliveryTime()); } @Test(expected = NotFoundException.class) public void testGetByReleaseWhenReleaseIsNotFound() throws Exception { long someReleaseIdNotExists = 1; when(releaseService.findOne(someReleaseIdNotExists)).thenReturn(null); instanceConfigController.getByRelease(someReleaseIdNotExists, pageable); } @Test public void testGetByReleasesNotIn() throws Exception { String someConfigAppId = "someConfigAppId"; String someConfigClusterName = "someConfigClusterName"; String someConfigNamespaceName = "someConfigNamespaceName"; long someReleaseId = 1; long anotherReleaseId = 2; String releaseIds = Joiner.on(",").join(someReleaseId, anotherReleaseId); Date someReleaseDeliveryTime = new Date(); Date anotherReleaseDeliveryTime = new Date(); Release someRelease = mock(Release.class); Release anotherRelease = mock(Release.class); String someReleaseKey = "someReleaseKey"; String anotherReleaseKey = "anotherReleaseKey"; when(someRelease.getReleaseKey()).thenReturn(someReleaseKey); when(anotherRelease.getReleaseKey()).thenReturn(anotherReleaseKey); when(releaseService.findByReleaseIds(Sets.newHashSet(someReleaseId, anotherReleaseId))) .thenReturn(Lists.newArrayList(someRelease, anotherRelease)); long someInstanceId = 1; long anotherInstanceId = 2; String someInstanceConfigReleaseKey = "someInstanceConfigReleaseKey"; String anotherInstanceConfigReleaseKey = "anotherInstanceConfigReleaseKey"; InstanceConfig someInstanceConfig = mock(InstanceConfig.class); InstanceConfig anotherInstanceConfig = mock(InstanceConfig.class); when(someInstanceConfig.getInstanceId()).thenReturn(someInstanceId); when(anotherInstanceConfig.getInstanceId()).thenReturn(anotherInstanceId); when(someInstanceConfig.getReleaseKey()).thenReturn(someInstanceConfigReleaseKey); when(anotherInstanceConfig.getReleaseKey()).thenReturn(anotherInstanceConfigReleaseKey); when(someInstanceConfig.getReleaseDeliveryTime()).thenReturn(someReleaseDeliveryTime); when(anotherInstanceConfig.getReleaseDeliveryTime()).thenReturn(anotherReleaseDeliveryTime); when(instanceService.findInstanceConfigsByNamespaceWithReleaseKeysNotIn(someConfigAppId, someConfigClusterName, someConfigNamespaceName, Sets.newHashSet(someReleaseKey, anotherReleaseKey))) .thenReturn(Lists.newArrayList(someInstanceConfig, anotherInstanceConfig)); String someInstanceAppId = "someInstanceAppId"; String someInstanceClusterName = "someInstanceClusterName"; String someInstanceNamespaceName = "someInstanceNamespaceName"; String someIp = "someIp"; String anotherIp = "anotherIp"; Instance someInstance = assembleInstance(someInstanceId, someInstanceAppId, someInstanceClusterName, someInstanceNamespaceName, someIp); Instance anotherInstance = assembleInstance(anotherInstanceId, someInstanceAppId, someInstanceClusterName, someInstanceNamespaceName, anotherIp); when(instanceService.findInstancesByIds(Sets.newHashSet(someInstanceId, anotherInstanceId))) .thenReturn(Lists.newArrayList(someInstance, anotherInstance)); Release someInstanceConfigRelease = new Release(); someInstanceConfigRelease.setReleaseKey(someInstanceConfigReleaseKey); Release anotherInstanceConfigRelease = new Release(); anotherInstanceConfigRelease.setReleaseKey(anotherInstanceConfigReleaseKey); when(releaseService.findByReleaseKeys( Sets.newHashSet(someInstanceConfigReleaseKey, anotherInstanceConfigReleaseKey))) .thenReturn(Lists.newArrayList(someInstanceConfigRelease, anotherInstanceConfigRelease)); List result = instanceConfigController.getByReleasesNotIn(someConfigAppId, someConfigClusterName, someConfigNamespaceName, releaseIds); assertEquals(2, result.size()); InstanceDTO someInstanceDto = null; InstanceDTO anotherInstanceDto = null; for (InstanceDTO instanceDTO : result) { if (instanceDTO.getId() == someInstanceId) { someInstanceDto = instanceDTO; } else if (instanceDTO.getId() == anotherInstanceId) { anotherInstanceDto = instanceDTO; } } verifyInstance(someInstance, someInstanceDto); verifyInstance(anotherInstance, anotherInstanceDto); assertEquals(someInstanceConfigReleaseKey, someInstanceDto.getConfigs().get(0).getRelease().getReleaseKey()); assertEquals(anotherInstanceConfigReleaseKey, anotherInstanceDto.getConfigs().get(0).getRelease().getReleaseKey()); assertEquals(someReleaseDeliveryTime, someInstanceDto.getConfigs().get(0).getReleaseDeliveryTime()); assertEquals(anotherReleaseDeliveryTime, anotherInstanceDto.getConfigs().get(0).getReleaseDeliveryTime()); } @Test public void testGetInstancesByNamespace() throws Exception { String someAppId = "someAppId"; String someClusterName = "someClusterName"; String someNamespaceName = "someNamespaceName"; String someIp = "someIp"; long someInstanceId = 1; long anotherInstanceId = 2; Instance someInstance = assembleInstance(someInstanceId, someAppId, someClusterName, someNamespaceName, someIp); Instance anotherInstance = assembleInstance(anotherInstanceId, someAppId, someClusterName, someNamespaceName, someIp); Page instances = new PageImpl<>(Lists.newArrayList(someInstance, anotherInstance), pageable, 2); when(instanceService.findInstancesByNamespace(someAppId, someClusterName, someNamespaceName, pageable)).thenReturn(instances); PageDTO result = instanceConfigController.getInstancesByNamespace(someAppId, someClusterName, someNamespaceName, null, pageable); assertEquals(2, result.getContent().size()); InstanceDTO someInstanceDto = null; InstanceDTO anotherInstanceDto = null; for (InstanceDTO instanceDTO : result.getContent()) { if (instanceDTO.getId() == someInstanceId) { someInstanceDto = instanceDTO; } else if (instanceDTO.getId() == anotherInstanceId) { anotherInstanceDto = instanceDTO; } } verifyInstance(someInstance, someInstanceDto); verifyInstance(anotherInstance, anotherInstanceDto); } @Test public void testGetInstancesByNamespaceAndInstanceAppId() throws Exception { String someInstanceAppId = "someInstanceAppId"; String someAppId = "someAppId"; String someClusterName = "someClusterName"; String someNamespaceName = "someNamespaceName"; String someIp = "someIp"; long someInstanceId = 1; long anotherInstanceId = 2; Instance someInstance = assembleInstance(someInstanceId, someAppId, someClusterName, someNamespaceName, someIp); Instance anotherInstance = assembleInstance(anotherInstanceId, someAppId, someClusterName, someNamespaceName, someIp); Page instances = new PageImpl<>(Lists.newArrayList(someInstance, anotherInstance), pageable, 2); when(instanceService.findInstancesByNamespaceAndInstanceAppId(someInstanceAppId, someAppId, someClusterName, someNamespaceName, pageable)).thenReturn(instances); PageDTO result = instanceConfigController.getInstancesByNamespace(someAppId, someClusterName, someNamespaceName, someInstanceAppId, pageable); assertEquals(2, result.getContent().size()); InstanceDTO someInstanceDto = null; InstanceDTO anotherInstanceDto = null; for (InstanceDTO instanceDTO : result.getContent()) { if (instanceDTO.getId() == someInstanceId) { someInstanceDto = instanceDTO; } else if (instanceDTO.getId() == anotherInstanceId) { anotherInstanceDto = instanceDTO; } } verifyInstance(someInstance, someInstanceDto); verifyInstance(anotherInstance, anotherInstanceDto); } @Test public void testGetInstancesCountByNamespace() throws Exception { String someAppId = "someAppId"; String someClusterName = "someClusterName"; String someNamespaceName = "someNamespaceName"; Page instances = new PageImpl<>(Collections.emptyList(), pageable, 2); when(instanceService.findInstancesByNamespace(eq(someAppId), eq(someClusterName), eq(someNamespaceName), any(Pageable.class))).thenReturn(instances); long result = instanceConfigController.getInstancesCountByNamespace(someAppId, someClusterName, someNamespaceName); assertEquals(2, result); } private void verifyInstance(Instance instance, InstanceDTO instanceDTO) { assertEquals(instance.getId(), instanceDTO.getId()); assertEquals(instance.getAppId(), instanceDTO.getAppId()); assertEquals(instance.getClusterName(), instanceDTO.getClusterName()); assertEquals(instance.getDataCenter(), instanceDTO.getDataCenter()); assertEquals(instance.getIp(), instanceDTO.getIp()); assertEquals(instance.getDataChangeCreatedTime(), instanceDTO.getDataChangeCreatedTime()); } private Instance assembleInstance(long instanceId, String appId, String clusterName, String dataCenter, String ip) { Instance instance = new Instance(); instance.setId(instanceId); instance.setAppId(appId); instance.setIp(ip); instance.setClusterName(clusterName); instance.setDataCenter(dataCenter); instance.setDataChangeCreatedTime(new Date()); return instance; } private InstanceConfig assembleInstanceConfig(long instanceId, String configAppId, String configNamespaceName, String releaseKey, Date releaseDeliveryTime) { InstanceConfig instanceConfig = new InstanceConfig(); instanceConfig.setInstanceId(instanceId); instanceConfig.setConfigAppId(configAppId); instanceConfig.setConfigNamespaceName(configNamespaceName); instanceConfig.setReleaseKey(releaseKey); instanceConfig.setDataChangeLastModifiedTime(new Date()); instanceConfig.setReleaseDeliveryTime(releaseDeliveryTime); return instanceConfig; } } ================================================ FILE: apollo-adminservice/src/test/java/com/ctrip/framework/apollo/adminservice/controller/ItemControllerTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.adminservice.controller; import static org.assertj.core.api.Assertions.assertThat; import com.ctrip.framework.apollo.biz.entity.Commit; import com.ctrip.framework.apollo.biz.repository.CommitRepository; import com.ctrip.framework.apollo.biz.repository.ItemRepository; import com.ctrip.framework.apollo.biz.service.ItemService; import com.ctrip.framework.apollo.common.dto.*; import java.util.List; import java.util.Objects; import org.junit.Assert; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.ParameterizedTypeReference; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.http.*; import org.springframework.test.context.jdbc.Sql; import org.springframework.test.context.jdbc.Sql.ExecutionPhase; /** * @author kl (http://kailing.pub) * @since 2023/3/21 */ public class ItemControllerTest extends AbstractControllerTest { @Autowired private CommitRepository commitRepository; @Autowired private ItemRepository itemRepository; @Autowired private ItemService itemService; @Test @Sql(scripts = "/controller/test-itemset.sql", executionPhase = ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/controller/cleanup.sql", executionPhase = ExecutionPhase.AFTER_TEST_METHOD) public void testCreate() { String appId = "someAppId"; AppDTO app = restTemplate.getForObject(appBaseUrl(), AppDTO.class, appId); assert app != null; ClusterDTO cluster = restTemplate.getForObject(clusterBaseUrl(), ClusterDTO.class, app.getAppId(), "default"); assert cluster != null; NamespaceDTO namespace = restTemplate.getForObject(namespaceBaseUrl(), NamespaceDTO.class, app.getAppId(), cluster.getName(), "application"); String itemKey = "test-key"; String itemValue = "test-value"; ItemDTO item = new ItemDTO(itemKey, itemValue, "", 1); assert namespace != null; item.setNamespaceId(namespace.getId()); item.setDataChangeLastModifiedBy("apollo"); ResponseEntity response = restTemplate.postForEntity(itemBaseUrl(), item, ItemDTO.class, app.getAppId(), cluster.getName(), namespace.getNamespaceName()); Assert.assertEquals(HttpStatus.OK, response.getStatusCode()); Assert.assertEquals(itemKey, Objects.requireNonNull(response.getBody()).getKey()); List commitList = commitRepository.findByAppIdAndClusterNameAndNamespaceNameOrderByIdDesc(app.getAppId(), cluster.getName(), namespace.getNamespaceName(), Pageable.ofSize(10)); Assert.assertEquals(1, commitList.size()); Commit commit = commitList.get(0); Assert.assertTrue(commit.getChangeSets().contains(itemKey)); Assert.assertTrue(commit.getChangeSets().contains(itemValue)); } @Test @Sql(scripts = "/controller/test-itemset.sql", executionPhase = ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/controller/cleanup.sql", executionPhase = ExecutionPhase.AFTER_TEST_METHOD) public void testUpdate() { this.testCreate(); String appId = "someAppId"; AppDTO app = restTemplate.getForObject(appBaseUrl(), AppDTO.class, appId); assert app != null; ClusterDTO cluster = restTemplate.getForObject(clusterBaseUrl(), ClusterDTO.class, app.getAppId(), "default"); assert cluster != null; NamespaceDTO namespace = restTemplate.getForObject(namespaceBaseUrl(), NamespaceDTO.class, app.getAppId(), cluster.getName(), "application"); String itemKey = "test-key"; String itemValue = "test-value-updated"; long itemId = itemRepository.findByKey(itemKey, Pageable.ofSize(1)).getContent().get(0).getId(); ItemDTO item = new ItemDTO(itemKey, itemValue, "", 1); item.setDataChangeLastModifiedBy("apollo"); String updateUrl = url("/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/items/{itemId}"); assert namespace != null; restTemplate.put(updateUrl, item, app.getAppId(), cluster.getName(), namespace.getNamespaceName(), itemId); itemRepository.findById(itemId).ifPresent(item1 -> { assertThat(item1.getValue()).isEqualTo(itemValue); assertThat(item1.getKey()).isEqualTo(itemKey); }); List commitList = commitRepository.findByAppIdAndClusterNameAndNamespaceNameOrderByIdDesc(app.getAppId(), cluster.getName(), namespace.getNamespaceName(), Pageable.ofSize(10)); assertThat(commitList).hasSize(2); } @Test @Sql(scripts = "/controller/test-itemset.sql", executionPhase = ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/controller/cleanup.sql", executionPhase = ExecutionPhase.AFTER_TEST_METHOD) public void testDelete() { this.testCreate(); String appId = "someAppId"; AppDTO app = restTemplate.getForObject(appBaseUrl(), AppDTO.class, appId); assert app != null; ClusterDTO cluster = restTemplate.getForObject(clusterBaseUrl(), ClusterDTO.class, app.getAppId(), "default"); assert cluster != null; NamespaceDTO namespace = restTemplate.getForObject(namespaceBaseUrl(), NamespaceDTO.class, app.getAppId(), cluster.getName(), "application"); String itemKey = "test-key"; long itemId = itemRepository.findByKey(itemKey, Pageable.ofSize(1)).getContent().get(0).getId(); String deleteUrl = url("/items/{itemId}?operator=apollo"); restTemplate.delete(deleteUrl, itemId); assertThat(itemRepository.findById(itemId).isPresent()).isFalse(); assert namespace != null; List commitList = commitRepository.findByAppIdAndClusterNameAndNamespaceNameOrderByIdDesc(app.getAppId(), cluster.getName(), namespace.getNamespaceName(), Pageable.ofSize(10)); assertThat(commitList).hasSize(2); } @Test @Sql(scripts = "/controller/test-itemset.sql", executionPhase = ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/controller/cleanup.sql", executionPhase = ExecutionPhase.AFTER_TEST_METHOD) public void testSearch() { this.testCreate(); String itemKey = "test-key"; String itemValue = "test-value"; Page itemInfoDTOS = itemService.getItemInfoBySearch(itemKey, itemValue, PageRequest.of(0, 200)); HttpHeaders headers = new HttpHeaders(); HttpEntity entity = new HttpEntity<>(headers); ResponseEntity> response = restTemplate.exchange( url("/items-search/key-and-value?key={key}&value={value}&page={page}&size={size}"), HttpMethod.GET, entity, new ParameterizedTypeReference>() {}, itemKey, itemValue, 0, 200); assertThat(itemInfoDTOS.getContent().toString()) .isEqualTo(response.getBody().getContent().toString()); } } ================================================ FILE: apollo-adminservice/src/test/java/com/ctrip/framework/apollo/adminservice/controller/ItemSetControllerTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.adminservice.controller; import com.ctrip.framework.apollo.biz.entity.Item; import com.ctrip.framework.apollo.biz.repository.ItemRepository; import com.ctrip.framework.apollo.common.dto.AppDTO; import com.ctrip.framework.apollo.common.dto.ClusterDTO; import com.ctrip.framework.apollo.common.dto.ItemChangeSets; import com.ctrip.framework.apollo.common.dto.ItemDTO; import com.ctrip.framework.apollo.common.dto.NamespaceDTO; import com.ctrip.framework.apollo.common.exception.BadRequestException; import java.util.Objects; import org.junit.Assert; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.test.context.jdbc.Sql; import org.springframework.test.context.jdbc.Sql.ExecutionPhase; import java.util.List; import org.springframework.web.client.HttpClientErrorException; public class ItemSetControllerTest extends AbstractControllerTest { @Autowired ItemRepository itemRepository; @Test @Sql(scripts = "/controller/test-itemset.sql", executionPhase = ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/controller/cleanup.sql", executionPhase = ExecutionPhase.AFTER_TEST_METHOD) public void testItemSetCreated() { String appId = "someAppId"; AppDTO app = restTemplate.getForObject(appBaseUrl(), AppDTO.class, appId); Assert.assertNotNull(app); ClusterDTO cluster = restTemplate.getForObject(clusterBaseUrl(), ClusterDTO.class, app.getAppId(), "default"); Assert.assertNotNull(cluster); NamespaceDTO namespace = restTemplate.getForObject(namespaceBaseUrl(), NamespaceDTO.class, app.getAppId(), cluster.getName(), "application"); Assert.assertNotNull(namespace); Assert.assertEquals("someAppId", app.getAppId()); Assert.assertEquals("default", cluster.getName()); Assert.assertEquals("application", namespace.getNamespaceName()); int createdSize = 3; ItemChangeSets itemSet = mockCreateItemChangeSets(namespace, createdSize); ResponseEntity response = restTemplate.postForEntity(itemSetBaseUrl(), itemSet, Void.class, app.getAppId(), cluster.getName(), namespace.getNamespaceName()); Assert.assertEquals(HttpStatus.OK, response.getStatusCode()); List items = itemRepository.findByNamespaceIdOrderByLineNumAsc(namespace.getId()); Assert.assertEquals(createdSize, items.size()); Item item0 = items.get(0); Assert.assertEquals("key_0", item0.getKey()); Assert.assertEquals("created_value_0", item0.getValue()); Assert.assertEquals("created", item0.getDataChangeCreatedBy()); Assert.assertNotNull(item0.getDataChangeCreatedTime()); } @Test @Sql(scripts = "/controller/test-itemset.sql", executionPhase = ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/controller/cleanup.sql", executionPhase = ExecutionPhase.AFTER_TEST_METHOD) public void testItemSetCreatedWithInvalidNamespaceId() { String appId = "someAppId"; String clusterName = "default"; String namespaceName = "application"; String someNamespaceName = "someNamespace"; NamespaceDTO namespace = restTemplate.getForObject(namespaceBaseUrl(), NamespaceDTO.class, appId, clusterName, namespaceName); NamespaceDTO someNamespace = restTemplate.getForObject(namespaceBaseUrl(), NamespaceDTO.class, appId, clusterName, someNamespaceName); Assert.assertNotNull(someNamespace); long someNamespaceId = someNamespace.getId(); int createdSize = 3; ItemChangeSets itemSet = mockCreateItemChangeSets(namespace, createdSize); itemSet.getCreateItems().get(createdSize - 1).setNamespaceId(someNamespaceId); try { restTemplate.postForEntity(itemSetBaseUrl(), itemSet, Void.class, appId, clusterName, namespaceName); } catch (HttpClientErrorException e) { Assert.assertEquals(HttpStatus.BAD_REQUEST, e.getStatusCode()); Assert.assertTrue(Objects.requireNonNull(e.getMessage()) .contains(BadRequestException.namespaceNotMatch().getMessage())); Assert.assertTrue(e.getMessage().contains(BadRequestException.class.getName())); } List items = itemRepository.findByNamespaceIdOrderByLineNumAsc(someNamespaceId); Assert.assertEquals(0, items.size()); } @Test @Sql(scripts = "/controller/test-itemset.sql", executionPhase = ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/controller/cleanup.sql", executionPhase = ExecutionPhase.AFTER_TEST_METHOD) public void testItemSetUpdated() { String appId = "someAppId"; AppDTO app = restTemplate.getForObject(appBaseUrl(), AppDTO.class, appId); Assert.assertNotNull(app); ClusterDTO cluster = restTemplate.getForObject(clusterBaseUrl(), ClusterDTO.class, app.getAppId(), "default"); Assert.assertNotNull(cluster); NamespaceDTO namespace = restTemplate.getForObject(namespaceBaseUrl(), NamespaceDTO.class, app.getAppId(), cluster.getName(), "application"); Assert.assertNotNull(namespace); Assert.assertEquals("someAppId", app.getAppId()); Assert.assertEquals("default", cluster.getName()); Assert.assertEquals("application", namespace.getNamespaceName()); int createdSize = 3; ItemChangeSets createChangeSet = mockCreateItemChangeSets(namespace, createdSize); ResponseEntity response = restTemplate.postForEntity(itemSetBaseUrl(), createChangeSet, Void.class, app.getAppId(), cluster.getName(), namespace.getNamespaceName()); Assert.assertEquals(HttpStatus.OK, response.getStatusCode()); ItemDTO[] items = restTemplate.getForObject(itemBaseUrl(), ItemDTO[].class, app.getAppId(), cluster.getName(), namespace.getNamespaceName()); ItemChangeSets updateChangeSet = new ItemChangeSets(); updateChangeSet.setDataChangeLastModifiedBy("updated"); int updatedSize = 2; for (int i = 0; i < updatedSize; i++) { items[i].setValue("updated_value_" + i); updateChangeSet.addUpdateItem(items[i]); } response = restTemplate.postForEntity(itemSetBaseUrl(), updateChangeSet, Void.class, app.getAppId(), cluster.getName(), namespace.getNamespaceName()); Assert.assertEquals(HttpStatus.OK, response.getStatusCode()); List savedItems = itemRepository.findByNamespaceIdOrderByLineNumAsc(namespace.getId()); Assert.assertEquals(createdSize, savedItems.size()); Item item0 = savedItems.get(0); Assert.assertEquals("key_0", item0.getKey()); Assert.assertEquals("updated_value_0", item0.getValue()); Assert.assertEquals("created", item0.getDataChangeCreatedBy()); Assert.assertEquals("updated", item0.getDataChangeLastModifiedBy()); Assert.assertNotNull(item0.getDataChangeCreatedTime()); Assert.assertNotNull(item0.getDataChangeLastModifiedTime()); } @Test @Sql(scripts = "/controller/test-itemset.sql", executionPhase = ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/controller/cleanup.sql", executionPhase = ExecutionPhase.AFTER_TEST_METHOD) public void testItemSetUpdatedWithInvalidNamespaceId() { String appId = "someAppId"; String clusterName = "default"; String namespaceName = "application"; String someNamespaceName = "someNamespace"; NamespaceDTO namespace = restTemplate.getForObject(namespaceBaseUrl(), NamespaceDTO.class, appId, clusterName, namespaceName); NamespaceDTO someNamespace = restTemplate.getForObject(namespaceBaseUrl(), NamespaceDTO.class, appId, clusterName, someNamespaceName); int createdSize = 3; ItemChangeSets createChangeSet = mockCreateItemChangeSets(namespace, createdSize); Assert.assertNotNull(namespace); ResponseEntity response = restTemplate.postForEntity(itemSetBaseUrl(), createChangeSet, Void.class, appId, clusterName, namespace.getNamespaceName()); Assert.assertEquals(HttpStatus.OK, response.getStatusCode()); ItemDTO[] items = restTemplate.getForObject(itemBaseUrl(), ItemDTO[].class, appId, clusterName, namespace.getNamespaceName()); ItemChangeSets updateChangeSet = new ItemChangeSets(); updateChangeSet.setDataChangeLastModifiedBy("updated"); int updatedSize = 2; for (int i = 0; i < updatedSize; i++) { items[i].setValue("updated_value_" + i); updateChangeSet.addUpdateItem(items[i]); } try { restTemplate.postForEntity(itemSetBaseUrl(), updateChangeSet, Void.class, appId, clusterName, someNamespaceName); } catch (HttpClientErrorException e) { Assert.assertEquals(HttpStatus.BAD_REQUEST, e.getStatusCode()); Assert.assertTrue(Objects.requireNonNull(e.getMessage()) .contains(BadRequestException.namespaceNotMatch().getMessage())); Assert.assertTrue(e.getMessage().contains(BadRequestException.class.getName())); } List savedItems = itemRepository.findByNamespaceIdOrderByLineNumAsc(someNamespace.getId()); Assert.assertEquals(0, savedItems.size()); } private ItemChangeSets mockCreateItemChangeSets(NamespaceDTO namespace, int createdSize) { ItemChangeSets createChangeSet = new ItemChangeSets(); createChangeSet.setDataChangeLastModifiedBy("created"); for (int i = 0; i < createdSize; i++) { ItemDTO item = new ItemDTO(); item.setNamespaceId(namespace.getId()); item.setKey("key_" + i); item.setValue("created_value_" + i); createChangeSet.addCreateItem(item); } return createChangeSet; } @Test @Sql(scripts = "/controller/test-itemset.sql", executionPhase = ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/controller/cleanup.sql", executionPhase = ExecutionPhase.AFTER_TEST_METHOD) public void testItemSetDeleted() { String appId = "someAppId"; AppDTO app = restTemplate.getForObject(appBaseUrl(), AppDTO.class, appId); Assert.assertNotNull(app); ClusterDTO cluster = restTemplate.getForObject(clusterBaseUrl(), ClusterDTO.class, app.getAppId(), "default"); Assert.assertNotNull(cluster); NamespaceDTO namespace = restTemplate.getForObject(namespaceBaseUrl(), NamespaceDTO.class, app.getAppId(), cluster.getName(), "application"); Assert.assertNotNull(namespace); Assert.assertEquals("someAppId", app.getAppId()); Assert.assertEquals("default", cluster.getName()); Assert.assertEquals("application", namespace.getNamespaceName()); int createdSize = 3; ItemChangeSets createChangeSet = mockCreateItemChangeSets(namespace, createdSize); ResponseEntity response = restTemplate.postForEntity(itemSetBaseUrl(), createChangeSet, Void.class, app.getAppId(), cluster.getName(), namespace.getNamespaceName()); Assert.assertEquals(HttpStatus.OK, response.getStatusCode()); ItemDTO[] items = restTemplate.getForObject(itemBaseUrl(), ItemDTO[].class, app.getAppId(), cluster.getName(), namespace.getNamespaceName()); ItemChangeSets deleteChangeSet = new ItemChangeSets(); deleteChangeSet.setDataChangeLastModifiedBy("deleted"); int deletedSize = 1; for (int i = 0; i < deletedSize; i++) { items[i].setValue("deleted_value_" + i); deleteChangeSet.addDeleteItem(items[i]); } response = restTemplate.postForEntity(itemSetBaseUrl(), deleteChangeSet, Void.class, app.getAppId(), cluster.getName(), namespace.getNamespaceName()); Assert.assertEquals(HttpStatus.OK, response.getStatusCode()); List savedItems = itemRepository.findByNamespaceIdOrderByLineNumAsc(namespace.getId()); Assert.assertEquals(createdSize - deletedSize, savedItems.size()); Item item0 = savedItems.get(0); Assert.assertEquals("key_1", item0.getKey()); Assert.assertEquals("created_value_1", item0.getValue()); Assert.assertEquals("created", item0.getDataChangeCreatedBy()); Assert.assertNotNull(item0.getDataChangeCreatedTime()); } @Test @Sql(scripts = "/controller/test-itemset.sql", executionPhase = ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/controller/cleanup.sql", executionPhase = ExecutionPhase.AFTER_TEST_METHOD) public void testItemSetDeletedWithInvalidNamespaceId() { String appId = "someAppId"; String clusterName = "default"; String namespaceName = "application"; String someNamespaceName = "someNamespace"; NamespaceDTO namespace = restTemplate.getForObject(namespaceBaseUrl(), NamespaceDTO.class, appId, clusterName, namespaceName); int createdSize = 3; ItemChangeSets createChangeSet = mockCreateItemChangeSets(namespace, createdSize); Assert.assertNotNull(namespace); ResponseEntity response = restTemplate.postForEntity(itemSetBaseUrl(), createChangeSet, Void.class, appId, clusterName, namespace.getNamespaceName()); Assert.assertEquals(HttpStatus.OK, response.getStatusCode()); ItemDTO[] items = restTemplate.getForObject(itemBaseUrl(), ItemDTO[].class, appId, clusterName, namespace.getNamespaceName()); ItemChangeSets deleteChangeSet = new ItemChangeSets(); deleteChangeSet.setDataChangeLastModifiedBy("deleted"); int deletedSize = 1; for (int i = 0; i < deletedSize; i++) { items[i].setValue("deleted_value_" + i); deleteChangeSet.addDeleteItem(items[i]); } try { restTemplate.postForEntity(itemSetBaseUrl(), deleteChangeSet, Void.class, appId, clusterName, someNamespaceName); } catch (HttpClientErrorException e) { Assert.assertEquals(HttpStatus.BAD_REQUEST, e.getStatusCode()); Assert.assertTrue(Objects.requireNonNull(e.getMessage()) .contains(BadRequestException.namespaceNotMatch().getMessage())); Assert.assertTrue(e.getMessage().contains(BadRequestException.class.getName())); } List savedItems = itemRepository.findByNamespaceIdOrderByLineNumAsc(namespace.getId()); Assert.assertEquals(createdSize, savedItems.size()); } private String itemSetBaseUrl() { return url("/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/itemset"); } } ================================================ FILE: apollo-adminservice/src/test/java/com/ctrip/framework/apollo/adminservice/controller/NamespaceControllerTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.adminservice.controller; import com.ctrip.framework.apollo.common.dto.NamespaceDTO; import com.ctrip.framework.apollo.common.utils.InputValidator; import org.junit.Assert; import org.junit.Test; import org.springframework.web.client.HttpClientErrorException; import static org.hamcrest.Matchers.containsString; /** * Created by kezhenxu at 2019/1/8 16:27. * * @author kezhenxu (kezhenxu94@163.com) */ public class NamespaceControllerTest extends AbstractControllerTest { @Test public void create() { try { NamespaceDTO namespaceDTO = new NamespaceDTO(); namespaceDTO.setClusterName("cluster"); namespaceDTO.setNamespaceName("invalid name"); namespaceDTO.setAppId("whatever"); restTemplate.postForEntity(url("/apps/{appId}/clusters/{clusterName}/namespaces"), namespaceDTO, NamespaceDTO.class, namespaceDTO.getAppId(), namespaceDTO.getClusterName()); Assert.fail("Should throw"); } catch (HttpClientErrorException e) { Assert.assertThat(new String(e.getResponseBodyAsByteArray()), containsString(InputValidator.INVALID_CLUSTER_NAMESPACE_MESSAGE)); } } } ================================================ FILE: apollo-adminservice/src/test/java/com/ctrip/framework/apollo/adminservice/controller/ReleaseControllerTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.adminservice.controller; import com.ctrip.framework.apollo.biz.entity.Namespace; import com.ctrip.framework.apollo.biz.message.MessageSender; import com.ctrip.framework.apollo.biz.message.Topics; import com.ctrip.framework.apollo.biz.repository.ReleaseRepository; import com.ctrip.framework.apollo.biz.service.NamespaceService; import com.ctrip.framework.apollo.biz.service.ReleaseService; import com.ctrip.framework.apollo.common.dto.AppDTO; import com.ctrip.framework.apollo.common.dto.ClusterDTO; import com.ctrip.framework.apollo.common.dto.ItemDTO; import com.ctrip.framework.apollo.common.dto.NamespaceDTO; import com.ctrip.framework.apollo.common.dto.ReleaseDTO; import com.ctrip.framework.apollo.core.ConfigConsts; import com.google.common.base.Joiner; import com.google.gson.Gson; import org.junit.Assert; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.test.context.jdbc.Sql; import org.springframework.test.context.jdbc.Sql.ExecutionPhase; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import java.util.LinkedHashMap; import java.util.Map; import static org.mockito.Mockito.*; public class ReleaseControllerTest extends AbstractControllerTest { private static final Gson GSON = new Gson(); @Autowired ReleaseRepository releaseRepository; @Test @Sql(scripts = "/controller/test-release.sql", executionPhase = ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/controller/cleanup.sql", executionPhase = ExecutionPhase.AFTER_TEST_METHOD) public void testReleaseBuild() { String appId = "someAppId"; AppDTO app = restTemplate.getForObject(appBaseUrl(), AppDTO.class, appId); Assert.assertNotNull(app); ClusterDTO cluster = restTemplate.getForObject(clusterBaseUrl(), ClusterDTO.class, app.getAppId(), "default"); Assert.assertNotNull(cluster); NamespaceDTO namespace = restTemplate.getForObject(namespaceBaseUrl(), NamespaceDTO.class, app.getAppId(), cluster.getName(), "application"); Assert.assertNotNull(namespace); Assert.assertEquals("someAppId", app.getAppId()); Assert.assertEquals("default", cluster.getName()); Assert.assertEquals("application", namespace.getNamespaceName()); ItemDTO[] items = restTemplate.getForObject(itemBaseUrl(), ItemDTO[].class, app.getAppId(), cluster.getName(), namespace.getNamespaceName()); Assert.assertNotNull(items); Assert.assertEquals(3, items.length); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); MultiValueMap parameters = new LinkedMultiValueMap<>(); parameters.add("name", "someReleaseName"); parameters.add("comment", "someComment"); parameters.add("operator", "test"); HttpEntity> entity = new HttpEntity<>(parameters, headers); ResponseEntity response = restTemplate.postForEntity( url("/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/releases"), entity, ReleaseDTO.class, app.getAppId(), cluster.getName(), namespace.getNamespaceName()); Assert.assertEquals(HttpStatus.OK, response.getStatusCode()); ReleaseDTO release = response.getBody(); Assert.assertNotNull(release); Assert.assertEquals("someReleaseName", release.getName()); Assert.assertEquals("someComment", release.getComment()); Assert.assertEquals("someAppId", release.getAppId()); Assert.assertEquals("default", release.getClusterName()); Assert.assertEquals("application", release.getNamespaceName()); Map configurations = new LinkedHashMap<>(); configurations.put("k1", "v1"); configurations.put("k2", "v2"); configurations.put("k3", "v3"); Assert.assertEquals(GSON.toJson(configurations), release.getConfigurations()); } @Test public void testMessageSendAfterBuildRelease() throws Exception { String someAppId = "someAppId"; String someNamespaceName = "someNamespace"; String someCluster = "someCluster"; String someName = "someName"; String someComment = "someComment"; NamespaceService someNamespaceService = mock(NamespaceService.class); ReleaseService someReleaseService = mock(ReleaseService.class); MessageSender someMessageSender = mock(MessageSender.class); Namespace someNamespace = mock(Namespace.class); ReleaseController releaseController = new ReleaseController(someReleaseService, someNamespaceService, someMessageSender, null); when(someNamespaceService.findOne(someAppId, someCluster, someNamespaceName)) .thenReturn(someNamespace); releaseController.publish(someAppId, someCluster, someNamespaceName, someName, someComment, "test", false); verify(someMessageSender, times(1)) .sendMessage(Joiner.on(ConfigConsts.CLUSTER_NAMESPACE_SEPARATOR).join(someAppId, someCluster, someNamespaceName), Topics.APOLLO_RELEASE_TOPIC); } } ================================================ FILE: apollo-adminservice/src/test/java/com/ctrip/framework/apollo/adminservice/controller/ServerConfigControllerTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.adminservice.controller; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import com.ctrip.framework.apollo.biz.entity.ServerConfig; import org.junit.jupiter.api.Test; import org.springframework.test.context.jdbc.Sql; import org.springframework.test.context.jdbc.Sql.ExecutionPhase; /** * @author kl (http://kailing.pub) * @since 2022/12/14 */ class ServerConfigControllerTest extends AbstractControllerTest { @Test @Sql(scripts = "/controller/test-server-config.sql", executionPhase = ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/controller/cleanup.sql", executionPhase = ExecutionPhase.AFTER_TEST_METHOD) void findAllServerConfig() { ServerConfig[] serverConfigs = restTemplate.getForObject(url("/server/config/find-all-config"), ServerConfig[].class); assertNotNull(serverConfigs); assertEquals(1, serverConfigs.length); assertEquals("name", serverConfigs[0].getKey()); assertEquals("kl", serverConfigs[0].getValue()); } @Test @Sql(scripts = "/controller/test-server-config.sql", executionPhase = ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/controller/cleanup.sql", executionPhase = ExecutionPhase.AFTER_TEST_METHOD) void createOrUpdatePortalDBConfig() { ServerConfig serverConfig = new ServerConfig(); serverConfig.setKey("name"); serverConfig.setValue("ckl"); ServerConfig response = restTemplate.postForObject(url("/server/config"), serverConfig, ServerConfig.class); assertNotNull(response); ServerConfig[] serverConfigs = restTemplate.getForObject(url("/server/config/find-all-config"), ServerConfig[].class); assertNotNull(serverConfigs); assertEquals(1, serverConfigs.length); assertEquals("name", serverConfigs[0].getKey()); assertEquals("ckl", serverConfigs[0].getValue()); serverConfig = new ServerConfig(); serverConfig.setKey("age"); serverConfig.setValue("30"); response = restTemplate.postForObject(url("/server/config"), serverConfig, ServerConfig.class); assertNotNull(response); serverConfigs = restTemplate.getForObject(url("/server/config/find-all-config"), ServerConfig[].class); assertNotNull(serverConfigs); assertEquals(2, serverConfigs.length); } } ================================================ FILE: apollo-adminservice/src/test/java/com/ctrip/framework/apollo/adminservice/controller/TestWebSecurityConfig.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.adminservice.controller; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.web.SecurityFilterChain; @Configuration @Order(0) public class TestWebSecurityConfig { @Bean @Order(0) public SecurityFilterChain testSecurityFilterChain(HttpSecurity http) throws Exception { http.securityMatcher("/", "/console/**"); http.httpBasic(Customizer.withDefaults()); http.csrf(csrf -> csrf.disable()); http.authorizeHttpRequests(authorizeHttpRequests -> authorizeHttpRequests.requestMatchers("/") .permitAll().requestMatchers("/console/**").permitAll()); http.headers(headers -> headers.frameOptions(frameOptions -> frameOptions.disable())); return http.build(); } } ================================================ FILE: apollo-adminservice/src/test/java/com/ctrip/framework/apollo/adminservice/filter/AdminServiceAuthenticationFilterTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.adminservice.filter; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import com.ctrip.framework.apollo.biz.config.BizConfig; import jakarta.servlet.FilterChain; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import org.springframework.http.HttpHeaders; @RunWith(MockitoJUnitRunner.class) public class AdminServiceAuthenticationFilterTest { @Mock private BizConfig bizConfig; private HttpServletRequest servletRequest; private HttpServletResponse servletResponse; private FilterChain filterChain; private AdminServiceAuthenticationFilter authenticationFilter; @Before public void setUp() throws Exception { authenticationFilter = new AdminServiceAuthenticationFilter(bizConfig); initVariables(); } private void initVariables() { servletRequest = mock(HttpServletRequest.class); servletResponse = mock(HttpServletResponse.class); filterChain = mock(FilterChain.class); } @Test public void testWithAccessControlDisabled() throws Exception { when(bizConfig.isAdminServiceAccessControlEnabled()).thenReturn(false); authenticationFilter.doFilter(servletRequest, servletResponse, filterChain); verify(bizConfig, times(1)).isAdminServiceAccessControlEnabled(); verify(filterChain, times(1)).doFilter(servletRequest, servletResponse); verify(bizConfig, never()).getAdminServiceAccessTokens(); verify(servletRequest, never()).getHeader(HttpHeaders.AUTHORIZATION); verify(servletResponse, never()).sendError(anyInt(), anyString()); } @Test public void testWithAccessControlEnabledWithTokenSpecifiedWithValidTokenPassed() throws Exception { String someValidToken = "someToken"; when(bizConfig.isAdminServiceAccessControlEnabled()).thenReturn(true); when(bizConfig.getAdminServiceAccessTokens()).thenReturn(someValidToken); when(servletRequest.getHeader(HttpHeaders.AUTHORIZATION)).thenReturn(someValidToken); authenticationFilter.doFilter(servletRequest, servletResponse, filterChain); verify(bizConfig, times(1)).isAdminServiceAccessControlEnabled(); verify(bizConfig, times(1)).getAdminServiceAccessTokens(); verify(filterChain, times(1)).doFilter(servletRequest, servletResponse); verify(servletResponse, never()).sendError(anyInt(), anyString()); } @Test public void testWithAccessControlEnabledWithTokenSpecifiedWithInvalidTokenPassed() throws Exception { String someValidToken = "someValidToken"; String someInvalidToken = "someInvalidToken"; when(bizConfig.isAdminServiceAccessControlEnabled()).thenReturn(true); when(bizConfig.getAdminServiceAccessTokens()).thenReturn(someValidToken); when(servletRequest.getHeader(HttpHeaders.AUTHORIZATION)).thenReturn(someInvalidToken); authenticationFilter.doFilter(servletRequest, servletResponse, filterChain); verify(bizConfig, times(1)).isAdminServiceAccessControlEnabled(); verify(bizConfig, times(1)).getAdminServiceAccessTokens(); verify(servletResponse, times(1)).sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized"); verify(filterChain, never()).doFilter(servletRequest, servletResponse); } @Test public void testWithAccessControlEnabledWithTokenSpecifiedWithNoTokenPassed() throws Exception { String someValidToken = "someValidToken"; when(bizConfig.isAdminServiceAccessControlEnabled()).thenReturn(true); when(bizConfig.getAdminServiceAccessTokens()).thenReturn(someValidToken); when(servletRequest.getHeader(HttpHeaders.AUTHORIZATION)).thenReturn(null); authenticationFilter.doFilter(servletRequest, servletResponse, filterChain); verify(bizConfig, times(1)).isAdminServiceAccessControlEnabled(); verify(bizConfig, times(1)).getAdminServiceAccessTokens(); verify(servletResponse, times(1)).sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized"); verify(filterChain, never()).doFilter(servletRequest, servletResponse); } @Test public void testWithAccessControlEnabledWithMultipleTokenSpecifiedWithValidTokenPassed() throws Exception { String someToken = "someToken"; String anotherToken = "anotherToken"; when(bizConfig.isAdminServiceAccessControlEnabled()).thenReturn(true); when(bizConfig.getAdminServiceAccessTokens()) .thenReturn(String.format("%s,%s", someToken, anotherToken)); when(servletRequest.getHeader(HttpHeaders.AUTHORIZATION)).thenReturn(someToken); authenticationFilter.doFilter(servletRequest, servletResponse, filterChain); verify(bizConfig, times(1)).isAdminServiceAccessControlEnabled(); verify(bizConfig, times(1)).getAdminServiceAccessTokens(); verify(filterChain, times(1)).doFilter(servletRequest, servletResponse); verify(servletResponse, never()).sendError(anyInt(), anyString()); } @Test public void testWithAccessControlEnabledWithNoTokenSpecifiedWithTokenPassed() throws Exception { String someToken = "someToken"; when(bizConfig.isAdminServiceAccessControlEnabled()).thenReturn(true); when(bizConfig.getAdminServiceAccessTokens()).thenReturn(null); when(servletRequest.getHeader(HttpHeaders.AUTHORIZATION)).thenReturn(someToken); authenticationFilter.doFilter(servletRequest, servletResponse, filterChain); verify(bizConfig, times(1)).isAdminServiceAccessControlEnabled(); verify(bizConfig, times(1)).getAdminServiceAccessTokens(); verify(filterChain, times(1)).doFilter(servletRequest, servletResponse); verify(servletResponse, never()).sendError(anyInt(), anyString()); } @Test public void testWithAccessControlEnabledWithNoTokenSpecifiedWithNoTokenPassed() throws Exception { String someToken = "someToken"; when(bizConfig.isAdminServiceAccessControlEnabled()).thenReturn(true); when(bizConfig.getAdminServiceAccessTokens()).thenReturn(null); when(servletRequest.getHeader(HttpHeaders.AUTHORIZATION)).thenReturn(null); authenticationFilter.doFilter(servletRequest, servletResponse, filterChain); verify(bizConfig, times(1)).isAdminServiceAccessControlEnabled(); verify(bizConfig, times(1)).getAdminServiceAccessTokens(); verify(filterChain, times(1)).doFilter(servletRequest, servletResponse); verify(servletResponse, never()).sendError(anyInt(), anyString()); } @Test public void testWithConfigChanged() throws Exception { String someToken = "someToken"; String anotherToken = "anotherToken"; String yetAnotherToken = "yetAnotherToken"; // case 1: init state when(bizConfig.isAdminServiceAccessControlEnabled()).thenReturn(true); when(bizConfig.getAdminServiceAccessTokens()).thenReturn(someToken); when(servletRequest.getHeader(HttpHeaders.AUTHORIZATION)).thenReturn(someToken); authenticationFilter.doFilter(servletRequest, servletResponse, filterChain); verify(filterChain, times(1)).doFilter(servletRequest, servletResponse); verify(servletResponse, never()).sendError(anyInt(), anyString()); // case 2: change access tokens specified initVariables(); when(bizConfig.getAdminServiceAccessTokens()) .thenReturn(String.format("%s,%s", anotherToken, yetAnotherToken)); when(servletRequest.getHeader(HttpHeaders.AUTHORIZATION)).thenReturn(someToken); authenticationFilter.doFilter(servletRequest, servletResponse, filterChain); verify(servletResponse, times(1)).sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized"); verify(filterChain, never()).doFilter(servletRequest, servletResponse); initVariables(); when(servletRequest.getHeader(HttpHeaders.AUTHORIZATION)).thenReturn(anotherToken); authenticationFilter.doFilter(servletRequest, servletResponse, filterChain); verify(filterChain, times(1)).doFilter(servletRequest, servletResponse); verify(servletResponse, never()).sendError(anyInt(), anyString()); // case 3: change access control flag initVariables(); when(bizConfig.isAdminServiceAccessControlEnabled()).thenReturn(false); authenticationFilter.doFilter(servletRequest, servletResponse, filterChain); verify(filterChain, times(1)).doFilter(servletRequest, servletResponse); verify(servletResponse, never()).sendError(anyInt(), anyString()); verify(servletRequest, never()).getHeader(HttpHeaders.AUTHORIZATION); } } ================================================ FILE: apollo-adminservice/src/test/java/com/ctrip/framework/apollo/adminservice/filter/AdminServiceAuthenticationIntegrationTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.adminservice.filter; import com.ctrip.framework.apollo.adminservice.controller.AbstractControllerTest; import com.ctrip.framework.apollo.common.config.RefreshablePropertySource; import com.ctrip.framework.apollo.common.dto.AppDTO; import java.util.List; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.jdbc.Sql; import org.springframework.test.context.jdbc.Sql.ExecutionPhase; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.web.client.HttpClientErrorException; @DirtiesContext public class AdminServiceAuthenticationIntegrationTest extends AbstractControllerTest { @Autowired private List propertySources; @Before public void setUp() throws Exception { doRefresh(propertySources); } @Test @Sql(scripts = "/controller/test-release.sql", executionPhase = ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/filter/test-access-control-disabled.sql", executionPhase = ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/controller/cleanup.sql", executionPhase = ExecutionPhase.AFTER_TEST_METHOD) public void testWithAccessControlDisabledExplicitly() { String appId = "someAppId"; AppDTO app = restTemplate.getForObject("http://localhost:" + port + "/apps/" + appId, AppDTO.class); Assert.assertEquals("someAppId", app.getAppId()); } @Test @Sql(scripts = "/controller/test-release.sql", executionPhase = ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/filter/test-access-control-disabled.sql", executionPhase = ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/controller/cleanup.sql", executionPhase = ExecutionPhase.AFTER_TEST_METHOD) public void testWithAccessControlDisabledExplicitlyWithAccessToken() { String appId = "someAppId"; String someToken = "someToken"; HttpHeaders headers = new HttpHeaders(); headers.add(HttpHeaders.AUTHORIZATION, someToken); HttpEntity entity = new HttpEntity<>(headers); AppDTO app = restTemplate.exchange("http://localhost:" + port + "/apps/" + appId, HttpMethod.GET, entity, AppDTO.class).getBody(); Assert.assertEquals("someAppId", app.getAppId()); } @Test @Sql(scripts = "/controller/test-release.sql", executionPhase = ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/filter/test-access-control-enabled.sql", executionPhase = ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/controller/cleanup.sql", executionPhase = ExecutionPhase.AFTER_TEST_METHOD) public void testWithAccessControlEnabledWithValidAccessToken() { String appId = "someAppId"; String someValidToken = "someToken"; HttpHeaders headers = new HttpHeaders(); headers.add(HttpHeaders.AUTHORIZATION, someValidToken); HttpEntity entity = new HttpEntity<>(headers); AppDTO app = restTemplate.exchange("http://localhost:" + port + "/apps/" + appId, HttpMethod.GET, entity, AppDTO.class).getBody(); Assert.assertEquals("someAppId", app.getAppId()); } @Test(expected = HttpClientErrorException.class) @Sql(scripts = "/controller/test-release.sql", executionPhase = ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/filter/test-access-control-enabled.sql", executionPhase = ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/controller/cleanup.sql", executionPhase = ExecutionPhase.AFTER_TEST_METHOD) public void testWithAccessControlEnabledWithNoAccessToken() { String appId = "someAppId"; AppDTO app = restTemplate.getForObject("http://localhost:" + port + "/apps/" + appId, AppDTO.class); } @Test(expected = HttpClientErrorException.class) @Sql(scripts = "/controller/test-release.sql", executionPhase = ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/filter/test-access-control-enabled.sql", executionPhase = ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/controller/cleanup.sql", executionPhase = ExecutionPhase.AFTER_TEST_METHOD) public void testWithAccessControlEnabledWithInValidAccessToken() { String appId = "someAppId"; String someValidToken = "someInvalidToken"; HttpHeaders headers = new HttpHeaders(); headers.add(HttpHeaders.AUTHORIZATION, someValidToken); HttpEntity entity = new HttpEntity<>(headers); AppDTO app = restTemplate.exchange("http://localhost:" + port + "/apps/" + appId, HttpMethod.GET, entity, AppDTO.class).getBody(); } @Test @Sql(scripts = "/controller/test-release.sql", executionPhase = ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/filter/test-access-control-enabled-no-token.sql", executionPhase = ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/controller/cleanup.sql", executionPhase = ExecutionPhase.AFTER_TEST_METHOD) public void testWithAccessControlEnabledWithNoTokenSpecified() { String appId = "someAppId"; String someToken = "someToken"; HttpHeaders headers = new HttpHeaders(); headers.add(HttpHeaders.AUTHORIZATION, someToken); HttpEntity entity = new HttpEntity<>(headers); AppDTO app = restTemplate.exchange("http://localhost:" + port + "/apps/" + appId, HttpMethod.GET, entity, AppDTO.class).getBody(); Assert.assertEquals("someAppId", app.getAppId()); } private void doRefresh(List propertySources) { propertySources.forEach(refreshablePropertySource -> ReflectionTestUtils .invokeMethod(refreshablePropertySource, "refresh")); } } ================================================ FILE: apollo-adminservice/src/test/resources/application.properties ================================================ # # Copyright 2024 Apollo Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # spring.cloud.consul.enabled=false spring.cloud.zookeeper.enabled=false spring.cloud.discovery.enabled=false spring.datasource.url = jdbc:h2:mem:~/apolloconfigdb;mode=mysql;DB_CLOSE_ON_EXIT=FALSE;DB_CLOSE_DELAY=-1;BUILTIN_ALIAS_OVERRIDE=TRUE;DATABASE_TO_UPPER=FALSE spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl spring.jpa.hibernate.globally_quoted_identifiers=false spring.jpa.properties.hibernate.globally_quoted_identifiers=false spring.jpa.properties.hibernate.show_sql=false spring.jpa.properties.hibernate.metadata_builder_contributor=com.ctrip.framework.apollo.common.jpa.SqlFunctionsMetadataBuilderContributor spring.jpa.defer-datasource-initialization=true spring.h2.console.enabled = true spring.h2.console.settings.web-allow-others=true spring.main.allow-bean-definition-overriding=true ================================================ FILE: apollo-adminservice/src/test/resources/application.yml ================================================ # # Copyright 2024 Apollo Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # spring: application: name: apollo-adminservice lifecycle: timeout-per-shutdown-phase: ${GRACEFUL_SHUTDOWN_TIMEOUT:10s} server: port: ${port:8090} shutdown: graceful eureka: instance: hostname: ${hostname:localhost} prefer-ip-address: true status-page-url-path: /info health-check-url-path: /health client: service-url: defaultZone: http://${eureka.instance.hostname}:8090/eureka/ healthcheck: enabled: true management: health: status: order: DOWN, OUT_OF_SERVICE, UNKNOWN, UP ================================================ FILE: apollo-adminservice/src/test/resources/controller/cleanup.sql ================================================ -- -- Copyright 2024 Apollo Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. -- You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. -- DELETE FROM "Item"; DELETE FROM "Namespace"; DELETE FROM "AppNamespace"; DELETE FROM "Cluster"; DELETE FROM "App"; DELETE FROM "NamespaceLock"; DELETE FROM "ServerConfig"; DELETE FROM "Commit"; ================================================ FILE: apollo-adminservice/src/test/resources/controller/test-itemset.sql ================================================ -- -- Copyright 2024 Apollo Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. -- You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. -- INSERT INTO "App" (AppId, Name, OwnerName, OwnerEmail) VALUES ('someAppId','someAppName','someOwnerName','someOwnerName@ctrip.com'); INSERT INTO "Cluster" (AppId, Name) VALUES ('someAppId', 'default'); INSERT INTO "AppNamespace" (AppId, Name) VALUES ('someAppId', 'application'); INSERT INTO "AppNamespace" (AppId, Name) VALUES ('someAppId', 'someNamespace'); INSERT INTO "Namespace" (AppId, ClusterName, NamespaceName) VALUES ('someAppId', 'default', 'application'); INSERT INTO "Namespace" (AppId, ClusterName, NamespaceName) VALUES ('someAppId', 'default', 'someNamespace'); ================================================ FILE: apollo-adminservice/src/test/resources/controller/test-release.sql ================================================ -- -- Copyright 2024 Apollo Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. -- You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. -- INSERT INTO "App" (AppId, Name, OwnerName, OwnerEmail) VALUES ('someAppId','someAppName','someOwnerName','someOwnerName@ctrip.com'); INSERT INTO "Cluster" (AppId, Name) VALUES ('someAppId', 'default'); INSERT INTO "AppNamespace" (AppId, Name) VALUES ('someAppId', 'application'); INSERT INTO "Namespace" (Id, AppId, ClusterName, NamespaceName) VALUES (100, 'someAppId', 'default', 'application'); INSERT INTO "Item" (NamespaceId, "Key", "Type", "Value", Comment) VALUES (100, 'k1', '0', 'v1', 'comment1'); INSERT INTO "Item" (NamespaceId, "Key", "Type", "Value", Comment) VALUES (100, 'k2', '0', 'v2', 'comment1'); INSERT INTO "Item" (NamespaceId, "Key", "Type", "Value", Comment) VALUES (100, 'k3', '0', 'v3', 'comment1'); ================================================ FILE: apollo-adminservice/src/test/resources/controller/test-server-config.sql ================================================ -- -- Copyright 2024 Apollo Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. -- You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. -- INSERT INTO "ServerConfig" ("Key", "Cluster", "Value") VALUES ('name', 'default', 'kl'); ================================================ FILE: apollo-adminservice/src/test/resources/data.sql ================================================ -- -- Copyright 2024 Apollo Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. -- You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. -- INSERT INTO "App" (AppId, Name, OwnerName, OwnerEmail) VALUES ('100003171','apollo-config-service','刘一鸣','liuym@ctrip.com'); INSERT INTO "App" (AppId, Name, OwnerName, OwnerEmail) VALUES ('100003172','apollo-admin-service','宋顺','song_s@ctrip.com'); INSERT INTO "App" (AppId, Name, OwnerName, OwnerEmail) VALUES ('100003173','apollo-portal','张乐','zhanglea@ctrip.com'); INSERT INTO "App" (AppId, Name, OwnerName, OwnerEmail) VALUES ('fxhermesproducer','fx-hermes-producer','梁锦华','jhliang@ctrip.com'); INSERT INTO "Cluster" (AppId, Name) VALUES ('100003171', 'default'); INSERT INTO "Cluster" (AppId, Name) VALUES ('100003171', 'cluster1'); INSERT INTO "Cluster" (AppId, Name) VALUES ('100003172', 'default'); INSERT INTO "Cluster" (AppId, Name) VALUES ('100003172', 'cluster2'); INSERT INTO "Cluster" (AppId, Name) VALUES ('100003173', 'default'); INSERT INTO "Cluster" (AppId, Name) VALUES ('100003173', 'cluster3'); INSERT INTO "Cluster" (AppId, Name) VALUES ('fxhermesproducer', 'default'); INSERT INTO "AppNamespace" (AppId, Name) VALUES ('100003171', 'application'); INSERT INTO "AppNamespace" (AppId, Name) VALUES ('100003171', 'fx.apollo.config'); INSERT INTO "AppNamespace" (AppId, Name) VALUES ('100003172', 'application'); INSERT INTO "AppNamespace" (AppId, Name) VALUES ('100003172', 'fx.apollo.admin'); INSERT INTO "AppNamespace" (AppId, Name) VALUES ('100003173', 'application'); INSERT INTO "AppNamespace" (AppId, Name) VALUES ('100003173', 'fx.apollo.portal'); INSERT INTO "AppNamespace" (AppId, Name) VALUES ('fxhermesproducer', 'fx.hermes.producer'); INSERT INTO "Namespace" (Id, AppId, ClusterName, NamespaceName) VALUES (1, '100003171', 'default', 'application'); INSERT INTO "Namespace" (Id, AppId, ClusterName, NamespaceName) VALUES (5, '100003171', 'cluster1', 'application'); INSERT INTO "Namespace" (Id, AppId, ClusterName, NamespaceName) VALUES (2, 'fxhermesproducer', 'default', 'fx.hermes.producer'); INSERT INTO "Namespace" (Id, AppId, ClusterName, NamespaceName) VALUES (3, '100003172', 'default', 'application'); INSERT INTO "Namespace" (Id, AppId, ClusterName, NamespaceName) VALUES (4, '100003173', 'default', 'application'); INSERT INTO "Item" (NamespaceId, "Key", "Value", Comment) VALUES (1, 'k1', 'v1', 'comment1'); INSERT INTO "Item" (NamespaceId, "Key", "Value", Comment) VALUES (1, 'k2', 'v2', 'comment2'); INSERT INTO "Item" (NamespaceId, "Key", "Value", Comment) VALUES (2, 'k3', 'v3', 'comment3'); INSERT INTO "Item" (NamespaceId, "Key", "Value", Comment, LineNum) VALUES (5, 'k1', 'v4', 'comment4',1); INSERT INTO "Release" (ReleaseKey, Name, Comment, AppId, ClusterName, NamespaceName, Configurations) VALUES ('TEST-RELEASE-KEY', 'REV1','First Release','100003171', 'default', 'application', '{"k1":"v1"}'); ================================================ FILE: apollo-adminservice/src/test/resources/filter/test-access-control-disabled.sql ================================================ -- -- Copyright 2024 Apollo Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. -- You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. -- INSERT INTO "ServerConfig" ("Key", "Cluster", "Value") VALUES ('admin-service.access.tokens', 'default', 'someToken,anotherToken'), ('admin-service.access.control.enabled', 'default', 'false'); ================================================ FILE: apollo-adminservice/src/test/resources/filter/test-access-control-enabled-no-token.sql ================================================ -- -- Copyright 2024 Apollo Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. -- You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. -- INSERT INTO "ServerConfig" ("Key", "Cluster", "Value") VALUES ('admin-service.access.control.enabled', 'default', 'true'); ================================================ FILE: apollo-adminservice/src/test/resources/filter/test-access-control-enabled.sql ================================================ -- -- Copyright 2024 Apollo Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. -- You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. -- INSERT INTO "ServerConfig" ("Key", "Cluster", "Value") VALUES ('admin-service.access.tokens', 'default', 'someToken,anotherToken'), ('admin-service.access.control.enabled', 'default', 'true'); ================================================ FILE: apollo-adminservice/src/test/resources/import.sql ================================================ -- -- Copyright 2024 Apollo Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. -- You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. -- ALTER TABLE "App" ALTER COLUMN DataChange_CreatedBy VARCHAR(255) NULL; ALTER TABLE "App" ALTER COLUMN DataChange_CreatedTime TIMESTAMP NULL; ALTER TABLE "App" ALTER COLUMN OrgName VARCHAR(255) NULL; ALTER TABLE "App" ALTER COLUMN OrgId VARCHAR(255) NULL; ALTER TABLE "AppNamespace" ALTER COLUMN DataChange_CreatedBy VARCHAR(255) NULL; ALTER TABLE "AppNamespace" ALTER COLUMN DataChange_CreatedTime TIMESTAMP NULL; ALTER TABLE "AppNamespace" ALTER COLUMN Format VARCHAR(255) NULL; ALTER TABLE "Cluster" ALTER COLUMN DataChange_CreatedBy VARCHAR(255) NULL; ALTER TABLE "Cluster" ALTER COLUMN DataChange_CreatedTime TIMESTAMP NULL; ALTER TABLE "Cluster" ALTER COLUMN ParentClusterId BIGINT DEFAULT 0; ALTER TABLE "Namespace" ALTER COLUMN DataChange_CreatedBy VARCHAR(255) NULL; ALTER TABLE "Namespace" ALTER COLUMN DataChange_CreatedTime TIMESTAMP NULL; ALTER TABLE "Item" ALTER COLUMN DataChange_CreatedBy VARCHAR(255) NULL; ALTER TABLE "Item" ALTER COLUMN DataChange_CreatedTime TIMESTAMP NULL; ALTER TABLE "Release" ALTER COLUMN DataChange_CreatedBy VARCHAR(255) NULL; ALTER TABLE "Release" ALTER COLUMN DataChange_CreatedTime TIMESTAMP NULL; ALTER TABLE "ServerConfig" ALTER COLUMN DataChange_CreatedBy VARCHAR(255) NULL; ALTER TABLE "ServerConfig" ALTER COLUMN DataChange_CreatedTime TIMESTAMP NULL; ALTER TABLE "ServerConfig" ALTER COLUMN Comment VARCHAR(255) NULL; ALTER TABLE "Audit" ALTER COLUMN DataChange_CreatedBy VARCHAR(255) NULL; ALTER TABLE "Audit" ALTER COLUMN DataChange_CreatedTime TIMESTAMP NULL; CREATE ALIAS IF NOT EXISTS UNIX_TIMESTAMP FOR "com.ctrip.framework.apollo.common.jpa.H2Function.unixTimestamp"; ================================================ FILE: apollo-adminservice/src/test/resources/logback-test.xml ================================================ utf-8 [%p] %c - %m%n ================================================ FILE: apollo-assembly/pom.xml ================================================ com.ctrip.framework.apollo apollo ${revision} ../pom.xml 4.0.0 apollo-assembly Apollo Assembly ${project.artifactId} com.ctrip.framework.apollo apollo-configservice com.ctrip.framework.apollo apollo-adminservice com.ctrip.framework.apollo apollo-portal org.apache.maven.plugins maven-resources-plugin 3.2.0 copy-resources validate copy-resources ${project.build.directory}/classes/META-INF/sql/profiles ${project.parent.basedir}/scripts/sql/profiles h2-default/apolloconfigdb.sql h2-default/apolloportaldb.sql mysql-database-not-specified/apolloconfigdb.sql mysql-database-not-specified/apolloportaldb.sql org.springframework.boot spring-boot-maven-plugin ================================================ FILE: apollo-assembly/src/main/java/com/ctrip/framework/apollo/assembly/ApolloApplication.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.assembly; import com.ctrip.framework.apollo.adminservice.AdminServiceApplication; import com.ctrip.framework.apollo.audit.configuration.ApolloAuditAutoConfiguration; import com.ctrip.framework.apollo.configservice.ConfigServiceApplication; import com.ctrip.framework.apollo.portal.PortalApplication; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.slf4j.MDC; import org.springframework.boot.WebApplicationType; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration; import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.cloud.context.scope.refresh.RefreshScope; import org.springframework.context.ConfigurableApplicationContext; @SpringBootApplication( exclude = {DataSourceAutoConfiguration.class, DataSourceTransactionManagerAutoConfiguration.class, HibernateJpaAutoConfiguration.class, ApolloAuditAutoConfiguration.class,}, excludeName = {"org.springframework.cloud.netflix.eureka.EurekaClientAutoConfiguration", "org.springframework.cloud.netflix.eureka.EurekaDiscoveryClientConfiguration"}) public class ApolloApplication { private static final Logger logger = LoggerFactory.getLogger(ApolloApplication.class); public static void main(String[] args) throws Exception { /** * Common */ MDC.put("starting_context", "[starting:common] "); logger.info("commonContext starting..."); ConfigurableApplicationContext commonContext = new SpringApplicationBuilder(ApolloApplication.class).web(WebApplicationType.NONE) .run(args); logger.info("commonContext [{}] isActive: {}", commonContext.getId(), commonContext.isActive()); /** * ConfigService */ MDC.put("starting_context", "[starting:config] "); logger.info("configContext starting..."); ConfigurableApplicationContext configContext = new SpringApplicationBuilder(ConfigServiceApplication.class).parent(commonContext) .profiles("assembly").sources(RefreshScope.class).run(args); logger.info("configContext [{}] isActive: {}", configContext.getId(), configContext.isActive()); /** * AdminService */ MDC.put("starting_context", "[starting:admin] "); logger.info("adminContext starting..."); ConfigurableApplicationContext adminContext = new SpringApplicationBuilder(AdminServiceApplication.class).parent(commonContext) .profiles("assembly").sources(RefreshScope.class).run(args); logger.info("adminContext [{}] isActive: {}", adminContext.getId(), adminContext.isActive()); /** * Portal */ MDC.put("starting_context", "[starting:portal] "); logger.info("portalContext starting..."); ConfigurableApplicationContext portalContext = new SpringApplicationBuilder(PortalApplication.class).parent(commonContext) .profiles("assembly").sources(RefreshScope.class).run(args); logger.info("portalContext [{}] isActive: {}", portalContext.getId(), portalContext.isActive()); MDC.clear(); } } ================================================ FILE: apollo-assembly/src/main/resources/application-database-discovery.properties ================================================ # # Copyright 2024 Apollo Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # apollo.eureka.server.enabled=false eureka.client.enabled=false spring.cloud.discovery.enabled=false apollo.service.registry.enabled=true apollo.service.registry.cluster=default apollo.service.registry.heartbeat-interval-in-second=10 apollo.service.discovery.enabled=true # health check by heartbeat, heartbeat time before 61s ago will be seemed as unhealthy apollo.service.discovery.health-check-interval-in-second = 61 ================================================ FILE: apollo-assembly/src/main/resources/application-github.properties ================================================ # # Copyright 2024 Apollo Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # Config DataSource spring.config-datasource.url=jdbc:h2:mem:~/apollo-config-db;mode=mysql;DB_CLOSE_ON_EXIT=FALSE;DB_CLOSE_DELAY=-1;BUILTIN_ALIAS_OVERRIDE=TRUE;DATABASE_TO_UPPER=FALSE #spring.config-datasource.username= #spring.config-datasource.password= spring.sql.config-init.schema-locations=@@repository@@/profiles/@@platform@@@@suffix@@/apolloconfigdb.sql spring.sql.config-init.mode=embedded # Portal DataSource spring.portal-datasource.url=jdbc:h2:mem:~/apollo-portal-db;mode=mysql;DB_CLOSE_ON_EXIT=FALSE;DB_CLOSE_DELAY=-1;BUILTIN_ALIAS_OVERRIDE=TRUE;DATABASE_TO_UPPER=FALSE #spring.portal-datasource.username= #spring.portal-datasource.password= spring.sql.portal-init.schema-locations=@@repository@@/profiles/@@platform@@@@suffix@@/apolloportaldb.sql spring.sql.portal-init.mode=embedded # Resolve Multi DataSource JMX name conflict spring.jmx.unique-names=true # H2 datasource spring.jpa.hibernate.ddl-auto=none spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl spring.jpa.properties.hibernate.show_sql=false spring.jpa.properties.hibernate.metadata_builder_contributor=com.ctrip.framework.apollo.common.jpa.SqlFunctionsMetadataBuilderContributor spring.h2.console.enabled=true spring.h2.console.settings.web-allow-others=true # Sql logging #logging.level.org.hibernate.SQL=DEBUG # Default env apollo.portal.envs=local # Spring session spring.session.store-type=none ================================================ FILE: apollo-assembly/src/main/resources/application.properties ================================================ # # Copyright 2024 Apollo Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # You may uncomment the following config to activate different spring profiles #spring.profiles.active=github,consul-discovery #spring.profiles.active=github,zookeeper-discovery #spring.profiles.active=github,custom-defined-discovery #spring.profiles.active=github,database-discovery # You may change the following config to activate different database profiles like h2/postgres spring.profiles.group.github = mysql # true: enabled the new feature of audit log # false/missing: disable it apollo.audit.log.enabled = true ================================================ FILE: apollo-assembly/src/main/resources/application.yml ================================================ # # Copyright 2024 Apollo Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # spring: profiles: active: ${apollo_profile} cloud: consul: enabled: false zookeeper: enabled: false session: store-type: none jpa: properties: hibernate: metadata_builder_contributor: com.ctrip.framework.apollo.common.jpa.SqlFunctionsMetadataBuilderContributor lifecycle: timeout-per-shutdown-phase: ${GRACEFUL_SHUTDOWN_TIMEOUT:10s} logging: file: name: /opt/logs/apollo-assembly.log management: health: status: order: DOWN, OUT_OF_SERVICE, UNKNOWN, UP ldap: enabled: false eureka: instance: hostname: ${hostname:localhost} prefer-ip-address: true status-page-url-path: /info health-check-url-path: /health server: peer-eureka-nodes-update-interval-ms: 60000 enable-self-preservation: false client: service-url: # This setting will be overridden by eureka.service.url setting from ApolloConfigDB.ServerConfig or System Property # see com.ctrip.framework.apollo.biz.eureka.ApolloEurekaClientConfig defaultZone: http://${eureka.instance.hostname}:8080/eureka/ # assembly classpath contains eureka server dependencies, so force eureka client to avoid jersey transport mode jersey: enabled: false healthcheck: enabled: true eureka-service-url-poll-interval-seconds: 60 fetch-registry: false register-with-eureka: false server: shutdown: graceful compression: enabled: true tomcat: use-relative-redirects: true servlet: session: cookie: # prevent csrf same-site: Lax ================================================ FILE: apollo-assembly/src/main/resources/logback.xml ================================================ propertyContains("LOG_APPENDERS", "FILE") && !propertyContains("LOG_APPENDERS", "CONSOLE") propertyContains("LOG_APPENDERS", "CONSOLE") && !propertyContains("LOG_APPENDERS", "FILE") ================================================ FILE: apollo-assembly/src/test/java/com/ctrip/framework/apollo/assembly/LocalApolloApplication.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.assembly; import com.ctrip.framework.apollo.adminservice.AdminServiceApplication; import com.ctrip.framework.apollo.configservice.ConfigServiceApplication; import com.ctrip.framework.apollo.portal.PortalApplication; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.WebApplicationType; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.cloud.context.scope.refresh.RefreshScope; import org.springframework.context.ConfigurableApplicationContext; @SpringBootApplication( exclude = {DataSourceAutoConfiguration.class, HibernateJpaAutoConfiguration.class}) public class LocalApolloApplication { private static final Logger logger = LoggerFactory.getLogger(ApolloApplication.class); public static void main(String[] args) throws Exception { /** * Common */ ConfigurableApplicationContext commonContext = new SpringApplicationBuilder(ApolloApplication.class).web(WebApplicationType.NONE) .run(args); logger.info(commonContext.getId() + " isActive: " + commonContext.isActive()); /** * ConfigService */ if (commonContext.getEnvironment().containsProperty("configservice")) { ConfigurableApplicationContext configContext = new SpringApplicationBuilder(ConfigServiceApplication.class).parent(commonContext) .sources(RefreshScope.class).run(args); logger.info(configContext.getId() + " isActive: " + configContext.isActive()); } /** * AdminService */ if (commonContext.getEnvironment().containsProperty("adminservice")) { ConfigurableApplicationContext adminContext = new SpringApplicationBuilder(AdminServiceApplication.class).parent(commonContext) .sources(RefreshScope.class).run(args); logger.info(adminContext.getId() + " isActive: " + adminContext.isActive()); } /** * Portal */ if (commonContext.getEnvironment().containsProperty("portal")) { ConfigurableApplicationContext portalContext = new SpringApplicationBuilder(PortalApplication.class).parent(commonContext).run(args); logger.info(portalContext.getId() + " isActive: " + portalContext.isActive()); } } } ================================================ FILE: apollo-assembly/src/test/resources/application.properties ================================================ # # Copyright 2024 Apollo Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # Config DataSource spring.config-datasource.url=jdbc:h2:mem:~/apollo-config-db;mode=mysql;DB_CLOSE_ON_EXIT=FALSE;DB_CLOSE_DELAY=-1;BUILTIN_ALIAS_OVERRIDE=TRUE;DATABASE_TO_UPPER=FALSE #spring.config-datasource.username= #spring.config-datasource.password= spring.sql.config-init.schema-locations=@@repository@@/profiles/@@platform@@@@suffix@@/apolloconfigdb.sql spring.sql.config-init.mode=embedded # Portal DataSource spring.portal-datasource.url=jdbc:h2:mem:~/apollo-portal-db;mode=mysql;DB_CLOSE_ON_EXIT=FALSE;DB_CLOSE_DELAY=-1;BUILTIN_ALIAS_OVERRIDE=TRUE;DATABASE_TO_UPPER=FALSE #spring.portal-datasource.username= #spring.portal-datasource.password= spring.sql.portal-init.schema-locations=@@repository@@/profiles/@@platform@@@@suffix@@/apolloportaldb.sql spring.sql.portal-init.mode=embedded # Resolve Multi DataSource JMX name conflict spring.jmx.unique-names=true # H2 datasource spring.jpa.hibernate.ddl-auto=none spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl spring.jpa.properties.hibernate.show_sql=false spring.jpa.properties.hibernate.metadata_builder_contributor=com.ctrip.framework.apollo.common.jpa.SqlFunctionsMetadataBuilderContributor spring.h2.console.enabled=true spring.h2.console.settings.web-allow-others=true # Default env apollo.portal.envs=local ================================================ FILE: apollo-assembly/src/test/resources/application.yml ================================================ # # Copyright 2024 Apollo Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # spring: profiles: active: local ================================================ FILE: apollo-assembly/src/test/resources/logback-test.xml ================================================ utf-8 [%p] %c - %m%n ================================================ FILE: apollo-audit/README.md ================================================ # Features: Apollo-Audit-Log This module provides audit log functions for other Apollo modules. Only apolloconfig's developer need to read it, apolloconfig's user doesn't need. ## How to enable/disable We can switch this module freely by properties: by adding properties to application.properties: ``` # true: enabled the new feature of audit log # false/missing: disable it apollo.audit.log.enabled = true ``` ## How to generate audit log ### Append an AuditLog Through an AuditLog, we have the ability to record **Who, When, Why, Where, What and How** operates something. We can do this by using annotations: ```java @ApolloAuditLog(type=OpType.CREATE,name="App.create") public App create() { // ... } ``` Through this, an AuditLog will be created and its AuditScope will be activated during the execution of this method. Equally, we can use ApolloAuditLogApi to do this manually: ```java public App create() { try(AutoCloseable auditScope = api.appendAuditLog(type, name)) { // ... } } /**************OR**************/ public App create() { Autocloseable auditScope = api.appendAuditLog(type, name); // ... auditScope.close(); } ``` The only thing you need to pay attention to is that you need to close this scope manually~ ### Append DataInfluence This function can also be implemented automatically and manually. There is a corresponding relationship between DataInfluences and a certain AuditLog, and they are caused by this AuditLog. But not all AuditLogs will generate DataInfluences! #### Mark which data change First, we need to add audit-bean-definition to class of the entity you want to audit: ```java @ApolloAuditLogDataInfluenceTable(tableName = "App") public class App extends BaseEntity { @ApolloAuditLogDataInfluenceTableField(fieldName = "Name") private String name; private String orgId; } ``` In class App, we define that its data-influence table' name is "App", the field "name" needs to be audited and its audit field name in the table "App" is "Name". The field "orgId" is no need to be audited. Second, use API's method to append it: Actually we don't need to manually call it. We can depend on the DomainEvents that pre-set in BaseEntity: ```java @DomainEvents public Collection domainEvents() { return Collections.singletonList(new ApolloAuditLogDataInfluenceEvent(this.getClass(), this)); } ``` And this will call appendDataInfluences automatically by the listener. #### Manually ```java /** * Append DataInfluences by a list of entities needs to be audited, and their * audit-bean-definition. */ ApolloAuditLogApi.appendDataInfluences(List entities, Class beanDefinition); ``` Just call the api method in an active scope, the data influences will combine with the log automatically. ```java public App create() { try(AutoCloseable auditScope = api.appendAuditLog(type, name)) { // ... api.appendDataInfluences(appList, App.class); // or. api.appendDataInfluence("App","10001","Name","xxx"); } } ``` #### some tricky situations Yet, sometimes we can't catch the domain events like some operations that directly change database fields. We can use annotations to catch the input parameters: ```java @ApolloAuditLog(type=OpType.DELETE,name="AppNamespace.batchDeleteByAppId") public AppNamespace batchDeleteByAppId( @ApolloAuditLogDataInfluence @ApolloAuditLogDataInfluenceTable(tableName="AppNamespace") @ApolloAuditLogDataInfluenceTableField(fieldName="AppId") String appId) { // ... } ``` This will generate a special data influence. It means that all entities matching the input parameter value have been affected. ## How to verify the audit-log work ### check-by-UI The entrance of audit log UI is in Admin Tools. Then, we can check if the AuditLogs are created properly by searching or just find in table below. Then, check in the trace detail page. We can check if the relationship between AuditLogs are correct and the DataInfluences caused by certain AuditLog is logically established. In the rightmost column, we can view the historical operation records of the specified field's value. Null means being deleted~ ### check-by-database The databases are in ApolloPortalDB, the table `AuditLog` and `AuditLogDataInfluence`. We can verify if the parent/followsfrom relationships are in line with our expectations. ================================================ FILE: apollo-audit/apollo-audit-annotation/pom.xml ================================================ apollo-audit com.ctrip.framework.apollo ${revision} 4.0.0 apollo-audit-annotation ${revision} ================================================ FILE: apollo-audit/apollo-audit-annotation/src/main/java/com/ctrip/framework/apollo/audit/annotation/ApolloAuditLog.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.audit.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * Mark which method should be audited, add to controller or service's method. *

* Define the attributes of the operation for persisting and querying. When adding to controller's * methods, suggested that don't set name, and it will automatically be set to request's url. *

* Example usage: *
 * {@code
 * @ApolloAuditLog(type=OpType.CREATE,name="App.create")
 * public App create() {
 *   // ...
 * }
 * }
 * 
* * @author luke0125 * @since 2.2.0 */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface ApolloAuditLog { /** * Define the type of operation. * * @return operation type */ OpType type(); /** * Define the name of operation. The requested URL will be taken by default if no specific name is * specified. * * @return operation name */ String name() default ""; /** * Define the description of operation. Default is "no description". * * @return operation description */ String description() default "no description"; } ================================================ FILE: apollo-audit/apollo-audit-annotation/src/main/java/com/ctrip/framework/apollo/audit/annotation/ApolloAuditLogDataInfluence.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.audit.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * Combine with {@link ApolloAuditLog}, mark which method's parameter is audit log's data change. *

* Example usage: *
 * {@code
 * @ApolloAuditLog(type=OpType.DELETE,name="AppNamespace.batchDeleteByAppId")
 * public AppNamespace batchDeleteByAppId(
 *            @ApolloAuditLogDataInfluence String appId) {
 *   // ...
 * }
 * }
 * 
* * @author luke0125 * @since 2.2.0 */ @Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME) public @interface ApolloAuditLogDataInfluence { } ================================================ FILE: apollo-audit/apollo-audit-annotation/src/main/java/com/ctrip/framework/apollo/audit/annotation/ApolloAuditLogDataInfluenceTable.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.audit.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * Mainly used in class definitions, indicates the name of the corresponding audit data table of * this class. *

* It could also be used on method parameters to express the table name of the class which this * parameter belongs to. *

* Example usage: *
 * {@code
 * CASE 1:
 * @ApolloAuditLogDataInfluenceTable(tableName="App")
 * public class App {
 *   // ...
 * }
 * CASE 2:
 * @ApolloAuditLog(type=OpType.DELETE,name="AppNamespace.batchDeleteByAppId")
 * public AppNamespace batchDeleteByAppId(
 *   @ApolloAuditLogDataInfluence
 *   @ApolloAuditLogDataInfluenceTable(tableName="AppNamespace") String appId) {
 *   // ...
 * }
 * }
 * 
* * @author luke0125 * @since 2.2.0 */ @Target({ElementType.TYPE, ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) public @interface ApolloAuditLogDataInfluenceTable { /** * Define the table name(entity name) of audited entity. * * @return table name */ String tableName(); } ================================================ FILE: apollo-audit/apollo-audit-annotation/src/main/java/com/ctrip/framework/apollo/audit/annotation/ApolloAuditLogDataInfluenceTableField.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.audit.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * Mainly used in field definitions, indicates the name of the corresponding audit data table field * of this member variables(attributes). *

* It could also be used on method parameters to express the field name of the field which this * parameter matches. *

* Example usage: *
 * {@code
 * CASE 1:
 * public class App {
 *   @ApolloAuditLogDataInfluenceTableField(fieldName="AppId")
 *   private String appId;
 *   // ...
 * }
 * CASE 2:
 * @ApolloAuditLog(type=OpType.DELETE,name="AppNamespace.batchDeleteByAppId")
 * public AppNamespace batchDeleteByAppId(
 *   @ApolloAuditLogDataInfluence
 *   @ApolloAuditLogDataInfluenceTable(tableName="AppNamespace")
 *   @ApolloAuditLogDataInfluenceTableField(fieldName="AppId") String appId) {
 *   // ...
 * }
 * }
 * 
* * @author luke0125 * @since 2.2.0 */ @Target({ElementType.FIELD, ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) public @interface ApolloAuditLogDataInfluenceTableField { /** * Define the field name of audited entity field. * * @return field name */ String fieldName(); } ================================================ FILE: apollo-audit/apollo-audit-annotation/src/main/java/com/ctrip/framework/apollo/audit/annotation/OpType.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.audit.annotation; /** * Includes all types of audit operations. * * @author luke0125 * @since 2.2.0 */ public enum OpType { CREATE, UPDATE, DELETE, RPC } ================================================ FILE: apollo-audit/apollo-audit-api/pom.xml ================================================ apollo-audit com.ctrip.framework.apollo ${revision} 4.0.0 apollo-audit-api ${revision} com.ctrip.framework.apollo apollo-audit-annotation ================================================ FILE: apollo-audit/apollo-audit-api/src/main/java/com/ctrip/framework/apollo/audit/api/ApolloAuditLogApi.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.audit.api; /** * API interface that integrates all functional interfaces. * * @author luke0125 * @since 2.2.0 */ public interface ApolloAuditLogApi extends ApolloAuditLogRecordApi, ApolloAuditLogQueryApi { } ================================================ FILE: apollo-audit/apollo-audit-api/src/main/java/com/ctrip/framework/apollo/audit/api/ApolloAuditLogQueryApi.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.audit.api; import com.ctrip.framework.apollo.audit.dto.ApolloAuditLogDTO; import com.ctrip.framework.apollo.audit.dto.ApolloAuditLogDataInfluenceDTO; import com.ctrip.framework.apollo.audit.dto.ApolloAuditLogDetailsDTO; import java.util.Date; import java.util.List; /** * Mainly used to query AuditLogs and DataInfluences. * * @author luke0125 * @since 2.2.0 */ public interface ApolloAuditLogQueryApi { /** * Query all AuditLogs by page * * @param page index from 0 * @param size size of a page * @return List of ApolloAuditLogDTO */ List queryLogs(int page, int size); /** * Query AuditLogs by operator name and time limit and page * * @param opName operation name of querying * @param startDate expect result after or equal this time * @param endDate expect result before or equal this time * @param page index from 0 * @param size size of a page * @return List of ApolloAuditLogDTO */ List queryLogsByOpName(String opName, Date startDate, Date endDate, int page, int size); /** * Query AuditLogDetails by trace id. *

* An AuditLogDetail contains an AuditLog and DataInfluences it caused. *

*
   * {@code
   *   An AuditLogDetail:
   *   {
   *     LogDTO:{},
   *     DataInfluencesDTO:[]
   *   }
   * }
   * 
* * @param traceId unique id of a operation trace * @return List of ApolloAuditLogDetailsDTO */ List queryTraceDetails(String traceId); /** * Query DataInfluences by specific entity's specified field and page * * @param entityName target entity's name(audit table name) * @param entityId target entity's id(audit table id) * @param fieldName target field's name(audit field id) * @param page index from 0 * @param size size of a page * @return List of ApolloAuditLogDetailsDTO */ List queryDataInfluencesByField(String entityName, String entityId, String fieldName, int page, int size); /** * Fuzzy search related AuditLog by query-string and page, page index from 0. * * @param query input query string, used to fuzzy search * @param page index from 0 * @param size size of a page * @return List of ApolloAuditLogDetailsDTO */ List searchLogByNameOrTypeOrOperator(String query, int page, int size); } ================================================ FILE: apollo-audit/apollo-audit-api/src/main/java/com/ctrip/framework/apollo/audit/api/ApolloAuditLogRecordApi.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.audit.api; import com.ctrip.framework.apollo.audit.annotation.OpType; import java.util.List; /** * Mainly used to Record AuditLogs and DataInfluences. * * @author luke0125 * @since 2.2.0 */ public interface ApolloAuditLogRecordApi { /** * Append a new AuditLog by type and name.The operation's description would be default by "no * description". *

* Functionally aligned with annotations. *

* Need to close the audited scope manually! * * @param type operation's type * @param name operation's name * @return Returns an AuditScope needs to be closed when the audited operation ends. */ AutoCloseable appendAuditLog(OpType type, String name); /** * Append a new AuditLog by type and name and description. *

* Functionally aligned with annotations. *

* Need to close the audited scope manually! * * @param type operation's type * @param name operation's name * @param description operation's description * @return Returns an AuditScope needs to be closed when the audited operation ends. */ AutoCloseable appendAuditLog(OpType type, String name, String description); /** * Directly append a new DataInfluence by the attributes it should have. *

* Only when there is an active AuditScope in the context at this time can appending DataInfluence * be performed correctly. It will be considered to be caused by currently active operations. * * @param entityName influenced entity's name (audit table name) * @param entityId influenced entity's id (audit table id) * @param fieldName influenced entity's field name (audit table field) * @param fieldCurrentValue influenced entity's field current value */ void appendDataInfluence(String entityName, String entityId, String fieldName, String fieldCurrentValue); /** * Append DataInfluences by a list of entities needs to be audited, and their * audit-bean-definition. *

* Only when there is an active AuditScope in the context at this time can appending * DataInfluences be performed correctly. They will be considered to be caused by currently active * operations. * * @param entities entities needs to be audited * @param beanDefinition entities' audit-bean-definition */ void appendDataInfluences(List entities, Class beanDefinition); } ================================================ FILE: apollo-audit/apollo-audit-api/src/main/java/com/ctrip/framework/apollo/audit/dto/ApolloAuditLogDTO.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.audit.dto; import java.util.Date; public class ApolloAuditLogDTO { private long id; private String traceId; private String spanId; private String parentSpanId; private String followsFromSpanId; private String operator; private String opType; private String opName; private String description; private Date happenedTime; public long getId() { return id; } public void setId(long id) { this.id = id; } public String getTraceId() { return traceId; } public void setTraceId(String traceId) { this.traceId = traceId; } public String getSpanId() { return spanId; } public void setSpanId(String spanId) { this.spanId = spanId; } public String getParentSpanId() { return parentSpanId; } public void setParentSpanId(String parentSpanId) { this.parentSpanId = parentSpanId; } public String getFollowsFromSpanId() { return followsFromSpanId; } public void setFollowsFromSpanId(String followsFromSpanId) { this.followsFromSpanId = followsFromSpanId; } public String getOperator() { return operator; } public void setOperator(String operator) { this.operator = operator; } public String getOpType() { return opType; } public void setOpType(String opType) { this.opType = opType; } public String getOpName() { return opName; } public void setOpName(String opName) { this.opName = opName; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } public Date getHappenedTime() { return happenedTime; } public void setHappenedTime(Date happenedTime) { this.happenedTime = happenedTime; } } ================================================ FILE: apollo-audit/apollo-audit-api/src/main/java/com/ctrip/framework/apollo/audit/dto/ApolloAuditLogDataInfluenceDTO.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.audit.dto; import java.util.Date; public class ApolloAuditLogDataInfluenceDTO { private long id; private String spanId; private String influenceEntityName; private String influenceEntityId; private String fieldName; private String fieldOldValue; private String fieldNewValue; private Date happenedTime; public long getId() { return id; } public void setId(long id) { this.id = id; } public String getSpanId() { return spanId; } public void setSpanId(String spanId) { this.spanId = spanId; } public String getInfluenceEntityName() { return influenceEntityName; } public void setInfluenceEntityName(String influenceEntityName) { this.influenceEntityName = influenceEntityName; } public String getInfluenceEntityId() { return influenceEntityId; } public void setInfluenceEntityId(String influenceEntityId) { this.influenceEntityId = influenceEntityId; } public String getFieldName() { return fieldName; } public void setFieldName(String fieldName) { this.fieldName = fieldName; } public String getFieldOldValue() { return fieldOldValue; } public void setFieldOldValue(String fieldOldValue) { this.fieldOldValue = fieldOldValue; } public String getFieldNewValue() { return fieldNewValue; } public void setFieldNewValue(String fieldNewValue) { this.fieldNewValue = fieldNewValue; } public Date getHappenedTime() { return happenedTime; } public void setHappenedTime(Date happenedTime) { this.happenedTime = happenedTime; } } ================================================ FILE: apollo-audit/apollo-audit-api/src/main/java/com/ctrip/framework/apollo/audit/dto/ApolloAuditLogDetailsDTO.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.audit.dto; import java.util.List; /** * A combine of a log and its data influences */ public class ApolloAuditLogDetailsDTO { private ApolloAuditLogDTO logDTO; private List dataInfluenceDTOList; public ApolloAuditLogDetailsDTO(ApolloAuditLogDTO logDTO, List dataInfluenceDTOList) { this.logDTO = logDTO; this.dataInfluenceDTOList = dataInfluenceDTOList; } public ApolloAuditLogDetailsDTO() {} public ApolloAuditLogDTO getLogDTO() { return logDTO; } public void setLogDTO(ApolloAuditLogDTO logDTO) { this.logDTO = logDTO; } public List getDataInfluenceDTOList() { return dataInfluenceDTOList; } public void setDataInfluenceDTOList(List dataInfluenceDTOList) { this.dataInfluenceDTOList = dataInfluenceDTOList; } } ================================================ FILE: apollo-audit/apollo-audit-api/src/main/java/com/ctrip/framework/apollo/audit/event/ApolloAuditLogDataInfluenceEvent.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.audit.event; public class ApolloAuditLogDataInfluenceEvent { private Class beanDefinition; private Object entity; public ApolloAuditLogDataInfluenceEvent(Class beanDefinition, Object entity) { this.beanDefinition = beanDefinition; this.entity = entity; } public Class getBeanDefinition() { return beanDefinition; } public void setBeanDefinition(Class beanDefinition) { this.beanDefinition = beanDefinition; } public Object getEntity() { return entity; } public void setEntity(Object entity) { this.entity = entity; } } ================================================ FILE: apollo-audit/apollo-audit-impl/pom.xml ================================================ apollo-audit com.ctrip.framework.apollo ${revision} 4.0.0 apollo-audit-impl ${revision} com.ctrip.framework.apollo apollo-audit-annotation com.ctrip.framework.apollo apollo-audit-api org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-data-jpa org.springframework.security spring-security-core ================================================ FILE: apollo-audit/apollo-audit-impl/src/main/java/com/ctrip/framework/apollo/audit/ApolloAuditProperties.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.audit; import org.springframework.boot.context.properties.ConfigurationProperties; @ConfigurationProperties(prefix = "apollo.audit.log") public class ApolloAuditProperties { private boolean enabled = false; public boolean isEnabled() { return enabled; } public void setEnabled(boolean enabled) { this.enabled = enabled; } } ================================================ FILE: apollo-audit/apollo-audit-impl/src/main/java/com/ctrip/framework/apollo/audit/ApolloAuditRegistrar.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.audit; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.boot.autoconfigure.AutoConfigurationPackages; import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; import org.springframework.core.type.AnnotationMetadata; public class ApolloAuditRegistrar implements ImportBeanDefinitionRegistrar { @Override public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { AutoConfigurationPackages.register(registry, "com.ctrip.framework.apollo.audit.entity", "com.ctrip.framework.apollo.audit.repository"); } } ================================================ FILE: apollo-audit/apollo-audit-impl/src/main/java/com/ctrip/framework/apollo/audit/aop/ApolloAuditSpanAspect.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.audit.aop; import com.ctrip.framework.apollo.audit.annotation.ApolloAuditLog; import com.ctrip.framework.apollo.audit.annotation.ApolloAuditLogDataInfluence; import com.ctrip.framework.apollo.audit.annotation.ApolloAuditLogDataInfluenceTable; import com.ctrip.framework.apollo.audit.annotation.ApolloAuditLogDataInfluenceTableField; import com.ctrip.framework.apollo.audit.api.ApolloAuditLogApi; import com.ctrip.framework.apollo.audit.constants.ApolloAuditConstants; import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.util.Collection; import java.util.Objects; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.Signature; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.cglib.core.ReflectUtils; @Aspect public class ApolloAuditSpanAspect { private final ApolloAuditLogApi api; public ApolloAuditSpanAspect(ApolloAuditLogApi api) { this.api = api; } @Pointcut("@annotation(auditLog)") public void setAuditSpan(ApolloAuditLog auditLog) {} @Around(value = "setAuditSpan(auditLog)") public Object around(ProceedingJoinPoint pjp, ApolloAuditLog auditLog) throws Throwable { String opName = auditLog.name(); try ( AutoCloseable scope = api.appendAuditLog(auditLog.type(), opName, auditLog.description())) { Object proceed = pjp.proceed(); auditDataInfluenceArg(pjp); return proceed; } } void auditDataInfluenceArg(ProceedingJoinPoint pjp) { Method method = findMethod(pjp); if (Objects.isNull(method)) { return; } Object[] args = pjp.getArgs(); for (int i = 0; i < args.length; i++) { Object arg = args[i]; Annotation[] annotations = method.getParameterAnnotations()[i]; boolean needAudit = false; String entityName = null; String fieldName = null; for (Annotation annotation : annotations) { if (annotation instanceof ApolloAuditLogDataInfluence) { needAudit = true; } if (annotation instanceof ApolloAuditLogDataInfluenceTable) { entityName = ((ApolloAuditLogDataInfluenceTable) annotation).tableName(); } if (annotation instanceof ApolloAuditLogDataInfluenceTableField) { fieldName = ((ApolloAuditLogDataInfluenceTableField) annotation).fieldName(); } } if (needAudit) { parseArgAndAppend(entityName, fieldName, arg); } } } Method findMethod(ProceedingJoinPoint pjp) { Class clazz = pjp.getTarget().getClass(); Signature pjpSignature = pjp.getSignature(); String methodName = pjp.getSignature().getName(); Class[] parameterTypes = null; if (pjpSignature instanceof MethodSignature) { parameterTypes = ((MethodSignature) pjpSignature).getParameterTypes(); } try { return ReflectUtils.findDeclaredMethod(clazz, methodName, parameterTypes); } catch (NoSuchMethodException e) { return null; } } void parseArgAndAppend(String entityName, String fieldName, Object arg) { if (entityName == null || fieldName == null || arg == null) { return; } if (arg instanceof Collection) { for (Object o : (Collection) arg) { String matchedValue = String.valueOf(o); api.appendDataInfluence(entityName, ApolloAuditConstants.ANY_MATCHED_ID, fieldName, matchedValue); } } else { String matchedValue = String.valueOf(arg); api.appendDataInfluence(entityName, ApolloAuditConstants.ANY_MATCHED_ID, fieldName, matchedValue); } } } ================================================ FILE: apollo-audit/apollo-audit-impl/src/main/java/com/ctrip/framework/apollo/audit/component/ApolloAuditHttpInterceptor.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.audit.component; import com.ctrip.framework.apollo.audit.context.ApolloAuditTraceContext; import java.io.IOException; import org.springframework.http.HttpRequest; import org.springframework.http.client.ClientHttpRequestExecution; import org.springframework.http.client.ClientHttpRequestInterceptor; import org.springframework.http.client.ClientHttpResponse; public class ApolloAuditHttpInterceptor implements ClientHttpRequestInterceptor { private final ApolloAuditTraceContext traceContext; public ApolloAuditHttpInterceptor(ApolloAuditTraceContext traceContext) { this.traceContext = traceContext; } @Override public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { if (traceContext.tracer() != null) { request = traceContext.tracer().inject(request); } return execution.execute(request, body); } } ================================================ FILE: apollo-audit/apollo-audit-impl/src/main/java/com/ctrip/framework/apollo/audit/component/ApolloAuditLogApiJpaImpl.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.audit.component; import com.ctrip.framework.apollo.audit.annotation.ApolloAuditLogDataInfluenceTableField; import com.ctrip.framework.apollo.audit.annotation.OpType; import com.ctrip.framework.apollo.audit.api.ApolloAuditLogApi; import com.ctrip.framework.apollo.audit.context.ApolloAuditScope; import com.ctrip.framework.apollo.audit.context.ApolloAuditTraceContext; import com.ctrip.framework.apollo.audit.context.ApolloAuditTracer; import com.ctrip.framework.apollo.audit.dto.ApolloAuditLogDTO; import com.ctrip.framework.apollo.audit.dto.ApolloAuditLogDataInfluenceDTO; import com.ctrip.framework.apollo.audit.dto.ApolloAuditLogDetailsDTO; import com.ctrip.framework.apollo.audit.entity.ApolloAuditLogDataInfluence; import com.ctrip.framework.apollo.audit.service.ApolloAuditLogDataInfluenceService; import com.ctrip.framework.apollo.audit.service.ApolloAuditLogService; import com.ctrip.framework.apollo.audit.util.ApolloAuditUtil; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Objects; public class ApolloAuditLogApiJpaImpl implements ApolloAuditLogApi { private final ApolloAuditLogService logService; private final ApolloAuditLogDataInfluenceService dataInfluenceService; private final ApolloAuditTraceContext traceContext; public ApolloAuditLogApiJpaImpl(ApolloAuditLogService logService, ApolloAuditLogDataInfluenceService dataInfluenceService, ApolloAuditTraceContext traceContext) { this.logService = logService; this.dataInfluenceService = dataInfluenceService; this.traceContext = traceContext; } @Override public AutoCloseable appendAuditLog(OpType type, String name) { return appendAuditLog(type, name, "no description"); } @Override public AutoCloseable appendAuditLog(OpType type, String name, String description) { ApolloAuditTracer tracer = traceContext.tracer(); if (Objects.isNull(tracer)) { return () -> { }; } ApolloAuditScope scope = tracer.startActiveSpan(type, name, description); logService.logSpan(scope.activeSpan()); return scope; } @Override public void appendDataInfluence(String entityName, String entityId, String fieldName, String fieldCurrentValue) { // might be if (traceContext.tracer() == null) { return; } if (traceContext.tracer().getActiveSpan() == null) { return; } String spanId = traceContext.tracer().getActiveSpan().spanId(); OpType type = traceContext.tracer().getActiveSpan().getOpType(); ApolloAuditLogDataInfluence.Builder builder = ApolloAuditLogDataInfluence.builder() .spanId(spanId).entityName(entityName).entityId(entityId).fieldName(fieldName); if (type == null) { return; } switch (type) { case CREATE: case UPDATE: builder.newVal(fieldCurrentValue); break; case DELETE: builder.oldVal(fieldCurrentValue); } dataInfluenceService.save(builder.build()); } @Override public void appendDataInfluences(List entities, Class beanDefinition) { String tableName = ApolloAuditUtil.getApolloAuditLogTableName(beanDefinition); if (Objects.isNull(tableName) || Objects.equals(tableName, "")) { return; } List dataInfluenceFields = ApolloAuditUtil .getAnnotatedFields(ApolloAuditLogDataInfluenceTableField.class, beanDefinition); Field idField = ApolloAuditUtil.getPersistenceIdFieldByAnnotation(beanDefinition); entities.forEach(e -> { try { idField.setAccessible(true); String tableId = idField.get(e).toString(); for (Field f : dataInfluenceFields) { f.setAccessible(true); String val = String.valueOf(f.get(e)); String fieldName = f.getAnnotation(ApolloAuditLogDataInfluenceTableField.class).fieldName(); appendDataInfluence(tableName, tableId, fieldName, val); } } catch (IllegalAccessException ex) { throw new IllegalArgumentException("failed append data influence, " + "might due to wrong beanDefinition for entity audited", ex); } }); } @Override public List queryLogs(int page, int size) { return ApolloAuditUtil.logListToDTOList(logService.findAll(page, size)); } @Override public List queryLogsByOpName(String opName, Date startDate, Date endDate, int page, int size) { if (startDate == null && endDate == null) { return ApolloAuditUtil.logListToDTOList(logService.findByOpName(opName, page, size)); } return ApolloAuditUtil .logListToDTOList(logService.findByOpNameAndTime(opName, startDate, endDate, page, size)); } @Override public List queryTraceDetails(String traceId) { List detailsDTOList = new ArrayList<>(); logService.findByTraceId(traceId).forEach(log -> { detailsDTOList.add(new ApolloAuditLogDetailsDTO(ApolloAuditUtil.logToDTO(log), ApolloAuditUtil .dataInfluenceListToDTOList(dataInfluenceService.findBySpanId(log.getSpanId())))); }); return detailsDTOList; } @Override public List queryDataInfluencesByField(String entityName, String entityId, String fieldName, int page, int size) { return ApolloAuditUtil.dataInfluenceListToDTOList(dataInfluenceService .findByEntityNameAndEntityIdAndFieldName(entityName, entityId, fieldName, page, size)); } @Override public List searchLogByNameOrTypeOrOperator(String query, int page, int size) { return ApolloAuditUtil .logListToDTOList(logService.searchLogByNameOrTypeOrOperator(query, page, size)); } } ================================================ FILE: apollo-audit/apollo-audit-impl/src/main/java/com/ctrip/framework/apollo/audit/component/ApolloAuditLogApiNoOpImpl.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.audit.component; import com.ctrip.framework.apollo.audit.annotation.OpType; import com.ctrip.framework.apollo.audit.api.ApolloAuditLogApi; import com.ctrip.framework.apollo.audit.dto.ApolloAuditLogDataInfluenceDTO; import com.ctrip.framework.apollo.audit.dto.ApolloAuditLogDetailsDTO; import com.ctrip.framework.apollo.audit.dto.ApolloAuditLogDTO; import java.util.Collections; import java.util.Date; import java.util.List; public class ApolloAuditLogApiNoOpImpl implements ApolloAuditLogApi { // do nothing, for default impl @Override public AutoCloseable appendAuditLog(OpType type, String name) { return appendAuditLog(type, name, null); } @Override public AutoCloseable appendAuditLog(OpType type, String name, String description) { return () -> { }; } @Override public void appendDataInfluence(String entityName, String entityId, String fieldName, String fieldCurrentValue) {} @Override public void appendDataInfluences(List entities, Class beanDefinition) {} @Override public List queryLogs(int page, int size) { return Collections.emptyList(); } @Override public List queryLogsByOpName(String opName, Date startDate, Date endDate, int page, int size) { return Collections.emptyList(); } @Override public List queryTraceDetails(String traceId) { return Collections.emptyList(); } @Override public List queryDataInfluencesByField(String entityName, String entityId, String fieldName, int page, int size) { return Collections.emptyList(); } @Override public List searchLogByNameOrTypeOrOperator(String query, int page, int size) { return Collections.emptyList(); } } ================================================ FILE: apollo-audit/apollo-audit-impl/src/main/java/com/ctrip/framework/apollo/audit/constants/ApolloAuditConstants.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.audit.constants; public interface ApolloAuditConstants { String TRACE_ID = "Apollo-Audit-TraceId"; String SPAN_ID = "Apollo-Audit-SpanId"; String OPERATOR = "Apollo-Audit-Operator"; String PARENT_ID = "Apollo-Audit-ParentId"; String FOLLOWS_FROM_ID = "Apollo-Audit-FollowsFromId"; String TRACER = "Apollo-Audit-Tracer"; String ANY_MATCHED_ID = "AnyMatched"; } ================================================ FILE: apollo-audit/apollo-audit-impl/src/main/java/com/ctrip/framework/apollo/audit/context/ApolloAuditScope.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.audit.context; public class ApolloAuditScope implements AutoCloseable { private final ApolloAuditScopeManager manager; private ApolloAuditSpan activeSpan; private ApolloAuditScope hangUp; private String lastSpanId; public ApolloAuditScope(ApolloAuditSpan activeSpan, ApolloAuditScopeManager manager) { this.hangUp = manager.getScope(); this.activeSpan = activeSpan; this.manager = manager; this.lastSpanId = null; } public ApolloAuditSpan activeSpan() { return this.activeSpan; } @Override public void close() { // closing span become parent-scope's last span if (hangUp != null) { hangUp.setLastSpanId(activeSpan().spanId()); } this.manager.setScope(hangUp); } public String getLastSpanId() { return lastSpanId; } public void setLastSpanId(String lastSpanId) { this.lastSpanId = lastSpanId; } } ================================================ FILE: apollo-audit/apollo-audit-impl/src/main/java/com/ctrip/framework/apollo/audit/context/ApolloAuditScopeManager.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.audit.context; public class ApolloAuditScopeManager { private ApolloAuditScope scope; public ApolloAuditScopeManager() {} public ApolloAuditScope activate(ApolloAuditSpan span) { setScope(new ApolloAuditScope(span, this)); return getScope(); } public void deactivate() { getScope().close(); } public ApolloAuditSpan activeSpan() { return getScope() == null ? null : getScope().activeSpan(); } public ApolloAuditScope getScope() { return scope; } public void setScope(ApolloAuditScope scope) { this.scope = scope; } } ================================================ FILE: apollo-audit/apollo-audit-impl/src/main/java/com/ctrip/framework/apollo/audit/context/ApolloAuditSpan.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.audit.context; import com.ctrip.framework.apollo.audit.annotation.OpType; import java.util.Date; public class ApolloAuditSpan { private OpType opType; private String opName; private String description; private Date startTime; private Date endTime; private ApolloAuditSpanContext context; public ApolloAuditSpanContext context() { return this.context; } // just do nothing public void finish() { endTime = new Date(); } public void log() {} // sugar method public String spanId() { return context.getSpanId(); } public String operator() { return context.getOperator(); } public String traceId() { return context.getTraceId(); } public String parentId() { return context.getParentId(); } public String followsFromId() { return context.getFollowsFromId(); } public OpType getOpType() { return opType; } public void setOpType(OpType opType) { this.opType = opType; } public String getOpName() { return opName; } public void setOpName(String opName) { this.opName = opName; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } public ApolloAuditSpanContext getContext() { return context; } public void setContext(ApolloAuditSpanContext context) { this.context = context; } public Date getStartTime() { return startTime; } public void setStartTime(Date startTime) { this.startTime = startTime; } public Date getEndTime() { return endTime; } public void setEndTime(Date endTime) { this.endTime = endTime; } } ================================================ FILE: apollo-audit/apollo-audit-impl/src/main/java/com/ctrip/framework/apollo/audit/context/ApolloAuditSpanContext.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.audit.context; public class ApolloAuditSpanContext { private String traceId; private String spanId; private String operator; private String parentId; private String followsFromId; public ApolloAuditSpanContext(String traceId, String spanId) { this.traceId = traceId; this.spanId = spanId; } public ApolloAuditSpanContext(String traceId, String spanId, String operator, String parentId, String followsFromId) { this.traceId = traceId; this.spanId = spanId; this.operator = operator; this.parentId = parentId; this.followsFromId = followsFromId; } public String getTraceId() { return traceId; } public void setTraceId(String traceId) { this.traceId = traceId; } public String getSpanId() { return spanId; } public void setSpanId(String spanId) { this.spanId = spanId; } public String getOperator() { return operator; } public void setOperator(String operator) { this.operator = operator; } public String getParentId() { return parentId; } public void setParentId(String parentId) { this.parentId = parentId; } public String getFollowsFromId() { return followsFromId; } public void setFollowsFromId(String followsFromId) { this.followsFromId = followsFromId; } } ================================================ FILE: apollo-audit/apollo-audit-impl/src/main/java/com/ctrip/framework/apollo/audit/context/ApolloAuditTraceContext.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.audit.context; import com.ctrip.framework.apollo.audit.constants.ApolloAuditConstants; import com.ctrip.framework.apollo.audit.spi.ApolloAuditOperatorSupplier; import java.util.Objects; import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.RequestContextHolder; public class ApolloAuditTraceContext { private final ApolloAuditOperatorSupplier operatorSupplier; public ApolloAuditTraceContext(ApolloAuditOperatorSupplier operatorSupplier) { this.operatorSupplier = operatorSupplier; } // if not get one, create one and re-get it public ApolloAuditTracer tracer() { RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); if (requestAttributes != null) { Object tracer = requestAttributes.getAttribute(ApolloAuditConstants.TRACER, RequestAttributes.SCOPE_REQUEST); if (tracer != null) { return ((ApolloAuditTracer) tracer); } else { ApolloAuditTracer newTracer = new ApolloAuditTracer(new ApolloAuditScopeManager(), operatorSupplier); setTracer(newTracer); return newTracer; } } return null; } void setTracer(ApolloAuditTracer tracer) { if (Objects.nonNull(RequestContextHolder.getRequestAttributes())) { RequestContextHolder.getRequestAttributes().setAttribute(ApolloAuditConstants.TRACER, tracer, RequestAttributes.SCOPE_REQUEST); } } } ================================================ FILE: apollo-audit/apollo-audit-impl/src/main/java/com/ctrip/framework/apollo/audit/context/ApolloAuditTracer.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.audit.context; import com.ctrip.framework.apollo.audit.annotation.OpType; import com.ctrip.framework.apollo.audit.constants.ApolloAuditConstants; import com.ctrip.framework.apollo.audit.spi.ApolloAuditOperatorSupplier; import com.ctrip.framework.apollo.audit.util.ApolloAuditUtil; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import jakarta.servlet.http.HttpServletRequest; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpRequest; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; public class ApolloAuditTracer { private final ApolloAuditScopeManager manager; private final ApolloAuditOperatorSupplier operatorSupplier; public ApolloAuditTracer(ApolloAuditScopeManager manager, ApolloAuditOperatorSupplier operatorSupplier) { this.manager = manager; this.operatorSupplier = operatorSupplier; } protected ApolloAuditScopeManager scopeManager() { return manager; } public HttpRequest inject(HttpRequest request) { Map> map = new HashMap<>(); if (manager.activeSpan() == null) { return request; } map.put(ApolloAuditConstants.TRACE_ID, Collections.singletonList(manager.activeSpan().traceId())); map.put(ApolloAuditConstants.SPAN_ID, Collections.singletonList(manager.activeSpan().spanId())); map.put(ApolloAuditConstants.OPERATOR, Collections.singletonList(manager.activeSpan().operator())); map.put(ApolloAuditConstants.PARENT_ID, Collections.singletonList(manager.activeSpan().parentId())); map.put(ApolloAuditConstants.FOLLOWS_FROM_ID, Collections.singletonList(manager.activeSpan().followsFromId())); HttpHeaders headers = request.getHeaders(); headers.putAll(map); return request; } public ApolloAuditSpan startSpan(OpType type, String name, String description) { ApolloAuditSpan activeSpan = getActiveSpan(); AuditSpanBuilder builder = new AuditSpanBuilder(type, name); builder = builder.description(description); builder = activeSpan == null ? builder.asRootSpan(operatorSupplier.getOperator()) : builder.asChildOf(activeSpan); String followsFromId = scopeManager().getScope() == null ? null : scopeManager().getScope().getLastSpanId(); builder = builder.followsFrom(followsFromId); return builder.build(); } public ApolloAuditScope startActiveSpan(OpType type, String name, String description) { ApolloAuditSpan startSpan = startSpan(type, name, description); return activate(startSpan); } public ApolloAuditScope activate(ApolloAuditSpan span) { return scopeManager().activate(span); } private ApolloAuditSpan getActiveSpanFromHttp() { ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); if (servletRequestAttributes == null) { return null; } HttpServletRequest request = servletRequestAttributes.getRequest(); String traceId = request.getHeader(ApolloAuditConstants.TRACE_ID); String spanId = request.getHeader(ApolloAuditConstants.SPAN_ID); String operator = request.getHeader(ApolloAuditConstants.OPERATOR); String parentId = request.getHeader(ApolloAuditConstants.PARENT_ID); String followsFromId = request.getHeader(ApolloAuditConstants.FOLLOWS_FROM_ID); if (Objects.isNull(traceId)) { return null; } else { ApolloAuditSpanContext context = new ApolloAuditSpanContext(traceId, spanId, operator, parentId, followsFromId); return spanBuilder(null, null).regenerateByContext(context); } } private ApolloAuditSpan getActiveSpanFromContext() { return scopeManager().activeSpan(); } public ApolloAuditSpan getActiveSpan() { ApolloAuditSpan activeSpan = getActiveSpanFromContext(); if (activeSpan != null) { return activeSpan; } activeSpan = getActiveSpanFromHttp(); // might be null, root span generate should be done in other place return activeSpan; } public AuditSpanBuilder spanBuilder(OpType type, String name) { return new AuditSpanBuilder(type, name); } public static class AuditSpanBuilder { private final OpType opType; private final String opName; private String spanId; private String traceId; private String operator; private String parentId; private String followsFromId; private String description; public AuditSpanBuilder(OpType type, String name) { opType = type; opName = name; } public AuditSpanBuilder asChildOf(ApolloAuditSpan parent) { traceId = parent.traceId(); operator = parent.operator(); parentId = parent.spanId(); return this; } public AuditSpanBuilder asRootSpan(String operator) { traceId = ApolloAuditUtil.generateId(); this.operator = operator; return this; } public AuditSpanBuilder followsFrom(String id) { this.followsFromId = id; return this; } public AuditSpanBuilder description(String val) { this.description = val; return this; } public ApolloAuditSpan regenerateByContext(ApolloAuditSpanContext val) { ApolloAuditSpan span = new ApolloAuditSpan(); span.setContext(val); return span; } public ApolloAuditSpan build() { ApolloAuditSpan span = new ApolloAuditSpan(); spanId = ApolloAuditUtil.generateId(); ApolloAuditSpanContext context = new ApolloAuditSpanContext(traceId, spanId, operator, parentId, followsFromId); span.setContext(context); span.setDescription(description); span.setOpName(opName); span.setOpType(opType); span.setStartTime(new Date()); return span; } } } ================================================ FILE: apollo-audit/apollo-audit-impl/src/main/java/com/ctrip/framework/apollo/audit/controller/ApolloAuditController.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.audit.controller; import com.ctrip.framework.apollo.audit.ApolloAuditProperties; import com.ctrip.framework.apollo.audit.api.ApolloAuditLogApi; import com.ctrip.framework.apollo.audit.dto.ApolloAuditLogDTO; import com.ctrip.framework.apollo.audit.dto.ApolloAuditLogDataInfluenceDTO; import com.ctrip.framework.apollo.audit.dto.ApolloAuditLogDetailsDTO; import java.util.Date; import java.util.List; import org.springframework.format.annotation.DateTimeFormat; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; /** * About page: index from 0, default size is 10 * * @author luke */ @RestController @RequestMapping("/apollo/audit") public class ApolloAuditController { private final ApolloAuditLogApi api; private final ApolloAuditProperties properties; public ApolloAuditController(ApolloAuditLogApi api, ApolloAuditProperties properties) { this.api = api; this.properties = properties; } @GetMapping("/properties") public ApolloAuditProperties getProperties() { return properties; } @GetMapping("/logs") @PreAuthorize(value = "@apolloAuditLogQueryApiPreAuthorizer.hasQueryPermission()") public List findAllAuditLogs(int page, int size) { return api.queryLogs(page, size); } @GetMapping("/trace") @PreAuthorize(value = "@apolloAuditLogQueryApiPreAuthorizer.hasQueryPermission()") public List findTraceDetails(@RequestParam String traceId) { return api.queryTraceDetails(traceId); } @GetMapping("/logs/opName") @PreAuthorize(value = "@apolloAuditLogQueryApiPreAuthorizer.hasQueryPermission()") public List findAllAuditLogsByOpNameAndTime(@RequestParam String opName, @RequestParam int page, @RequestParam int size, @RequestParam(value = "startDate", required = false) @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss.S") Date startDate, @RequestParam(value = "endDate", required = false) @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss.S") Date endDate) { return api.queryLogsByOpName(opName, startDate, endDate, page, size); } @GetMapping("/logs/dataInfluences/field") @PreAuthorize(value = "@apolloAuditLogQueryApiPreAuthorizer.hasQueryPermission()") public List findDataInfluencesByField( @RequestParam String entityName, @RequestParam String entityId, @RequestParam String fieldName, int page, int size) { return api.queryDataInfluencesByField(entityName, entityId, fieldName, page, size); } @GetMapping("/logs/by-name-or-type-or-operator") @PreAuthorize(value = "@apolloAuditLogQueryApiPreAuthorizer.hasQueryPermission()") public List findAuditLogsByNameOrTypeOrOperator(@RequestParam String query, int page, int size) { return api.searchLogByNameOrTypeOrOperator(query, page, size); } } ================================================ FILE: apollo-audit/apollo-audit-impl/src/main/java/com/ctrip/framework/apollo/audit/entity/ApolloAuditLog.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.audit.entity; import java.util.Date; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.Table; @Entity @Table(name = "`AuditLog`") public class ApolloAuditLog extends BaseEntity { @Column(name = "TraceId", nullable = false) private String traceId; @Column(name = "SpanId", nullable = false) private String spanId; @Column(name = "ParentSpanId", nullable = true) private String parentSpanId; @Column(name = "FollowsFromSpanId", nullable = true) private String followsFromSpanId; @Column(name = "Operator", nullable = true) private String operator; @Column(name = "OpType", nullable = true) private String opType; @Column(name = "OpName", nullable = true) private String opName; @Column(name = "Description", nullable = true) private String description; public static Builder builder() { return new Builder(); } public String getTraceId() { return traceId; } public void setTraceId(String traceId) { this.traceId = traceId; } public String getSpanId() { return spanId; } public void setSpanId(String spanId) { this.spanId = spanId; } public String getParentSpanId() { return parentSpanId; } public void setParentSpanId(String parentSpanId) { this.parentSpanId = parentSpanId; } public String getFollowsFromSpanId() { return followsFromSpanId; } public void setFollowsFromSpanId(String followsFromSpanId) { this.followsFromSpanId = followsFromSpanId; } public String getOperator() { return operator; } public void setOperator(String operator) { this.operator = operator; } public String getOpType() { return opType; } public void setOpType(String opType) { this.opType = opType; } public String getOpName() { return opName; } public void setOpName(String opName) { this.opName = opName; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } public static class Builder { ApolloAuditLog auditLog = new ApolloAuditLog(); public Builder() {} public Builder traceId(String val) { auditLog.setTraceId(val); return this; } public Builder spanId(String val) { auditLog.setSpanId(val); return this; } public Builder parentSpanId(String val) { auditLog.setParentSpanId(val); return this; } public Builder followsFromSpanId(String val) { auditLog.setFollowsFromSpanId(val); return this; } public Builder operator(String val) { auditLog.setOperator(val); return this; } public Builder opType(String val) { auditLog.setOpType(val); return this; } public Builder opName(String val) { auditLog.setOpName(val); return this; } public Builder description(String val) { auditLog.setDescription(val); return this; } public Builder happenedTime(Date val) { auditLog.setDataChangeCreatedTime(val); return this; } public ApolloAuditLog build() { return auditLog; } } } ================================================ FILE: apollo-audit/apollo-audit-impl/src/main/java/com/ctrip/framework/apollo/audit/entity/ApolloAuditLogDataInfluence.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.audit.entity; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.Table; @Entity @Table(name = "`AuditLogDataInfluence`") public class ApolloAuditLogDataInfluence extends BaseEntity { @Column(name = "SpanId", nullable = false) private String spanId; @Column(name = "InfluenceEntityName", nullable = false) private String influenceEntityName; @Column(name = "InfluenceEntityId", nullable = false) private String influenceEntityId; @Column(name = "FieldName") private String fieldName; @Column(name = "FieldOldValue") private String fieldOldValue; @Column(name = "FieldNewValue") private String fieldNewValue; public ApolloAuditLogDataInfluence() {} public ApolloAuditLogDataInfluence(String spanId, String entityName, String entityId, String fieldName, String oldVal, String newVal) { this.spanId = spanId; this.influenceEntityName = entityName; this.influenceEntityId = entityId; this.fieldName = fieldName; this.fieldOldValue = oldVal; this.fieldNewValue = newVal; } public static Builder builder() { return new Builder(); } public String getSpanId() { return spanId; } public void setSpanId(String spanId) { this.spanId = spanId; } public String getInfluenceEntityName() { return influenceEntityName; } public void setInfluenceEntityName(String influenceEntityName) { this.influenceEntityName = influenceEntityName; } public String getInfluenceEntityId() { return influenceEntityId; } public void setInfluenceEntityId(String influenceEntityId) { this.influenceEntityId = influenceEntityId; } public String getFieldName() { return fieldName; } public void setFieldName(String fieldName) { this.fieldName = fieldName; } public String getFieldOldValue() { return fieldOldValue; } public void setFieldOldValue(String fieldOldValue) { this.fieldOldValue = fieldOldValue; } public String getFieldNewValue() { return fieldNewValue; } public void setFieldNewValue(String fieldNewValue) { this.fieldNewValue = fieldNewValue; } public static class Builder { ApolloAuditLogDataInfluence influence = new ApolloAuditLogDataInfluence(); public Builder() {} public Builder spanId(String val) { influence.setSpanId(val); return this; } public Builder entityId(String val) { influence.setInfluenceEntityId(val); return this; } public Builder entityName(String val) { influence.setInfluenceEntityName(val); return this; } public Builder fieldName(String val) { influence.setFieldName(val); return this; } public Builder oldVal(String val) { influence.setFieldOldValue(val); return this; } public Builder newVal(String val) { influence.setFieldNewValue(val); return this; } public ApolloAuditLogDataInfluence build() { return influence; } } } ================================================ FILE: apollo-audit/apollo-audit-impl/src/main/java/com/ctrip/framework/apollo/audit/entity/BaseEntity.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.audit.entity; import java.util.Date; import jakarta.persistence.Column; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.MappedSuperclass; import jakarta.persistence.PrePersist; import jakarta.persistence.PreRemove; import jakarta.persistence.PreUpdate; @MappedSuperclass public abstract class BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "`Id`") private long id; @Column(name = "`IsDeleted`", columnDefinition = "Bit default '0'") protected boolean isDeleted = false; @Column(name = "`DeletedAt`", columnDefinition = "Bigint default '0'") protected long deletedAt; @Column(name = "`DataChange_CreatedBy`") private String dataChangeCreatedBy; @Column(name = "`DataChange_CreatedTime`") private Date dataChangeCreatedTime; @Column(name = "`DataChange_LastModifiedBy`") private String dataChangeLastModifiedBy; @Column(name = "`DataChange_LastTime`") private Date dataChangeLastModifiedTime; public long getId() { return id; } public void setId(long id) { this.id = id; } public boolean isDeleted() { return isDeleted; } public void setDeleted(boolean deleted) { isDeleted = deleted; if (deleted && this.deletedAt == 0) { // also set deletedAt value as epoch millisecond this.deletedAt = System.currentTimeMillis(); } else if (!deleted) { this.deletedAt = 0L; } } public long getDeletedAt() { return deletedAt; } public String getDataChangeCreatedBy() { return dataChangeCreatedBy; } public void setDataChangeCreatedBy(String dataChangeCreatedBy) { this.dataChangeCreatedBy = dataChangeCreatedBy; } public Date getDataChangeCreatedTime() { return dataChangeCreatedTime; } public void setDataChangeCreatedTime(Date dataChangeCreatedTime) { this.dataChangeCreatedTime = dataChangeCreatedTime; } public String getDataChangeLastModifiedBy() { return dataChangeLastModifiedBy; } public void setDataChangeLastModifiedBy(String dataChangeLastModifiedBy) { this.dataChangeLastModifiedBy = dataChangeLastModifiedBy; } public Date getDataChangeLastModifiedTime() { return dataChangeLastModifiedTime; } public void setDataChangeLastModifiedTime(Date dataChangeLastModifiedTime) { this.dataChangeLastModifiedTime = dataChangeLastModifiedTime; } @PrePersist protected void prePersist() { if (this.dataChangeCreatedTime == null) { dataChangeCreatedTime = new Date(); } if (this.dataChangeLastModifiedTime == null) { dataChangeLastModifiedTime = new Date(); } } @PreUpdate protected void preUpdate() { this.dataChangeLastModifiedTime = new Date(); } @PreRemove protected void preRemove() { this.dataChangeLastModifiedTime = new Date(); } } ================================================ FILE: apollo-audit/apollo-audit-impl/src/main/java/com/ctrip/framework/apollo/audit/listener/ApolloAuditLogDataInfluenceEventListener.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.audit.listener; import com.ctrip.framework.apollo.audit.api.ApolloAuditLogApi; import com.ctrip.framework.apollo.audit.event.ApolloAuditLogDataInfluenceEvent; import java.util.Collections; import org.springframework.context.event.EventListener; public class ApolloAuditLogDataInfluenceEventListener { private final ApolloAuditLogApi api; public ApolloAuditLogDataInfluenceEventListener(ApolloAuditLogApi api) { this.api = api; } @EventListener public void handleEvent(ApolloAuditLogDataInfluenceEvent event) { Object e = event.getEntity(); api.appendDataInfluences(Collections.singletonList(e), event.getBeanDefinition()); } } ================================================ FILE: apollo-audit/apollo-audit-impl/src/main/java/com/ctrip/framework/apollo/audit/repository/ApolloAuditLogDataInfluenceRepository.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.audit.repository; import com.ctrip.framework.apollo.audit.entity.ApolloAuditLogDataInfluence; import java.util.List; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; public interface ApolloAuditLogDataInfluenceRepository extends JpaRepository { List findBySpanId(String spanId); List findByInfluenceEntityNameAndInfluenceEntityId( String influenceEntityName, String influenceEntityId, Pageable page); List findByInfluenceEntityNameAndInfluenceEntityIdAndFieldName( String influenceEntityName, String influenceEntityId, String fieldName, Pageable page); } ================================================ FILE: apollo-audit/apollo-audit-impl/src/main/java/com/ctrip/framework/apollo/audit/repository/ApolloAuditLogRepository.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.audit.repository; import com.ctrip.framework.apollo.audit.entity.ApolloAuditLog; import java.util.Date; import java.util.List; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; public interface ApolloAuditLogRepository extends JpaRepository { List findByTraceIdOrderByDataChangeCreatedTimeDesc(String traceId); List findByOpName(String opName, Pageable page); List findByOpNameAndDataChangeCreatedTimeGreaterThanEqualAndDataChangeCreatedTimeLessThanEqual( String opName, Date startDate, Date endDate, Pageable pageable); List findByOpNameContainingOrOpTypeContainingOrOperatorContaining(String opName, String opType, String operator, Pageable pageable); } ================================================ FILE: apollo-audit/apollo-audit-impl/src/main/java/com/ctrip/framework/apollo/audit/service/ApolloAuditLogDataInfluenceService.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.audit.service; import com.ctrip.framework.apollo.audit.entity.ApolloAuditLogDataInfluence; import com.ctrip.framework.apollo.audit.repository.ApolloAuditLogDataInfluenceRepository; import java.util.List; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort.Direction; import org.springframework.data.domain.Sort.Order; public class ApolloAuditLogDataInfluenceService { private final ApolloAuditLogDataInfluenceRepository dataInfluenceRepository; public ApolloAuditLogDataInfluenceService( ApolloAuditLogDataInfluenceRepository dataInfluenceRepository) { this.dataInfluenceRepository = dataInfluenceRepository; } public ApolloAuditLogDataInfluence save(ApolloAuditLogDataInfluence dataInfluence) { return dataInfluenceRepository.save(dataInfluence); } public List findBySpanId(String spanId) { return dataInfluenceRepository.findBySpanId(spanId); } public List findByEntityNameAndEntityIdAndFieldName( String entityName, String entityId, String fieldName, int page, int size) { Pageable pageable = pageSortByTime(page, size); return dataInfluenceRepository.findByInfluenceEntityNameAndInfluenceEntityIdAndFieldName( entityName, entityId, fieldName, pageable); } Pageable pageSortByTime(int page, int size) { return PageRequest.of(page, size, Sort.by(new Order(Direction.DESC, "DataChangeCreatedTime"))); } } ================================================ FILE: apollo-audit/apollo-audit-impl/src/main/java/com/ctrip/framework/apollo/audit/service/ApolloAuditLogService.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.audit.service; import com.ctrip.framework.apollo.audit.context.ApolloAuditSpan; import com.ctrip.framework.apollo.audit.entity.ApolloAuditLog; import com.ctrip.framework.apollo.audit.repository.ApolloAuditLogRepository; import java.util.Date; import java.util.List; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort.Direction; import org.springframework.data.domain.Sort.Order; public class ApolloAuditLogService { private final ApolloAuditLogRepository logRepository; public ApolloAuditLogService(ApolloAuditLogRepository logRepository) { this.logRepository = logRepository; } public ApolloAuditLog save(ApolloAuditLog auditLog) { return logRepository.save(auditLog); } public void logSpan(ApolloAuditSpan span) { ApolloAuditLog auditLog = ApolloAuditLog.builder().traceId(span.traceId()).spanId(span.spanId()) .parentSpanId(span.parentId()).followsFromSpanId(span.followsFromId()) .operator(span.operator() != null ? span.operator() : "anonymous").opName(span.getOpName()) .opType(span.getOpType().toString()).description(span.getDescription()) .happenedTime(new Date()).build(); logRepository.save(auditLog); } public List findByTraceId(String traceId) { return logRepository.findByTraceIdOrderByDataChangeCreatedTimeDesc(traceId); } public List findAll(int page, int size) { Pageable pageable = pageSortByTime(page, size); return logRepository.findAll(pageable).getContent(); } public List findByOpName(String opName, int page, int size) { Pageable pageable = pageSortByTime(page, size); return logRepository.findByOpName(opName, pageable); } public List findByOpNameAndTime(String opName, Date startDate, Date endDate, int page, int size) { Pageable pageable = pageSortByTime(page, size); return logRepository .findByOpNameAndDataChangeCreatedTimeGreaterThanEqualAndDataChangeCreatedTimeLessThanEqual( opName, startDate, endDate, pageable); } public List searchLogByNameOrTypeOrOperator(String query, int page, int size) { Pageable pageable = pageSortByTime(page, size); return logRepository.findByOpNameContainingOrOpTypeContainingOrOperatorContaining(query, query, query, pageable); } Pageable pageSortByTime(int page, int size) { return PageRequest.of(page, size, Sort.by(new Order(Direction.DESC, "dataChangeCreatedTime"))); } } ================================================ FILE: apollo-audit/apollo-audit-impl/src/main/java/com/ctrip/framework/apollo/audit/spi/ApolloAuditLogQueryApiPreAuthorizer.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.audit.spi; public interface ApolloAuditLogQueryApiPreAuthorizer { boolean hasQueryPermission(); } ================================================ FILE: apollo-audit/apollo-audit-impl/src/main/java/com/ctrip/framework/apollo/audit/spi/ApolloAuditOperatorSupplier.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.audit.spi; public interface ApolloAuditOperatorSupplier { String getOperator(); } ================================================ FILE: apollo-audit/apollo-audit-impl/src/main/java/com/ctrip/framework/apollo/audit/spi/defaultimpl/ApolloAuditLogQueryApiDefaultPreAuthorizer.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.audit.spi.defaultimpl; import com.ctrip.framework.apollo.audit.spi.ApolloAuditLogQueryApiPreAuthorizer; public class ApolloAuditLogQueryApiDefaultPreAuthorizer implements ApolloAuditLogQueryApiPreAuthorizer { @Override public boolean hasQueryPermission() { return true; } } ================================================ FILE: apollo-audit/apollo-audit-impl/src/main/java/com/ctrip/framework/apollo/audit/spi/defaultimpl/ApolloAuditOperatorDefaultSupplier.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.audit.spi.defaultimpl; import com.ctrip.framework.apollo.audit.constants.ApolloAuditConstants; import com.ctrip.framework.apollo.audit.context.ApolloAuditSpan; import com.ctrip.framework.apollo.audit.context.ApolloAuditTracer; import com.ctrip.framework.apollo.audit.spi.ApolloAuditOperatorSupplier; import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.RequestContextHolder; public class ApolloAuditOperatorDefaultSupplier implements ApolloAuditOperatorSupplier { @Override public String getOperator() { RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); if (requestAttributes != null) { Object tracer = requestAttributes.getAttribute(ApolloAuditConstants.TRACER, RequestAttributes.SCOPE_REQUEST); if (tracer != null) { ApolloAuditSpan activeSpan = ((ApolloAuditTracer) tracer).getActiveSpan(); return activeSpan != null ? activeSpan.operator() : "anonymous"; } else { return null; } } return null; } } ================================================ FILE: apollo-audit/apollo-audit-impl/src/main/java/com/ctrip/framework/apollo/audit/util/ApolloAuditUtil.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.audit.util; import com.ctrip.framework.apollo.audit.annotation.ApolloAuditLogDataInfluenceTable; import com.ctrip.framework.apollo.audit.dto.ApolloAuditLogDTO; import com.ctrip.framework.apollo.audit.dto.ApolloAuditLogDataInfluenceDTO; import com.ctrip.framework.apollo.audit.entity.ApolloAuditLog; import com.ctrip.framework.apollo.audit.entity.ApolloAuditLogDataInfluence; import java.lang.annotation.Annotation; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.UUID; import java.util.stream.Collectors; import jakarta.persistence.Id; public class ApolloAuditUtil { public static String generateId() { return UUID.randomUUID().toString().replaceAll("-", ""); } public static List getAnnotatedFields(Class annoClass, Class clazz) { return Arrays.stream(clazz.getDeclaredFields()) .filter(field -> field.isAnnotationPresent(annoClass)).collect(Collectors.toList()); } public static List toList(Object obj) { if (obj instanceof Collection) { Collection collection = (Collection) obj; return new ArrayList<>(collection); } else { return Collections.singletonList(obj); } } public static String getApolloAuditLogTableName(Class clazz) { return clazz.isAnnotationPresent(ApolloAuditLogDataInfluenceTable.class) ? clazz.getAnnotation(ApolloAuditLogDataInfluenceTable.class).tableName() : null; } public static Field getPersistenceIdFieldByAnnotation(Class clazz) { while (clazz != null) { Field[] fields = clazz.getDeclaredFields(); for (Field field : fields) { if (field.isAnnotationPresent(Id.class)) { field.setAccessible(true); return field; } } clazz = clazz.getSuperclass(); } return null; } public static ApolloAuditLogDTO logToDTO(ApolloAuditLog auditLog) { ApolloAuditLogDTO dto = new ApolloAuditLogDTO(); dto.setId(auditLog.getId()); dto.setOpType(auditLog.getOpType()); dto.setOpName(auditLog.getOpName()); dto.setDescription(auditLog.getDescription()); dto.setOperator(auditLog.getOperator()); dto.setHappenedTime(auditLog.getDataChangeCreatedTime()); dto.setSpanId(auditLog.getSpanId()); dto.setTraceId(auditLog.getTraceId()); dto.setFollowsFromSpanId(auditLog.getFollowsFromSpanId()); dto.setParentSpanId(auditLog.getParentSpanId()); return dto; } public static ApolloAuditLogDataInfluenceDTO dataInfluenceToDTO( ApolloAuditLogDataInfluence dataInfluence) { ApolloAuditLogDataInfluenceDTO dto = new ApolloAuditLogDataInfluenceDTO(); dto.setId(dataInfluence.getId()); dto.setInfluenceEntityName(dataInfluence.getInfluenceEntityName()); dto.setInfluenceEntityId(dataInfluence.getInfluenceEntityId()); dto.setFieldName(dataInfluence.getFieldName()); dto.setFieldOldValue(dataInfluence.getFieldOldValue()); dto.setFieldNewValue(dataInfluence.getFieldNewValue()); dto.setHappenedTime(dataInfluence.getDataChangeCreatedTime()); dto.setSpanId(dataInfluence.getSpanId()); return dto; } public static List logListToDTOList(List logList) { List logDTOList = new ArrayList<>(); logList.forEach(log -> { logDTOList.add(logToDTO(log)); }); return logDTOList; } public static List dataInfluenceListToDTOList( List dataInfluenceList) { List dataInfluenceDTOList = new ArrayList<>(); dataInfluenceList.forEach(dataInfluence -> { dataInfluenceDTOList.add(dataInfluenceToDTO(dataInfluence)); }); return dataInfluenceDTOList; } } ================================================ FILE: apollo-audit/apollo-audit-impl/src/test/java/com/ctrip/framework/apollo/audit/MockBeanFactory.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.audit; import com.ctrip.framework.apollo.audit.dto.ApolloAuditLogDTO; import com.ctrip.framework.apollo.audit.dto.ApolloAuditLogDataInfluenceDTO; import com.ctrip.framework.apollo.audit.dto.ApolloAuditLogDetailsDTO; import com.ctrip.framework.apollo.audit.entity.ApolloAuditLog; import com.ctrip.framework.apollo.audit.entity.ApolloAuditLogDataInfluence; import java.util.ArrayList; import java.util.List; public class MockBeanFactory { public static ApolloAuditLogDTO mockAuditLogDTO() { return new ApolloAuditLogDTO(); } public static List mockAuditLogDTOListByLength(int length) { List mockList = new ArrayList<>(); for (int i = 0; i < length; i++) { mockList.add(mockAuditLogDTO()); } return mockList; } public static ApolloAuditLog mockAuditLog() { return new ApolloAuditLog(); } public static List mockAuditLogListByLength(int length) { List mockList = new ArrayList<>(); for (int i = 0; i < length; i++) { mockList.add(mockAuditLog()); } return mockList; } public static List mockTraceDetailsDTOListByLength(int length) { List mockList = new ArrayList<>(); for (int i = 0; i < length; i++) { ApolloAuditLogDetailsDTO dto = new ApolloAuditLogDetailsDTO(); dto.setLogDTO(mockAuditLogDTO()); mockList.add(dto); } return mockList; } public static ApolloAuditLogDataInfluenceDTO mockDataInfluenceDTO() { return new ApolloAuditLogDataInfluenceDTO(); } public static List mockDataInfluenceDTOListByLength(int length) { List mockList = new ArrayList<>(); for (int i = 0; i < length; i++) { mockList.add(mockDataInfluenceDTO()); } return mockList; } public static ApolloAuditLogDataInfluence mockDataInfluence() { return new ApolloAuditLogDataInfluence(); } public static List mockDataInfluenceListByLength(int length) { List mockList = new ArrayList<>(); for (int i = 0; i < length; i++) { mockList.add(mockDataInfluence()); } return mockList; } public static MockDataInfluenceEntity mockDataInfluenceEntity() { return new MockDataInfluenceEntity(); } public static List mockDataInfluenceEntityListByLength(int length) { List mockList = new ArrayList<>(); for (int i = 0; i < length; i++) { MockDataInfluenceEntity e = mockDataInfluenceEntity(); e.setId(i + 1); mockList.add(e); } return mockList; } } ================================================ FILE: apollo-audit/apollo-audit-impl/src/test/java/com/ctrip/framework/apollo/audit/MockDataInfluenceEntity.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.audit; import com.ctrip.framework.apollo.audit.annotation.ApolloAuditLogDataInfluenceTable; import com.ctrip.framework.apollo.audit.annotation.ApolloAuditLogDataInfluenceTableField; import jakarta.persistence.Id; @ApolloAuditLogDataInfluenceTable(tableName = "MockTableName") public class MockDataInfluenceEntity { @Id private long id; @ApolloAuditLogDataInfluenceTableField(fieldName = "MarkedAttribute") private String markedAttribute; private String unMarkedAttribute; private boolean isDeleted; public long getId() { return id; } public void setId(long id) { this.id = id; } public String getMarkedAttribute() { return markedAttribute; } public void setMarkedAttribute(String markedAttribute) { this.markedAttribute = markedAttribute; } public String getUnMarkedAttribute() { return unMarkedAttribute; } public void setUnMarkedAttribute(String unMarkedAttribute) { this.unMarkedAttribute = unMarkedAttribute; } public boolean isDeleted() { return isDeleted; } public void setDeleted(boolean deleted) { isDeleted = deleted; } } ================================================ FILE: apollo-audit/apollo-audit-impl/src/test/java/com/ctrip/framework/apollo/audit/aop/ApolloAuditSpanAspectTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.audit.aop; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import com.ctrip.framework.apollo.audit.annotation.ApolloAuditLog; import com.ctrip.framework.apollo.audit.annotation.ApolloAuditLogDataInfluence; import com.ctrip.framework.apollo.audit.annotation.ApolloAuditLogDataInfluenceTable; import com.ctrip.framework.apollo.audit.annotation.ApolloAuditLogDataInfluenceTableField; import com.ctrip.framework.apollo.audit.annotation.OpType; import com.ctrip.framework.apollo.audit.api.ApolloAuditLogApi; import com.ctrip.framework.apollo.audit.constants.ApolloAuditConstants; import java.lang.reflect.Method; import java.util.Arrays; import java.util.List; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.reflect.MethodSignature; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.test.context.ContextConfiguration; @SpringBootTest @ContextConfiguration(classes = ApolloAuditSpanAspect.class) public class ApolloAuditSpanAspectTest { @SpyBean ApolloAuditSpanAspect aspect; @MockBean ApolloAuditLogApi api; @Test public void testAround() throws Throwable { final OpType opType = OpType.CREATE; final String opName = "App.create"; final String description = "no description"; ProceedingJoinPoint mockPJP = mock(ProceedingJoinPoint.class); ApolloAuditLog mockAnnotation = mock(ApolloAuditLog.class); AutoCloseable mockScope = mock(AutoCloseable.class); { when(mockAnnotation.type()).thenReturn(opType); when(mockAnnotation.name()).thenReturn(opName); when(mockAnnotation.description()).thenReturn(description); when(api.appendAuditLog(eq(opType), eq(opName), eq(description))).thenReturn(mockScope); doNothing().when(aspect).auditDataInfluenceArg(mockPJP); } aspect.around(mockPJP, mockAnnotation); verify(api, times(1)).appendAuditLog(eq(opType), eq(opName), eq(description)); verify(mockScope, times(1)).close(); verify(aspect, times(1)).auditDataInfluenceArg(eq(mockPJP)); } @Test public void testAuditDataInfluenceArg() throws NoSuchMethodException { ProceedingJoinPoint mockPJP = mock(ProceedingJoinPoint.class); Object[] args = new Object[] {new Object(), new Object()}; Method method = MockAuditClass.class.getMethod("mockAuditMethod", Object.class, Object.class); { doReturn(method).when(aspect).findMethod(any()); when(mockPJP.getArgs()).thenReturn(args); } aspect.auditDataInfluenceArg(mockPJP); verify(aspect, times(1)).parseArgAndAppend(eq("App"), eq("Name"), eq(args[0])); } @Test public void testAuditDataInfluenceArgCaseFindMethodReturnNull() throws NoSuchMethodException { ProceedingJoinPoint mockPJP = mock(ProceedingJoinPoint.class); Object[] args = new Object[] {new Object(), new Object()}; { doReturn(null).when(aspect).findMethod(any()); when(mockPJP.getArgs()).thenReturn(args); } aspect.auditDataInfluenceArg(mockPJP); verify(aspect, times(0)).parseArgAndAppend(eq("App"), eq("Name"), eq(args[0])); } @Test public void testFindMethod() throws NoSuchMethodException { ProceedingJoinPoint mockPJP = mock(ProceedingJoinPoint.class); MockAuditClass mockAuditClass = new MockAuditClass(); MethodSignature signature = mock(MethodSignature.class); Method method = MockAuditClass.class.getMethod("mockAuditMethod", Object.class, Object.class); Method sameNameMethod = MockAuditClass.class.getMethod("mockAuditMethod", Object.class); { when(mockPJP.getTarget()).thenReturn(mockAuditClass); when(mockPJP.getSignature()).thenReturn(signature); when(signature.getName()).thenReturn("mockAuditMethod"); when(signature.getParameterTypes()).thenReturn(new Class[] {Object.class, Object.class}); } Method methodFounded = aspect.findMethod(mockPJP); assertEquals(method, methodFounded); assertNotEquals(sameNameMethod, methodFounded); } @Test public void testParseArgAndAppendCaseNullName() { Object somewhat = new Object(); aspect.parseArgAndAppend(null, null, somewhat); verify(api, times(0)).appendDataInfluence(any(), any(), any(), any()); } @Test public void testParseArgAndAppendCaseCollectionTypeArg() { final String entityName = "App"; final String fieldName = "Name"; List list = Arrays.asList(new Object(), new Object(), new Object()); { doNothing().when(api).appendDataInfluence(any(), any(), any(), any()); } aspect.parseArgAndAppend(entityName, fieldName, list); verify(api, times(list.size())).appendDataInfluence(eq(entityName), eq(ApolloAuditConstants.ANY_MATCHED_ID), eq(fieldName), any()); } @Test public void testParseArgAndAppendCaseNormalTypeArg() { final String entityName = "App"; final String fieldName = "Name"; Object arg = new Object(); { doNothing().when(api).appendDataInfluence(any(), any(), any(), any()); } aspect.parseArgAndAppend(entityName, fieldName, arg); verify(api, times(1)).appendDataInfluence(eq(entityName), eq(ApolloAuditConstants.ANY_MATCHED_ID), eq(fieldName), any()); } public class MockAuditClass { public void mockAuditMethod( @ApolloAuditLogDataInfluence @ApolloAuditLogDataInfluenceTable(tableName = "App") @ApolloAuditLogDataInfluenceTableField(fieldName = "Name") Object val1, Object val2) {} // same name method test public void mockAuditMethod(Object val2) {} } } ================================================ FILE: apollo-audit/apollo-audit-impl/src/test/java/com/ctrip/framework/apollo/audit/component/ApolloAuditHttpInterceptorTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.audit.component; import com.ctrip.framework.apollo.audit.context.ApolloAuditTraceContext; import com.ctrip.framework.apollo.audit.context.ApolloAuditTracer; import java.io.IOException; import org.junit.jupiter.api.Test; import org.mockito.Mockito; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.http.HttpRequest; import org.springframework.http.client.ClientHttpRequestExecution; import org.springframework.test.context.ContextConfiguration; @SpringBootTest @ContextConfiguration(classes = ApolloAuditHttpInterceptor.class) public class ApolloAuditHttpInterceptorTest { @SpyBean ApolloAuditHttpInterceptor interceptor; @MockBean ApolloAuditTraceContext traceContext; @Test public void testInterceptor() throws IOException { ClientHttpRequestExecution execution = Mockito.mock(ClientHttpRequestExecution.class); HttpRequest request = Mockito.mock(HttpRequest.class); byte[] body = new byte[] {}; ApolloAuditTracer tracer = Mockito.mock(ApolloAuditTracer.class); HttpRequest mockInjected = Mockito.mock(HttpRequest.class); Mockito.when(traceContext.tracer()).thenReturn(tracer); Mockito.when(tracer.inject(Mockito.eq(request))).thenReturn(mockInjected); interceptor.intercept(request, body, execution); Mockito.verify(execution, Mockito.times(1)).execute(Mockito.eq(mockInjected), Mockito.eq(body)); } @Test public void testInterceptorCaseNoTracer() throws IOException { ClientHttpRequestExecution execution = Mockito.mock(ClientHttpRequestExecution.class); HttpRequest request = Mockito.mock(HttpRequest.class); byte[] body = new byte[] {}; Mockito.when(traceContext.tracer()).thenReturn(null); interceptor.intercept(request, body, execution); Mockito.verify(execution, Mockito.times(1)).execute(Mockito.eq(request), Mockito.eq(body)); } } ================================================ FILE: apollo-audit/apollo-audit-impl/src/test/java/com/ctrip/framework/apollo/audit/component/ApolloAuditLogApiJpaImplTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.audit.component; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import com.ctrip.framework.apollo.audit.MockBeanFactory; import com.ctrip.framework.apollo.audit.MockDataInfluenceEntity; import com.ctrip.framework.apollo.audit.annotation.OpType; import com.ctrip.framework.apollo.audit.context.ApolloAuditScope; import com.ctrip.framework.apollo.audit.context.ApolloAuditScopeManager; import com.ctrip.framework.apollo.audit.context.ApolloAuditSpan; import com.ctrip.framework.apollo.audit.context.ApolloAuditSpanContext; import com.ctrip.framework.apollo.audit.context.ApolloAuditTraceContext; import com.ctrip.framework.apollo.audit.context.ApolloAuditTracer; import com.ctrip.framework.apollo.audit.dto.ApolloAuditLogDTO; import com.ctrip.framework.apollo.audit.dto.ApolloAuditLogDataInfluenceDTO; import com.ctrip.framework.apollo.audit.dto.ApolloAuditLogDetailsDTO; import com.ctrip.framework.apollo.audit.entity.ApolloAuditLog; import com.ctrip.framework.apollo.audit.entity.ApolloAuditLogDataInfluence; import com.ctrip.framework.apollo.audit.service.ApolloAuditLogDataInfluenceService; import com.ctrip.framework.apollo.audit.service.ApolloAuditLogService; import java.util.ArrayList; import java.util.Date; import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mockito; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.test.context.ContextConfiguration; @SpringBootTest @ContextConfiguration(classes = ApolloAuditLogApiJpaImpl.class) public class ApolloAuditLogApiJpaImplTest { // record api final OpType create = OpType.CREATE; final OpType delete = OpType.DELETE; final String opName = "test.create"; final String traceId = "test-trace-id"; final String spanId = "test-span-id"; final String entityId = "1"; final String entityName = "App"; final String fieldName = "name"; final String fieldCurrentValue = "xxx"; final int entityNum = 3; // query api final int page = 0; final int size = 10; @SpyBean ApolloAuditLogApiJpaImpl api; @MockBean ApolloAuditLogService logService; @MockBean ApolloAuditLogDataInfluenceService dataInfluenceService; @MockBean ApolloAuditTraceContext traceContext; @MockBean ApolloAuditTracer tracer; @Captor private ArgumentCaptor influenceCaptor; @BeforeEach void beforeEach() { Mockito.reset(traceContext, tracer); Mockito.when(traceContext.tracer()).thenReturn(tracer); } @Test public void testAppendAuditLog() { final String description = "no description"; { ApolloAuditSpan activeSpan = new ApolloAuditSpan(); activeSpan.setOpType(create); activeSpan.setOpName(opName); activeSpan.setContext(new ApolloAuditSpanContext(traceId, spanId)); ApolloAuditScopeManager manager = new ApolloAuditScopeManager(); ApolloAuditScope scope = new ApolloAuditScope(activeSpan, manager); Mockito.when( tracer.startActiveSpan(Mockito.eq(create), Mockito.eq(opName), Mockito.eq(description))) .thenReturn(scope); } ApolloAuditScope scope = (ApolloAuditScope) api.appendAuditLog(create, opName); Mockito.verify(traceContext, Mockito.times(1)).tracer(); Mockito.verify(tracer, Mockito.times(1)).startActiveSpan(Mockito.eq(create), Mockito.eq(opName), Mockito.eq(description)); assertEquals(create, scope.activeSpan().getOpType()); assertEquals(opName, scope.activeSpan().getOpName()); assertEquals(traceId, scope.activeSpan().traceId()); assertEquals(spanId, scope.activeSpan().spanId()); } @Test public void testAppendDataInfluenceCaseCreateOrUpdate() { { ApolloAuditSpan span = Mockito.mock(ApolloAuditSpan.class); Mockito.when(tracer.getActiveSpan()).thenReturn(span); Mockito.when(span.spanId()).thenReturn(spanId); Mockito.when(span.getOpType()).thenReturn(create); } api.appendDataInfluence(entityName, entityId, fieldName, fieldCurrentValue); Mockito.verify(dataInfluenceService, Mockito.times(1)).save(influenceCaptor.capture()); ApolloAuditLogDataInfluence capturedInfluence = influenceCaptor.getValue(); assertEquals(entityId, capturedInfluence.getInfluenceEntityId()); assertEquals(entityName, capturedInfluence.getInfluenceEntityName()); assertEquals(fieldName, capturedInfluence.getFieldName()); assertNull(capturedInfluence.getFieldOldValue()); assertEquals(fieldCurrentValue, capturedInfluence.getFieldNewValue()); assertEquals(spanId, capturedInfluence.getSpanId()); } @Test public void testAppendDataInfluenceCaseDelete() { { ApolloAuditSpan span = Mockito.mock(ApolloAuditSpan.class); Mockito.when(tracer.getActiveSpan()).thenReturn(span); Mockito.when(span.spanId()).thenReturn(spanId); Mockito.when(span.getOpType()).thenReturn(delete); } api.appendDataInfluence(entityName, entityId, fieldName, fieldCurrentValue); Mockito.verify(dataInfluenceService, Mockito.times(1)).save(influenceCaptor.capture()); ApolloAuditLogDataInfluence capturedInfluence = influenceCaptor.getValue(); assertEquals(entityId, capturedInfluence.getInfluenceEntityId()); assertEquals(entityName, capturedInfluence.getInfluenceEntityName()); assertEquals(fieldName, capturedInfluence.getFieldName()); assertEquals(fieldCurrentValue, capturedInfluence.getFieldOldValue()); assertNull(capturedInfluence.getFieldNewValue()); assertEquals(spanId, capturedInfluence.getSpanId()); } @Test public void testAppendDataInfluenceCaseTracerIsNull() { Mockito.when(traceContext.tracer()).thenReturn(null); api.appendDataInfluence(entityName, entityId, fieldName, fieldCurrentValue); Mockito.verify(traceContext, Mockito.times(1)).tracer(); } @Test public void testAppendDataInfluenceCaseActiveSpanIsNull() { Mockito.when(tracer.getActiveSpan()).thenReturn(null); api.appendDataInfluence(entityName, entityId, fieldName, fieldCurrentValue); Mockito.verify(traceContext, Mockito.times(2)).tracer(); Mockito.verify(tracer, Mockito.times(1)).getActiveSpan(); } @Test public void testAppendDataInfluences() { List entities = MockBeanFactory.mockDataInfluenceEntityListByLength(entityNum); api.appendDataInfluences(entities, MockDataInfluenceEntity.class); Mockito.verify(api, Mockito.times(entityNum)).appendDataInfluence(Mockito.eq("MockTableName"), Mockito.any(), Mockito.eq("MarkedAttribute"), Mockito.any()); } @Test public void testAppendDataInfluencesCaseWrongBeanDefinition() { List entities = new ArrayList<>(); entities.add(new Object()); assertThrows(IllegalArgumentException.class, () -> { api.appendDataInfluences(entities, MockDataInfluenceEntity.class); }); } @Test public void testAppendDataInfluencesCaseIncompleteConditions() { List entities = new ArrayList<>(entityNum); api.appendDataInfluences(entities, Object.class); Mockito.verify(api, Mockito.times(0)).appendDataInfluence(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()); } @Test public void testQueryLogs() { { List logList = MockBeanFactory.mockAuditLogListByLength(size); Mockito.when(logService.findAll(Mockito.eq(page), Mockito.eq(size))).thenReturn(logList); } List dtoList = api.queryLogs(page, size); Mockito.verify(logService, Mockito.times(1)).findAll(Mockito.eq(page), Mockito.eq(size)); assertEquals(size, dtoList.size()); } @Test public void testQueryLogsByOpNameCaseDateIsNull() { final String opName = "query-op-name"; final Date startDate = null; final Date endDate = null; { List logList = MockBeanFactory.mockAuditLogListByLength(size); Mockito.when(logService.findByOpName(Mockito.eq(opName), Mockito.eq(page), Mockito.eq(size))) .thenReturn(logList); } List dtoList = api.queryLogsByOpName(opName, startDate, endDate, page, size); Mockito.verify(logService, Mockito.times(1)).findByOpName(Mockito.eq(opName), Mockito.eq(page), Mockito.eq(size)); assertEquals(size, dtoList.size()); } @Test public void testQueryLogsByOpName() { final String opName = "query-op-name"; final Date startDate = new Date(); final Date endDate = new Date(); { List logList = MockBeanFactory.mockAuditLogListByLength(size); Mockito.when(logService.findByOpNameAndTime(Mockito.eq(opName), Mockito.eq(startDate), Mockito.eq(endDate), Mockito.eq(page), Mockito.eq(size))).thenReturn(logList); } List dtoList = api.queryLogsByOpName(opName, startDate, endDate, page, size); Mockito.verify(logService, Mockito.times(1)).findByOpNameAndTime(Mockito.eq(opName), Mockito.eq(startDate), Mockito.eq(endDate), Mockito.eq(page), Mockito.eq(size)); assertEquals(size, dtoList.size()); } @Test public void testQueryTraceDetails() { final String traceId = "query-trace-id"; final int traceDetailsLength = 3; final int dataInfluenceOfEachLog = 3; { List logList = MockBeanFactory.mockAuditLogListByLength(traceDetailsLength); Mockito.when(logService.findByTraceId(Mockito.eq(traceId))).thenReturn(logList); List dataInfluenceList = MockBeanFactory.mockDataInfluenceListByLength(dataInfluenceOfEachLog); Mockito.when(dataInfluenceService.findBySpanId(Mockito.any())).thenReturn(dataInfluenceList); } List detailsDTOList = api.queryTraceDetails(traceId); Mockito.verify(logService, Mockito.times(1)).findByTraceId(Mockito.eq(traceId)); Mockito.verify(dataInfluenceService, Mockito.times(3)).findBySpanId(Mockito.any()); assertEquals(traceDetailsLength, detailsDTOList.size()); assertEquals(dataInfluenceOfEachLog, detailsDTOList.get(0).getDataInfluenceDTOList().size()); } @Test public void testQueryDataInfluencesByField() { final String entityName = "App"; final String entityId = "1"; final String fieldName = "xxx"; { List dataInfluenceList = MockBeanFactory.mockDataInfluenceListByLength(size); Mockito .when(dataInfluenceService.findByEntityNameAndEntityIdAndFieldName(Mockito.eq(entityName), Mockito.eq(entityId), Mockito.eq(fieldName), Mockito.eq(page), Mockito.eq(size))) .thenReturn(dataInfluenceList); } List dtoList = api.queryDataInfluencesByField(entityName, entityId, fieldName, page, size); Mockito.verify(dataInfluenceService, Mockito.times(1)).findByEntityNameAndEntityIdAndFieldName( Mockito.eq(entityName), Mockito.eq(entityId), Mockito.eq(fieldName), Mockito.eq(page), Mockito.eq(size)); assertEquals(size, dtoList.size()); } } ================================================ FILE: apollo-audit/apollo-audit-impl/src/test/java/com/ctrip/framework/apollo/audit/component/ApolloAuditScopeManagerTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.audit.component; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import com.ctrip.framework.apollo.audit.context.ApolloAuditScope; import com.ctrip.framework.apollo.audit.context.ApolloAuditScopeManager; import com.ctrip.framework.apollo.audit.context.ApolloAuditSpan; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.test.context.ContextConfiguration; @SpringBootTest @ContextConfiguration(classes = ApolloAuditScopeManager.class) public class ApolloAuditScopeManagerTest { @SpyBean ApolloAuditScopeManager manager; @Test public void testActivate() { ApolloAuditSpan mockSpan = mock(ApolloAuditSpan.class); ApolloAuditScope activeScope = manager.activate(mockSpan); verify(manager).setScope(eq(activeScope)); assertEquals(mockSpan, activeScope.activeSpan()); } } ================================================ FILE: apollo-audit/apollo-audit-impl/src/test/java/com/ctrip/framework/apollo/audit/context/ApolloAuditTraceContextTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.audit.context; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import com.ctrip.framework.apollo.audit.constants.ApolloAuditConstants; import com.ctrip.framework.apollo.audit.spi.ApolloAuditOperatorSupplier; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executors; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mockito; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.test.context.ContextConfiguration; import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.RequestContextHolder; @SpringBootTest @ContextConfiguration(classes = ApolloAuditTraceContext.class) public class ApolloAuditTraceContextTest { @SpyBean ApolloAuditTraceContext traceContext; @MockBean ApolloAuditOperatorSupplier supplier; @BeforeEach public void beforeEach() { // will be null of each unit-test begin RequestContextHolder.resetRequestAttributes(); Mockito.reset(traceContext); } @Test public void testGetTracerNotInRequestThread() { ApolloAuditTracer get = traceContext.tracer(); assertNull(get); } @Test public void testGetTracerCaseNoTracerExistsInRequestThreads() { RequestAttributes mockRequestAttributes = Mockito.mock(RequestAttributes.class); RequestContextHolder.setRequestAttributes(mockRequestAttributes); ApolloAuditTracer get = traceContext.tracer(); assertNotNull(get); Mockito.verify(traceContext, Mockito.times(1)).setTracer(Mockito.any(ApolloAuditTracer.class)); } @Test public void testGetTracerInRequestThreads() { ApolloAuditTracer mockTracer = new ApolloAuditTracer(Mockito.mock(ApolloAuditScopeManager.class), supplier); RequestAttributes mockRequestAttributes = Mockito.mock(RequestAttributes.class); RequestContextHolder.setRequestAttributes(mockRequestAttributes); Mockito.when(mockRequestAttributes.getAttribute(Mockito.eq(ApolloAuditConstants.TRACER), Mockito.eq(RequestAttributes.SCOPE_REQUEST))).thenReturn(mockTracer); ApolloAuditTracer get = traceContext.tracer(); assertNotNull(get); Mockito.verify(traceContext, Mockito.times(0)).setTracer(Mockito.any(ApolloAuditTracer.class)); } @Test public void testGetTracerInAnotherThreadButSameRequest() { ApolloAuditTracer mockTracer = Mockito.mock(ApolloAuditTracer.class); { Mockito.when(traceContext.tracer()).thenReturn(mockTracer); } CountDownLatch latch = new CountDownLatch(1); Executors.newSingleThreadExecutor().submit(() -> { ApolloAuditTracer tracer = traceContext.tracer(); assertEquals(mockTracer, tracer); latch.countDown(); }); } @Test public void testGetTracerInAnotherRequest() { ApolloAuditTracer mockTracer = Mockito.mock(ApolloAuditTracer.class); { Mockito.when(traceContext.tracer()).thenReturn(mockTracer); } CountDownLatch latch = new CountDownLatch(1); Executors.newSingleThreadExecutor().submit(() -> { RequestContextHolder.resetRequestAttributes(); ApolloAuditTracer tracer = traceContext.tracer(); assertNotEquals(mockTracer, tracer); latch.countDown(); }); } } ================================================ FILE: apollo-audit/apollo-audit-impl/src/test/java/com/ctrip/framework/apollo/audit/context/ApolloAuditTracerTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.audit.context; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.times; import com.ctrip.framework.apollo.audit.annotation.OpType; import com.ctrip.framework.apollo.audit.constants.ApolloAuditConstants; import com.ctrip.framework.apollo.audit.spi.ApolloAuditOperatorSupplier; import jakarta.servlet.http.HttpServletRequest; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mockito; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpRequest; import org.springframework.test.context.ContextConfiguration; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; @SpringBootTest @ContextConfiguration(classes = ApolloAuditTracer.class) public class ApolloAuditTracerTest { final OpType opType = OpType.CREATE; final String opName = "create"; final String description = "description"; final String activeTraceId = "10001"; final String activeSpanId = "100010001"; final String operator = "luke"; @SpyBean ApolloAuditTracer tracer; @MockBean ApolloAuditScopeManager manager; @MockBean ApolloAuditOperatorSupplier supplier; @BeforeEach public void beforeEach() { RequestContextHolder.resetRequestAttributes(); Mockito.reset(tracer, manager, supplier); } @Test public void testInject() { HttpRequest mockRequest = Mockito.mock(HttpRequest.class); { ApolloAuditSpan activeSpan = Mockito.mock(ApolloAuditSpan.class); Mockito.when(manager.activeSpan()).thenReturn(activeSpan); Mockito.when(mockRequest.getHeaders()).thenReturn(new HttpHeaders()); } HttpRequest injected = tracer.inject(mockRequest); HttpHeaders headers = injected.getHeaders(); assertNotNull(headers.get(ApolloAuditConstants.TRACE_ID)); assertNotNull(headers.get(ApolloAuditConstants.SPAN_ID)); assertNotNull(headers.get(ApolloAuditConstants.OPERATOR)); assertNotNull(headers.get(ApolloAuditConstants.PARENT_ID)); assertNotNull(headers.get(ApolloAuditConstants.FOLLOWS_FROM_ID)); } @Test public void testInjectCaseActiveSpanIsNull() { HttpRequest mockRequest = Mockito.mock(HttpRequest.class); { Mockito.when(manager.activeSpan()).thenReturn(null); Mockito.when(mockRequest.getHeaders()).thenReturn(new HttpHeaders()); } HttpRequest injected = tracer.inject(mockRequest); HttpHeaders headers = injected.getHeaders(); assertNull(headers.get(ApolloAuditConstants.TRACE_ID)); assertNull(headers.get(ApolloAuditConstants.SPAN_ID)); assertNull(headers.get(ApolloAuditConstants.OPERATOR)); assertNull(headers.get(ApolloAuditConstants.PARENT_ID)); assertNull(headers.get(ApolloAuditConstants.FOLLOWS_FROM_ID)); } @Test public void testStartSpanCaseActiveSpanExistsAndNoFollowsFrom() { { // has parent span ApolloAuditSpan activeSpan = Mockito.mock(ApolloAuditSpan.class); Mockito.when(tracer.getActiveSpan()).thenReturn(activeSpan); Mockito.when(activeSpan.traceId()).thenReturn(activeTraceId); Mockito.when(activeSpan.spanId()).thenReturn(activeSpanId); Mockito.when(activeSpan.operator()).thenReturn(operator); // not follows from any span ApolloAuditScope mockScope = Mockito.mock(ApolloAuditScope.class); Mockito.when(manager.getScope()).thenReturn(mockScope); Mockito.when(mockScope.getLastSpanId()).thenReturn(null); } ApolloAuditSpan build = tracer.startSpan(opType, opName, description); assertEquals(opType, build.getOpType()); assertEquals(opName, build.getOpName()); assertEquals(description, build.getDescription()); assertEquals(activeTraceId, build.traceId()); assertEquals(activeSpanId, build.parentId()); assertEquals(operator, build.operator()); assertNotNull(build.spanId()); assertNull(build.followsFromId()); } @Test public void testStartSpanCaseActiveSpanExistsAndHasFollowsFrom() { final String followsFromSpanId = "100010000"; { // has parent span ApolloAuditSpan activeSpan = Mockito.mock(ApolloAuditSpan.class); Mockito.when(tracer.getActiveSpan()).thenReturn(activeSpan); Mockito.when(activeSpan.traceId()).thenReturn(activeTraceId); Mockito.when(activeSpan.spanId()).thenReturn(activeSpanId); Mockito.when(activeSpan.operator()).thenReturn(operator); // has follows from span ApolloAuditScope mockScope = Mockito.mock(ApolloAuditScope.class); Mockito.when(manager.getScope()).thenReturn(mockScope); Mockito.when(mockScope.getLastSpanId()).thenReturn(followsFromSpanId); } ApolloAuditSpan build = tracer.startSpan(opType, opName, description); assertEquals(opType, build.getOpType()); assertEquals(opName, build.getOpName()); assertEquals(description, build.getDescription()); assertEquals(activeTraceId, build.traceId()); assertEquals(activeSpanId, build.parentId()); assertEquals(operator, build.operator()); assertNotNull(build.spanId()); assertEquals(followsFromSpanId, build.followsFromId()); } @Test public void testStartSpanCaseNoActiveSpanExists() { { // no parent span Mockito.when(tracer.getActiveSpan()).thenReturn(null); // is the origin of a trace, need to get operator Mockito.when(supplier.getOperator()).thenReturn(operator); // of course no scope at this time Mockito.when(manager.getScope()).thenReturn(null); } ApolloAuditSpan build = tracer.startSpan(opType, opName, description); assertEquals(opType, build.getOpType()); assertEquals(opName, build.getOpName()); assertEquals(description, build.getDescription()); assertNotNull(build.traceId()); assertNotNull(build.spanId()); assertNull(build.parentId()); assertEquals(operator, build.operator()); assertNull(build.followsFromId()); } @Test public void testStartActiveSpan() { ApolloAuditSpan activeSpan = Mockito.mock(ApolloAuditSpan.class); { doReturn(activeSpan).when(tracer).startSpan(Mockito.eq(opType), Mockito.eq(opName), Mockito.eq(description)); } tracer.startActiveSpan(opType, opName, description); Mockito.verify(tracer, Mockito.times(1)).startSpan(Mockito.eq(opType), Mockito.eq(opName), Mockito.eq(description)); Mockito.verify(manager, times(1)).activate(Mockito.eq(activeSpan)); } @Test public void testGetActiveSpanFromContext() { ApolloAuditSpan activeSpan = Mockito.mock(ApolloAuditSpan.class); { Mockito.when(manager.activeSpan()).thenReturn(activeSpan); } ApolloAuditSpan get = tracer.getActiveSpan(); assertEquals(activeSpan, get); } @Test public void testGetActiveSpanFromHttpRequestCaseNotInRequestThread() { { // no span would be in context Mockito.when(manager.activeSpan()).thenReturn(null); // not in request thread } ApolloAuditSpan get = tracer.getActiveSpan(); assertNull(get); } @Test public void testGetActiveSpanFromHttpRequestCaseInRequestThread() { final String httpParentId = "100010002"; final String httpFollowsFromId = "100010003"; { // no span would be in context Mockito.when(manager.activeSpan()).thenReturn(null); // in request thread HttpServletRequest request = Mockito.mock(HttpServletRequest.class); RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request)); Mockito.when(request.getHeader(Mockito.eq(ApolloAuditConstants.TRACE_ID))) .thenReturn(activeTraceId); Mockito.when(request.getHeader(Mockito.eq(ApolloAuditConstants.SPAN_ID))) .thenReturn(activeSpanId); Mockito.when(request.getHeader(Mockito.eq(ApolloAuditConstants.OPERATOR))) .thenReturn(operator); Mockito.when(request.getHeader(Mockito.eq(ApolloAuditConstants.PARENT_ID))) .thenReturn(httpParentId); Mockito.when(request.getHeader(Mockito.eq(ApolloAuditConstants.FOLLOWS_FROM_ID))) .thenReturn(httpFollowsFromId); } ApolloAuditSpan get = tracer.getActiveSpan(); assertEquals(activeTraceId, get.traceId()); assertEquals(activeSpanId, get.spanId()); assertEquals(operator, get.operator()); assertEquals(httpParentId, get.parentId()); assertEquals(httpFollowsFromId, get.followsFromId()); assertNull(get.getOpType()); assertNull(get.getOpName()); } } ================================================ FILE: apollo-audit/apollo-audit-impl/src/test/java/com/ctrip/framework/apollo/audit/controller/ApolloAuditControllerTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.audit.controller; import com.ctrip.framework.apollo.audit.ApolloAuditProperties; import com.ctrip.framework.apollo.audit.MockBeanFactory; import com.ctrip.framework.apollo.audit.api.ApolloAuditLogApi; import com.ctrip.framework.apollo.audit.dto.ApolloAuditLogDTO; import com.ctrip.framework.apollo.audit.dto.ApolloAuditLogDataInfluenceDTO; import com.ctrip.framework.apollo.audit.dto.ApolloAuditLogDetailsDTO; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; import java.util.List; import org.junit.jupiter.api.Test; import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.test.web.servlet.result.MockMvcResultMatchers; @WebMvcTest @ContextConfiguration(classes = ApolloAuditController.class) public class ApolloAuditControllerTest { final int page = 0; final int size = 10; @Autowired ApolloAuditController apolloAuditController; @Autowired private MockMvc mockMvc; @MockBean private ApolloAuditLogApi api; @MockBean private ApolloAuditProperties properties; @Test public void testFindAllAuditLogs() throws Exception { { List mockLogDTOList = MockBeanFactory.mockAuditLogDTOListByLength(size); Mockito.when(api.queryLogs(Mockito.eq(page), Mockito.eq(size))).thenReturn(mockLogDTOList); } mockMvc .perform(MockMvcRequestBuilders.get("/apollo/audit/logs") .param("page", String.valueOf(page)).param("size", String.valueOf(size))) .andExpect(MockMvcResultMatchers.status().isOk()) .andExpect(MockMvcResultMatchers.jsonPath("$").isArray()) .andExpect(MockMvcResultMatchers.jsonPath("$.length()").value(size)); Mockito.verify(api, Mockito.times(1)).queryLogs(Mockito.eq(page), Mockito.eq(size)); } @Test public void testFindTraceDetails() throws Exception { final String traceId = "query-trace-id"; final int traceDetailsListLength = 3; { List mockDetailsDTOList = MockBeanFactory.mockTraceDetailsDTOListByLength(traceDetailsListLength); mockDetailsDTOList.forEach(e -> e.getLogDTO().setTraceId(traceId)); Mockito.when(api.queryTraceDetails(Mockito.eq(traceId))).thenReturn(mockDetailsDTOList); } mockMvc.perform(MockMvcRequestBuilders.get("/apollo/audit/trace").param("traceId", traceId)) .andExpect(MockMvcResultMatchers.status().isOk()) .andExpect(MockMvcResultMatchers.jsonPath("$").isArray()) .andExpect(MockMvcResultMatchers.jsonPath("$.length()").value(traceDetailsListLength)) .andExpect(MockMvcResultMatchers.jsonPath("$[0].logDTO.traceId").value(traceId)); Mockito.verify(api, Mockito.times(1)).queryTraceDetails(Mockito.eq(traceId)); } @Test public void testFindAllAuditLogsByOpNameAndTime() throws Exception { final String opName = "query-op-name"; final Date startDate = new Date(2023, Calendar.OCTOBER, 15); final Date endDate = new Date(2023, Calendar.OCTOBER, 16); SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.S"); { List mockLogDTOList = MockBeanFactory.mockAuditLogDTOListByLength(size); mockLogDTOList.forEach(e -> { e.setOpName(opName); }); Mockito.when(api.queryLogsByOpName(Mockito.eq(opName), Mockito.eq(startDate), Mockito.eq(endDate), Mockito.eq(page), Mockito.eq(size))).thenReturn(mockLogDTOList); } mockMvc .perform(MockMvcRequestBuilders.get("/apollo/audit/logs/opName").param("opName", opName) .param("startDate", sdf.format(startDate)).param("endDate", sdf.format(endDate)) .param("page", String.valueOf(page)).param("size", String.valueOf(size))) .andExpect(MockMvcResultMatchers.status().isOk()) .andExpect(MockMvcResultMatchers.jsonPath("$").isArray()) .andExpect(MockMvcResultMatchers.jsonPath("$.length()").value(size)) .andExpect(MockMvcResultMatchers.jsonPath("$.[0].opName").value(opName)); Mockito.verify(api, Mockito.times(1)).queryLogsByOpName(Mockito.eq(opName), Mockito.eq(startDate), Mockito.eq(endDate), Mockito.eq(page), Mockito.eq(size)); } @Test public void testFindDataInfluencesByField() throws Exception { final String entityName = "query-entity-name"; final String entityId = "query-entity-id"; final String fieldName = "query-field-name"; { List mockDataInfluenceDTOList = MockBeanFactory.mockDataInfluenceDTOListByLength(size); mockDataInfluenceDTOList.forEach(e -> { e.setInfluenceEntityName(entityName); e.setInfluenceEntityId(entityId); e.setFieldName(fieldName); }); Mockito .when(api.queryDataInfluencesByField(Mockito.eq(entityName), Mockito.eq(entityId), Mockito.eq(fieldName), Mockito.eq(page), Mockito.eq(size))) .thenReturn(mockDataInfluenceDTOList); } mockMvc .perform(MockMvcRequestBuilders.get("/apollo/audit/logs/dataInfluences/field") .param("entityName", entityName).param("entityId", entityId) .param("fieldName", fieldName).param("page", String.valueOf(page)) .param("size", String.valueOf(size))) .andExpect(MockMvcResultMatchers.status().isOk()) .andExpect(MockMvcResultMatchers.jsonPath("$").isArray()) .andExpect(MockMvcResultMatchers.jsonPath("$.length()").value(size)); Mockito.verify(api, Mockito.times(1)).queryDataInfluencesByField(Mockito.eq(entityName), Mockito.eq(entityId), Mockito.eq(fieldName), Mockito.eq(page), Mockito.eq(size)); } } ================================================ FILE: apollo-audit/apollo-audit-impl/src/test/java/com/ctrip/framework/apollo/audit/spi/ApolloAuditOperatorSupplierTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.audit.spi; import static org.junit.jupiter.api.Assertions.assertEquals; import com.ctrip.framework.apollo.audit.constants.ApolloAuditConstants; import com.ctrip.framework.apollo.audit.context.ApolloAuditSpan; import com.ctrip.framework.apollo.audit.context.ApolloAuditSpanContext; import com.ctrip.framework.apollo.audit.context.ApolloAuditTracer; import com.ctrip.framework.apollo.audit.spi.defaultimpl.ApolloAuditOperatorDefaultSupplier; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mockito; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.test.context.ContextConfiguration; import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.RequestContextHolder; @SpringBootTest @ContextConfiguration(classes = ApolloAuditOperatorSupplier.class) public class ApolloAuditOperatorSupplierTest { @SpyBean ApolloAuditOperatorDefaultSupplier defaultSupplier; @MockBean RequestAttributes requestAttributes; @MockBean ApolloAuditTracer tracer; @BeforeEach public void setUp() { Mockito.when(requestAttributes.getAttribute(Mockito.eq(ApolloAuditConstants.TRACER), Mockito.eq(RequestAttributes.SCOPE_REQUEST))).thenReturn(tracer); RequestContextHolder.setRequestAttributes(requestAttributes); } @Test public void testGetOperatorCaseActiveSpanExist() { final String operator = "test"; { ApolloAuditSpan activeSpan = new ApolloAuditSpan(); activeSpan.setContext(new ApolloAuditSpanContext(null, null, operator, null, null)); Mockito.when(tracer.getActiveSpan()).thenReturn(activeSpan); } assertEquals(operator, defaultSupplier.getOperator()); } @Test public void testGetOperatorCaseActiveSpanNotExist() { assertEquals("anonymous", defaultSupplier.getOperator()); } } ================================================ FILE: apollo-audit/apollo-audit-spring-boot-starter/pom.xml ================================================ apollo-audit com.ctrip.framework.apollo ${revision} 4.0.0 apollo-audit-spring-boot-starter ${revision} com.ctrip.framework.apollo apollo-audit-impl third party<--> org.springframework.boot spring-boot-autoconfigure ================================================ FILE: apollo-audit/apollo-audit-spring-boot-starter/src/main/java/com/ctrip/framework/apollo/audit/configuration/ApolloAuditAutoConfiguration.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.audit.configuration; import com.ctrip.framework.apollo.audit.ApolloAuditProperties; import com.ctrip.framework.apollo.audit.ApolloAuditRegistrar; import com.ctrip.framework.apollo.audit.aop.ApolloAuditSpanAspect; import com.ctrip.framework.apollo.audit.api.ApolloAuditLogApi; import com.ctrip.framework.apollo.audit.component.ApolloAuditHttpInterceptor; import com.ctrip.framework.apollo.audit.component.ApolloAuditLogApiJpaImpl; import com.ctrip.framework.apollo.audit.context.ApolloAuditTraceContext; import com.ctrip.framework.apollo.audit.controller.ApolloAuditController; import com.ctrip.framework.apollo.audit.listener.ApolloAuditLogDataInfluenceEventListener; import com.ctrip.framework.apollo.audit.repository.ApolloAuditLogDataInfluenceRepository; import com.ctrip.framework.apollo.audit.repository.ApolloAuditLogRepository; import com.ctrip.framework.apollo.audit.service.ApolloAuditLogDataInfluenceService; import com.ctrip.framework.apollo.audit.service.ApolloAuditLogService; import com.ctrip.framework.apollo.audit.spi.ApolloAuditLogQueryApiPreAuthorizer; import com.ctrip.framework.apollo.audit.spi.ApolloAuditOperatorSupplier; import com.ctrip.framework.apollo.audit.spi.defaultimpl.ApolloAuditLogQueryApiDefaultPreAuthorizer; import com.ctrip.framework.apollo.audit.spi.defaultimpl.ApolloAuditOperatorDefaultSupplier; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; @Configuration @EnableConfigurationProperties(ApolloAuditProperties.class) @Import(ApolloAuditRegistrar.class) @ConditionalOnProperty(prefix = "apollo.audit.log", name = "enabled", havingValue = "true") public class ApolloAuditAutoConfiguration { private static final Logger logger = LoggerFactory.getLogger(ApolloAuditAutoConfiguration.class); private final ApolloAuditProperties apolloAuditProperties; public ApolloAuditAutoConfiguration(ApolloAuditProperties apolloAuditProperties) { this.apolloAuditProperties = apolloAuditProperties; logger.info("ApolloAuditAutoConfigure initializing..."); } @Bean public ApolloAuditLogDataInfluenceService apolloAuditLogDataInfluenceService( ApolloAuditLogDataInfluenceRepository dataInfluenceRepository) { return new ApolloAuditLogDataInfluenceService(dataInfluenceRepository); } @Bean public ApolloAuditLogService apolloAuditLogService(ApolloAuditLogRepository logRepository) { return new ApolloAuditLogService(logRepository); } @Bean @ConditionalOnMissingBean(ApolloAuditOperatorSupplier.class) public ApolloAuditOperatorSupplier apolloAuditLogOperatorSupplier() { return new ApolloAuditOperatorDefaultSupplier(); } @Bean public ApolloAuditTraceContext apolloAuditTraceContext( ApolloAuditOperatorSupplier apolloAuditLogOperatorSupplier) { return new ApolloAuditTraceContext(apolloAuditLogOperatorSupplier); } @Bean public ApolloAuditLogApi apolloAuditLogApi(ApolloAuditLogService logService, ApolloAuditLogDataInfluenceService dataInfluenceService, ApolloAuditTraceContext apolloAuditTraceContext) { return new ApolloAuditLogApiJpaImpl(logService, dataInfluenceService, apolloAuditTraceContext); } @Bean public ApolloAuditSpanAspect apolloAuditSpanAspect(ApolloAuditLogApi apolloAuditLogApi) { return new ApolloAuditSpanAspect(apolloAuditLogApi); } @Bean public ApolloAuditHttpInterceptor apolloAuditHttpInterceptor( ApolloAuditTraceContext traceContext) { return new ApolloAuditHttpInterceptor(traceContext); } @Bean(name = "apolloAuditLogQueryApiPreAuthorizer") @ConditionalOnMissingBean(ApolloAuditLogQueryApiPreAuthorizer.class) public ApolloAuditLogQueryApiPreAuthorizer apolloAuditLogQueryApiPreAuthorizer() { return new ApolloAuditLogQueryApiDefaultPreAuthorizer(); } @Bean public ApolloAuditController apolloAuditController(ApolloAuditLogApi api, ApolloAuditProperties apolloAuditProperties) { return new ApolloAuditController(api, apolloAuditProperties); } @Bean public ApolloAuditLogDataInfluenceEventListener apolloAuditLogDataInfluenceEventListener( ApolloAuditLogApi api) { return new ApolloAuditLogDataInfluenceEventListener(api); } } ================================================ FILE: apollo-audit/apollo-audit-spring-boot-starter/src/main/java/com/ctrip/framework/apollo/audit/configuration/ApolloAuditNoOpAutoConfiguration.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.audit.configuration; import com.ctrip.framework.apollo.audit.ApolloAuditProperties; import com.ctrip.framework.apollo.audit.api.ApolloAuditLogApi; import com.ctrip.framework.apollo.audit.component.ApolloAuditHttpInterceptor; import com.ctrip.framework.apollo.audit.component.ApolloAuditLogApiNoOpImpl; import com.ctrip.framework.apollo.audit.context.ApolloAuditTraceContext; import com.ctrip.framework.apollo.audit.controller.ApolloAuditController; import com.ctrip.framework.apollo.audit.spi.ApolloAuditLogQueryApiPreAuthorizer; import com.ctrip.framework.apollo.audit.spi.ApolloAuditOperatorSupplier; import com.ctrip.framework.apollo.audit.spi.defaultimpl.ApolloAuditLogQueryApiDefaultPreAuthorizer; import com.ctrip.framework.apollo.audit.spi.defaultimpl.ApolloAuditOperatorDefaultSupplier; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration @EnableConfigurationProperties(ApolloAuditProperties.class) @ConditionalOnProperty(prefix = "apollo.audit.log", name = "enabled", havingValue = "false", matchIfMissing = true) public class ApolloAuditNoOpAutoConfiguration { private final ApolloAuditProperties apolloAuditProperties; public ApolloAuditNoOpAutoConfiguration(ApolloAuditProperties apolloAuditProperties) { this.apolloAuditProperties = apolloAuditProperties; } @Bean @ConditionalOnMissingBean(ApolloAuditLogApi.class) public ApolloAuditLogApi apolloAuditLogApi() { return new ApolloAuditLogApiNoOpImpl(); } @Bean @ConditionalOnMissingBean(ApolloAuditOperatorSupplier.class) public ApolloAuditOperatorSupplier apolloAuditLogOperatorSupplier() { return new ApolloAuditOperatorDefaultSupplier(); } @Bean @ConditionalOnMissingBean(ApolloAuditTraceContext.class) public ApolloAuditTraceContext apolloAuditTraceContext(ApolloAuditOperatorSupplier supplier) { return new ApolloAuditTraceContext(supplier); } @Bean @ConditionalOnMissingBean(ApolloAuditHttpInterceptor.class) public ApolloAuditHttpInterceptor apolloAuditLogHttpInterceptor( ApolloAuditTraceContext traceContext) { return new ApolloAuditHttpInterceptor(traceContext); } @Bean(name = "apolloAuditLogQueryApiPreAuthorizer") @ConditionalOnMissingBean(ApolloAuditLogQueryApiPreAuthorizer.class) public ApolloAuditLogQueryApiPreAuthorizer apolloAuditLogQueryApiPreAuthorizer() { return new ApolloAuditLogQueryApiDefaultPreAuthorizer(); } @Bean public ApolloAuditController apolloAuditController(ApolloAuditLogApi api, ApolloAuditProperties apolloAuditProperties) { return new ApolloAuditController(api, apolloAuditProperties); } } ================================================ FILE: apollo-audit/apollo-audit-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports ================================================ com.ctrip.framework.apollo.audit.configuration.ApolloAuditAutoConfiguration com.ctrip.framework.apollo.audit.configuration.ApolloAuditNoOpAutoConfiguration ================================================ FILE: apollo-audit/apollo-audit-spring-boot-starter/src/main/resources/META-INF/spring.factories ================================================ org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.ctrip.framework.apollo.audit.configuration.ApolloAuditAutoConfiguration,\ com.ctrip.framework.apollo.audit.configuration.ApolloAuditNoOpAutoConfiguration ================================================ FILE: apollo-audit/pom.xml ================================================ apollo com.ctrip.framework.apollo ${revision} 4.0.0 apollo-audit pom Apollo Audit apollo-audit-annotation apollo-audit-impl apollo-audit-api apollo-audit-spring-boot-starter ${project.artifactId} ================================================ FILE: apollo-biz/pom.xml ================================================ com.ctrip.framework.apollo apollo ${revision} 4.0.0 apollo-biz Apollo Biz jar ${project.artifactId} com.ctrip.framework.apollo apollo-common com.ctrip.framework.apollo apollo-audit-api com.ctrip.framework.apollo apollo-audit-spring-boot-starter test org.springframework.cloud spring-cloud-starter-netflix-eureka-client org.springframework.cloud spring-cloud-starter-consul-discovery org.springframework.cloud spring-cloud-starter-zookeeper-discovery ================================================ FILE: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/ApolloBizAssemblyConfiguration.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz; import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.context.annotation.Profile; @Profile("assembly") @Configuration public class ApolloBizAssemblyConfiguration { @Primary @ConfigurationProperties(prefix = "spring.config-datasource") @Bean public static DataSourceProperties dataSourceProperties() { return new DataSourceProperties(); } } ================================================ FILE: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/ApolloBizConfig.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; @EnableAutoConfiguration @Configuration @ComponentScan(basePackageClasses = ApolloBizConfig.class) public class ApolloBizConfig { } ================================================ FILE: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/auth/WebSecurityConfig.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.auth; import com.ctrip.framework.apollo.common.condition.ConditionalOnMissingProfile; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.provisioning.InMemoryUserDetailsManager; import org.springframework.security.web.SecurityFilterChain; @ConditionalOnMissingProfile({"auth", "assembly"}) @Configuration @EnableWebSecurity @EnableMethodSecurity(prePostEnabled = true) public class WebSecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http.httpBasic(Customizer.withDefaults()); http.csrf(csrf -> csrf.disable()); http.headers(headers -> headers.frameOptions(frameOptions -> frameOptions.sameOrigin())); return http.build(); } /** * Although the authentication below is useless, we may not remove them for backward compatibility. * Because if we remove them and the old clients(before 0.9.0) still send the authentication * information, the server will return 401, which should cause big problems. * * We may remove the following once we remove spring security from Apollo. */ @Bean public UserDetailsService userDetailsService() { return new InMemoryUserDetailsManager( User.withUsername("user").password("{noop}").roles("USER").build(), User.withUsername("apollo").password("{noop}").roles("USER", "ADMIN").build()); } } ================================================ FILE: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/config/BizConfig.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.config; import com.ctrip.framework.apollo.biz.service.BizDBPropertySource; import com.ctrip.framework.apollo.common.config.RefreshableConfig; import com.ctrip.framework.apollo.common.config.RefreshablePropertySource; import com.google.common.base.Strings; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; import java.lang.reflect.Type; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.function.Predicate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; @Component public class BizConfig extends RefreshableConfig { private final static Logger logger = LoggerFactory.getLogger(BizConfig.class); private static final int DEFAULT_ITEM_KEY_LENGTH = 128; private static final int DEFAULT_ITEM_VALUE_LENGTH = 20000; private static final int DEFAULT_MAX_NAMESPACE_NUM = 200; private static final int DEFAULT_MAX_ITEM_NUM = 1000; private static final int DEFAULT_APPNAMESPACE_CACHE_REBUILD_INTERVAL = 60; // 60s private static final int DEFAULT_GRAY_RELEASE_RULE_SCAN_INTERVAL = 60; // 60s private static final int DEFAULT_APPNAMESPACE_CACHE_SCAN_INTERVAL = 1; // 1s private static final int DEFAULT_ACCESS_KEY_CACHE_SCAN_INTERVAL = 1; // 1s private static final int DEFAULT_ACCESS_KEY_CACHE_REBUILD_INTERVAL = 60; // 60s private static final int DEFAULT_ACCESS_KEY_AUTH_TIME_DIFF_TOLERANCE = 60; // 60s private static final int DEFAULT_RELEASE_MESSAGE_CACHE_SCAN_INTERVAL = 1; // 1s private static final int DEFAULT_RELEASE_MESSAGE_SCAN_INTERVAL_IN_MS = 1000; // 1000ms private static final int DEFAULT_RELEASE_MESSAGE_NOTIFICATION_BATCH = 100; private static final int DEFAULT_RELEASE_MESSAGE_NOTIFICATION_BATCH_INTERVAL_IN_MILLI = 100;// 100ms private static final int DEFAULT_LONG_POLLING_TIMEOUT = 60; // 60s public static final int DEFAULT_RELEASE_HISTORY_RETENTION_SIZE = -1; private static final int DEFAULT_INSTANCE_CONFIG_AUDIT_MAX_SIZE = 10000; private static final int DEFAULT_INSTANCE_CACHE_MAX_SIZE = 50000; private static final int DEFAULT_INSTANCE_CONFIG_CACHE_MAX_SIZE = 50000; private static final int DEFAULT_INSTANCE_CONFIG_AUDIT_TIME_THRESHOLD_IN_MINUTE = 10;// 10 minutes private static final Gson GSON = new Gson(); private static final Type appIdValueLengthOverrideTypeReference = new TypeToken>() {}.getType(); private static final Type namespaceValueLengthOverrideTypeReference = new TypeToken>() {}.getType(); private static final Type releaseHistoryRetentionSizeOverrideTypeReference = new TypeToken>() {}.getType(); private final BizDBPropertySource propertySource; public BizConfig(final BizDBPropertySource propertySource) { this.propertySource = propertySource; } @Override protected List getRefreshablePropertySources() { return Collections.singletonList(propertySource); } public List eurekaServiceUrls() { String configuration = getValue("eureka.service.url", ""); if (Strings.isNullOrEmpty(configuration)) { return Collections.emptyList(); } return splitter.splitToList(configuration); } public int grayReleaseRuleScanInterval() { int interval = getIntProperty("apollo.gray-release-rule-scan.interval", DEFAULT_GRAY_RELEASE_RULE_SCAN_INTERVAL); return checkInt(interval, 1, Integer.MAX_VALUE, DEFAULT_GRAY_RELEASE_RULE_SCAN_INTERVAL); } public long longPollingTimeoutInMilli() { int timeout = getIntProperty("long.polling.timeout", DEFAULT_LONG_POLLING_TIMEOUT); // java client's long polling timeout is 90 seconds, so server side long polling timeout must be // less than 90 timeout = checkInt(timeout, 1, 90, DEFAULT_LONG_POLLING_TIMEOUT); return TimeUnit.SECONDS.toMillis(timeout); } public int itemKeyLengthLimit() { int limit = getIntProperty("item.key.length.limit", DEFAULT_ITEM_KEY_LENGTH); return checkInt(limit, 5, Integer.MAX_VALUE, DEFAULT_ITEM_KEY_LENGTH); } public int itemValueLengthLimit() { int limit = getIntProperty("item.value.length.limit", DEFAULT_ITEM_VALUE_LENGTH); return checkInt(limit, 5, Integer.MAX_VALUE, DEFAULT_ITEM_VALUE_LENGTH); } public Map appIdValueLengthLimitOverride() { String appIdValueLengthOverrideString = getValue("appid.value.length.limit.override"); return parseOverrideConfig(appIdValueLengthOverrideString, appIdValueLengthOverrideTypeReference, value -> value > 0); } public Map namespaceValueLengthLimitOverride() { String namespaceValueLengthOverrideString = getValue("namespace.value.length.limit.override"); return parseOverrideConfig(namespaceValueLengthOverrideString, namespaceValueLengthOverrideTypeReference, value -> value > 0); } public boolean isNamespaceNumLimitEnabled() { return getBooleanProperty("namespace.num.limit.enabled", false); } public int namespaceNumLimit() { int limit = getIntProperty("namespace.num.limit", DEFAULT_MAX_NAMESPACE_NUM); return checkInt(limit, 0, Integer.MAX_VALUE, DEFAULT_MAX_NAMESPACE_NUM); } public Set namespaceNumLimitWhite() { return Sets.newHashSet(getArrayProperty("namespace.num.limit.white", new String[0])); } public boolean isItemNumLimitEnabled() { return getBooleanProperty("item.num.limit.enabled", false); } public int itemNumLimit() { int limit = getIntProperty("item.num.limit", DEFAULT_MAX_ITEM_NUM); return checkInt(limit, 5, Integer.MAX_VALUE, DEFAULT_MAX_ITEM_NUM); } public boolean isNamespaceLockSwitchOff() { return !getBooleanProperty("namespace.lock.switch", false); } public int appNamespaceCacheScanInterval() { int interval = getIntProperty("apollo.app-namespace-cache-scan.interval", DEFAULT_APPNAMESPACE_CACHE_SCAN_INTERVAL); return checkInt(interval, 1, Integer.MAX_VALUE, DEFAULT_APPNAMESPACE_CACHE_SCAN_INTERVAL); } public TimeUnit appNamespaceCacheScanIntervalTimeUnit() { return TimeUnit.SECONDS; } public int appNamespaceCacheRebuildInterval() { int interval = getIntProperty("apollo.app-namespace-cache-rebuild.interval", DEFAULT_APPNAMESPACE_CACHE_REBUILD_INTERVAL); return checkInt(interval, 1, Integer.MAX_VALUE, DEFAULT_APPNAMESPACE_CACHE_REBUILD_INTERVAL); } public TimeUnit appNamespaceCacheRebuildIntervalTimeUnit() { return TimeUnit.SECONDS; } public int accessKeyCacheScanInterval() { int interval = getIntProperty("apollo.access-key-cache-scan.interval", DEFAULT_ACCESS_KEY_CACHE_SCAN_INTERVAL); return checkInt(interval, 1, Integer.MAX_VALUE, DEFAULT_ACCESS_KEY_CACHE_SCAN_INTERVAL); } public TimeUnit accessKeyCacheScanIntervalTimeUnit() { return TimeUnit.SECONDS; } public int accessKeyCacheRebuildInterval() { int interval = getIntProperty("apollo.access-key-cache-rebuild.interval", DEFAULT_ACCESS_KEY_CACHE_REBUILD_INTERVAL); return checkInt(interval, 1, Integer.MAX_VALUE, DEFAULT_ACCESS_KEY_CACHE_REBUILD_INTERVAL); } public TimeUnit accessKeyCacheRebuildIntervalTimeUnit() { return TimeUnit.SECONDS; } public int accessKeyAuthTimeDiffTolerance() { int authTimeDiffTolerance = getIntProperty("apollo.access-key.auth-time-diff-tolerance", DEFAULT_ACCESS_KEY_AUTH_TIME_DIFF_TOLERANCE); return checkInt(authTimeDiffTolerance, 1, Integer.MAX_VALUE, DEFAULT_ACCESS_KEY_AUTH_TIME_DIFF_TOLERANCE); } public int releaseHistoryRetentionSize() { int count = getIntProperty("apollo.release-history.retention.size", DEFAULT_RELEASE_HISTORY_RETENTION_SIZE); return checkInt(count, 1, Integer.MAX_VALUE, DEFAULT_RELEASE_HISTORY_RETENTION_SIZE); } public Map releaseHistoryRetentionSizeOverride() { String overrideString = getValue("apollo.release-history.retention.size.override"); return parseOverrideConfig(overrideString, releaseHistoryRetentionSizeOverrideTypeReference, value -> value > 0); } public int releaseMessageCacheScanInterval() { int interval = getIntProperty("apollo.release-message-cache-scan.interval", DEFAULT_RELEASE_MESSAGE_CACHE_SCAN_INTERVAL); return checkInt(interval, 1, Integer.MAX_VALUE, DEFAULT_RELEASE_MESSAGE_CACHE_SCAN_INTERVAL); } public TimeUnit releaseMessageCacheScanIntervalTimeUnit() { return TimeUnit.SECONDS; } public int releaseMessageScanIntervalInMilli() { int interval = getIntProperty("apollo.message-scan.interval", DEFAULT_RELEASE_MESSAGE_SCAN_INTERVAL_IN_MS); return checkInt(interval, 100, Integer.MAX_VALUE, DEFAULT_RELEASE_MESSAGE_SCAN_INTERVAL_IN_MS); } public int releaseMessageNotificationBatch() { int batch = getIntProperty("apollo.release-message.notification.batch", DEFAULT_RELEASE_MESSAGE_NOTIFICATION_BATCH); return checkInt(batch, 1, Integer.MAX_VALUE, DEFAULT_RELEASE_MESSAGE_NOTIFICATION_BATCH); } public int releaseMessageNotificationBatchIntervalInMilli() { int interval = getIntProperty("apollo.release-message.notification.batch.interval", DEFAULT_RELEASE_MESSAGE_NOTIFICATION_BATCH_INTERVAL_IN_MILLI); return checkInt(interval, 10, Integer.MAX_VALUE, DEFAULT_RELEASE_MESSAGE_NOTIFICATION_BATCH_INTERVAL_IN_MILLI); } public boolean isConfigServiceCacheEnabled() { return getBooleanProperty("config-service.cache.enabled", false); } public boolean isConfigServiceCacheStatsEnabled() { return getBooleanProperty("config-service.cache.stats.enabled", false); } public boolean isConfigServiceCacheKeyIgnoreCase() { return getBooleanProperty("config-service.cache.key.ignore-case", false); } public int getInstanceConfigAuditMaxSize() { int auditMaxSize = getIntProperty("instance.config.audit.max.size", DEFAULT_INSTANCE_CONFIG_AUDIT_MAX_SIZE); return checkInt(auditMaxSize, 10, Integer.MAX_VALUE, DEFAULT_INSTANCE_CONFIG_AUDIT_MAX_SIZE); } public int getInstanceCacheMaxSize() { int cacheMaxSize = getIntProperty("instance.cache.max.size", DEFAULT_INSTANCE_CACHE_MAX_SIZE); return checkInt(cacheMaxSize, 10, Integer.MAX_VALUE, DEFAULT_INSTANCE_CACHE_MAX_SIZE); } public int getInstanceConfigCacheMaxSize() { int cacheMaxSize = getIntProperty("instance.config.cache.max.size", DEFAULT_INSTANCE_CONFIG_CACHE_MAX_SIZE); return checkInt(cacheMaxSize, 10, Integer.MAX_VALUE, DEFAULT_INSTANCE_CONFIG_CACHE_MAX_SIZE); } public long getInstanceConfigAuditTimeThresholdInMilli() { int timeThreshold = getIntProperty("instance.config.audit.time.threshold.minutes", DEFAULT_INSTANCE_CONFIG_AUDIT_TIME_THRESHOLD_IN_MINUTE); timeThreshold = checkInt(timeThreshold, 5, Integer.MAX_VALUE, DEFAULT_INSTANCE_CONFIG_AUDIT_TIME_THRESHOLD_IN_MINUTE); return TimeUnit.MINUTES.toMillis(timeThreshold); } public boolean isConfigServiceIncrementalChangeEnabled() { return getBooleanProperty("config-service.incremental.change.enabled", false); } int checkInt(int value, int min, int max, int defaultValue) { if (value >= min && value <= max) { return value; } return defaultValue; } public boolean isAdminServiceAccessControlEnabled() { return getBooleanProperty("admin-service.access.control.enabled", false); } public String getAdminServiceAccessTokens() { return getValue("admin-service.access.tokens"); } private Map parseOverrideConfig(String configValue, Type typeReference, Predicate valueFilter) { Map result = Maps.newHashMap(); if (!Strings.isNullOrEmpty(configValue)) { try { Map parsed = GSON.fromJson(configValue, typeReference); for (Map.Entry entry : parsed.entrySet()) { if (entry.getValue() != null && valueFilter.test(entry.getValue())) { result.put(entry.getKey(), entry.getValue()); } } } catch (Exception e) { logger.error("Invalid override config value: {}", configValue, e); } } return Collections.unmodifiableMap(result); } } ================================================ FILE: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/entity/AccessKey.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.entity; import com.ctrip.framework.apollo.common.entity.BaseEntity; import org.hibernate.annotations.SQLDelete; import org.hibernate.annotations.Where; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.Table; @Entity @Table(name = "`AccessKey`") @SQLDelete( sql = "Update `AccessKey` set IsDeleted = true, DeletedAt = ROUND(UNIX_TIMESTAMP(NOW(4))*1000) where Id = ?") @Where(clause = "`IsDeleted` = false") public class AccessKey extends BaseEntity { @Column(name = "`AppId`", nullable = false) private String appId; @Column(name = "`Secret`", nullable = false) private String secret; @Column(name = "`Mode`") private int mode; @Column(name = "`IsEnabled`", columnDefinition = "Bit default '0'") private boolean enabled; public String getAppId() { return appId; } public void setAppId(String appId) { this.appId = appId; } public String getSecret() { return secret; } public void setSecret(String secret) { this.secret = secret; } public int getMode() { return mode; } public void setMode(int mode) { this.mode = mode; } public boolean isEnabled() { return enabled; } public void setEnabled(boolean enabled) { this.enabled = enabled; } @Override public String toString() { return toStringHelper().add("appId", appId).add("secret", secret).add("mode", mode) .add("enabled", enabled).toString(); } } ================================================ FILE: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/entity/Audit.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.entity; import com.ctrip.framework.apollo.common.entity.BaseEntity; import org.hibernate.annotations.SQLDelete; import org.hibernate.annotations.Where; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.Table; @Entity @Table(name = "`Audit`") @SQLDelete( sql = "Update `Audit` set IsDeleted = true, DeletedAt = ROUND(UNIX_TIMESTAMP(NOW(4))*1000) where Id = ?") @Where(clause = "`IsDeleted` = false") public class Audit extends BaseEntity { public enum OP { INSERT, UPDATE, DELETE } @Column(name = "`EntityName`", nullable = false) private String entityName; @Column(name = "`EntityId`") private Long entityId; @Column(name = "`OpName`", nullable = false) private String opName; @Column(name = "`Comment`") private String comment; public String getComment() { return comment; } public Long getEntityId() { return entityId; } public String getEntityName() { return entityName; } public String getOpName() { return opName; } public void setComment(String comment) { this.comment = comment; } public void setEntityId(Long entityId) { this.entityId = entityId; } public void setEntityName(String entityName) { this.entityName = entityName; } public void setOpName(String opName) { this.opName = opName; } @Override public String toString() { return toStringHelper().add("entityName", entityName).add("entityId", entityId) .add("opName", opName).add("comment", comment).toString(); } } ================================================ FILE: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/entity/Cluster.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.entity; import com.ctrip.framework.apollo.common.entity.BaseEntity; import org.hibernate.annotations.SQLDelete; import org.hibernate.annotations.Where; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.Table; /** * @author Jason Song(song_s@ctrip.com) */ @Entity @Table(name = "`Cluster`") @SQLDelete( sql = "Update `Cluster` set IsDeleted = true, DeletedAt = ROUND(UNIX_TIMESTAMP(NOW(4))*1000) where Id = ?") @Where(clause = "`IsDeleted` = false") public class Cluster extends BaseEntity implements Comparable { @Column(name = "`Name`", nullable = false) private String name; @Column(name = "`AppId`", nullable = false) private String appId; @Column(name = "`ParentClusterId`", nullable = false) private long parentClusterId; @Column(name = "`Comment`") private String comment; public String getAppId() { return appId; } public String getName() { return name; } public void setAppId(String appId) { this.appId = appId; } public void setName(String name) { this.name = name; } public long getParentClusterId() { return parentClusterId; } public void setParentClusterId(long parentClusterId) { this.parentClusterId = parentClusterId; } public String getComment() { return comment; } public void setComment(String comment) { this.comment = comment; } @Override public String toString() { return toStringHelper().add("name", name).add("appId", appId) .add("parentClusterId", parentClusterId).add("comment", comment).toString(); } @Override public int compareTo(Cluster o) { if (o == null || getId() > o.getId()) { return 1; } if (getId() == o.getId()) { return 0; } return -1; } } ================================================ FILE: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/entity/Commit.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.entity; import com.ctrip.framework.apollo.common.entity.BaseEntity; import org.hibernate.annotations.SQLDelete; import org.hibernate.annotations.Where; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.Lob; import jakarta.persistence.Table; @Entity @Table(name = "`Commit`") @SQLDelete( sql = "Update `Commit` set IsDeleted = true, DeletedAt = ROUND(UNIX_TIMESTAMP(NOW(4))*1000) where Id = ?") @Where(clause = "`IsDeleted` = false") public class Commit extends BaseEntity { @Lob @Column(name = "`ChangeSets`", nullable = false) private String changeSets; @Column(name = "`AppId`", nullable = false) private String appId; @Column(name = "`ClusterName`", nullable = false) private String clusterName; @Column(name = "`NamespaceName`", nullable = false) private String namespaceName; @Column(name = "`Comment`") private String comment; public String getChangeSets() { return changeSets; } public void setChangeSets(String changeSets) { this.changeSets = changeSets; } public String getAppId() { return appId; } public void setAppId(String appId) { this.appId = appId; } public String getClusterName() { return clusterName; } public void setClusterName(String clusterName) { this.clusterName = clusterName; } public String getNamespaceName() { return namespaceName; } public void setNamespaceName(String namespaceName) { this.namespaceName = namespaceName; } public String getComment() { return comment; } public void setComment(String comment) { this.comment = comment; } @Override public String toString() { return toStringHelper().add("changeSets", changeSets).add("appId", appId) .add("clusterName", clusterName).add("namespaceName", namespaceName).add("comment", comment) .toString(); } } ================================================ FILE: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/entity/GrayReleaseRule.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.entity; import com.ctrip.framework.apollo.common.entity.BaseEntity; import org.hibernate.annotations.SQLDelete; import org.hibernate.annotations.Where; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.Table; @Entity @Table(name = "`GrayReleaseRule`") @SQLDelete( sql = "Update `GrayReleaseRule` set IsDeleted = true, DeletedAt = ROUND(UNIX_TIMESTAMP(NOW(4))*1000) where Id = ?") @Where(clause = "`IsDeleted` = false") public class GrayReleaseRule extends BaseEntity { @Column(name = "`AppId`", nullable = false) private String appId; @Column(name = "`ClusterName`", nullable = false) private String clusterName; @Column(name = "`NamespaceName`", nullable = false) private String namespaceName; @Column(name = "`BranchName`", nullable = false) private String branchName; @Column(name = "`Rules`") private String rules; @Column(name = "`ReleaseId`", nullable = false) private Long releaseId; @Column(name = "`BranchStatus`", nullable = false) private int branchStatus; public String getAppId() { return appId; } public void setAppId(String appId) { this.appId = appId; } public String getClusterName() { return clusterName; } public void setClusterName(String clusterName) { this.clusterName = clusterName; } public String getNamespaceName() { return namespaceName; } public void setNamespaceName(String namespaceName) { this.namespaceName = namespaceName; } public String getBranchName() { return branchName; } public void setBranchName(String branchName) { this.branchName = branchName; } public String getRules() { return rules; } public void setRules(String rules) { this.rules = rules; } public Long getReleaseId() { return releaseId; } public void setReleaseId(Long releaseId) { this.releaseId = releaseId; } public int getBranchStatus() { return branchStatus; } public void setBranchStatus(int branchStatus) { this.branchStatus = branchStatus; } } ================================================ FILE: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/entity/Instance.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.entity; import com.google.common.base.MoreObjects; import java.util.Date; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.PrePersist; import jakarta.persistence.Table; /** * @author Jason Song(song_s@ctrip.com) */ @Entity @Table(name = "`Instance`") public class Instance { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "`Id`") private long id; @Column(name = "`AppId`", nullable = false) private String appId; @Column(name = "`ClusterName`", nullable = false) private String clusterName; @Column(name = "`DataCenter`", nullable = false) private String dataCenter; @Column(name = "`Ip`", nullable = false) private String ip; @Column(name = "`DataChange_CreatedTime`", nullable = false) private Date dataChangeCreatedTime; @Column(name = "`DataChange_LastTime`") private Date dataChangeLastModifiedTime; @PrePersist protected void prePersist() { if (this.dataChangeCreatedTime == null) { dataChangeCreatedTime = new Date(); } if (this.dataChangeLastModifiedTime == null) { dataChangeLastModifiedTime = dataChangeCreatedTime; } } public long getId() { return id; } public void setId(long id) { this.id = id; } public String getAppId() { return appId; } public void setAppId(String appId) { this.appId = appId; } public String getClusterName() { return clusterName; } public void setClusterName(String clusterName) { this.clusterName = clusterName; } public String getDataCenter() { return dataCenter; } public void setDataCenter(String dataCenter) { this.dataCenter = dataCenter; } public String getIp() { return ip; } public void setIp(String ip) { this.ip = ip; } public Date getDataChangeCreatedTime() { return dataChangeCreatedTime; } public void setDataChangeCreatedTime(Date dataChangeCreatedTime) { this.dataChangeCreatedTime = dataChangeCreatedTime; } public Date getDataChangeLastModifiedTime() { return dataChangeLastModifiedTime; } public void setDataChangeLastModifiedTime(Date dataChangeLastModifiedTime) { this.dataChangeLastModifiedTime = dataChangeLastModifiedTime; } @Override public String toString() { return MoreObjects.toStringHelper(this).omitNullValues().add("id", id).add("appId", appId) .add("clusterName", clusterName).add("dataCenter", dataCenter).add("ip", ip) .add("dataChangeCreatedTime", dataChangeCreatedTime) .add("dataChangeLastModifiedTime", dataChangeLastModifiedTime).toString(); } } ================================================ FILE: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/entity/InstanceConfig.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.entity; import com.google.common.base.MoreObjects; import java.util.Date; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.PrePersist; import jakarta.persistence.PreUpdate; import jakarta.persistence.Table; /** * @author Jason Song(song_s@ctrip.com) */ @Entity @Table(name = "`InstanceConfig`") public class InstanceConfig { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "`Id`") private long id; @Column(name = "`InstanceId`") private long instanceId; @Column(name = "`ConfigAppId`", nullable = false) private String configAppId; @Column(name = "`ConfigClusterName`", nullable = false) private String configClusterName; @Column(name = "`ConfigNamespaceName`", nullable = false) private String configNamespaceName; @Column(name = "`ReleaseKey`", nullable = false) private String releaseKey; @Column(name = "`ReleaseDeliveryTime`", nullable = false) private Date releaseDeliveryTime; @Column(name = "`DataChange_CreatedTime`", nullable = false) private Date dataChangeCreatedTime; @Column(name = "`DataChange_LastTime`") private Date dataChangeLastModifiedTime; @PrePersist protected void prePersist() { if (this.dataChangeCreatedTime == null) { dataChangeCreatedTime = new Date(); } if (this.dataChangeLastModifiedTime == null) { dataChangeLastModifiedTime = dataChangeCreatedTime; } } @PreUpdate protected void preUpdate() { this.dataChangeLastModifiedTime = new Date(); } public long getId() { return id; } public void setId(long id) { this.id = id; } public long getInstanceId() { return instanceId; } public void setInstanceId(long instanceId) { this.instanceId = instanceId; } public String getConfigAppId() { return configAppId; } public void setConfigAppId(String configAppId) { this.configAppId = configAppId; } public String getConfigNamespaceName() { return configNamespaceName; } public void setConfigNamespaceName(String configNamespaceName) { this.configNamespaceName = configNamespaceName; } public String getReleaseKey() { return releaseKey; } public void setReleaseKey(String releaseKey) { this.releaseKey = releaseKey; } public Date getDataChangeCreatedTime() { return dataChangeCreatedTime; } public void setDataChangeCreatedTime(Date dataChangeCreatedTime) { this.dataChangeCreatedTime = dataChangeCreatedTime; } public Date getDataChangeLastModifiedTime() { return dataChangeLastModifiedTime; } public void setDataChangeLastModifiedTime(Date dataChangeLastModifiedTime) { this.dataChangeLastModifiedTime = dataChangeLastModifiedTime; } public String getConfigClusterName() { return configClusterName; } public void setConfigClusterName(String configClusterName) { this.configClusterName = configClusterName; } public Date getReleaseDeliveryTime() { return releaseDeliveryTime; } public void setReleaseDeliveryTime(Date releaseDeliveryTime) { this.releaseDeliveryTime = releaseDeliveryTime; } @Override public String toString() { return MoreObjects.toStringHelper(this).omitNullValues().add("id", id) .add("configAppId", configAppId).add("configClusterName", configClusterName) .add("configNamespaceName", configNamespaceName).add("releaseKey", releaseKey) .add("dataChangeCreatedTime", dataChangeCreatedTime) .add("dataChangeLastModifiedTime", dataChangeLastModifiedTime).toString(); } } ================================================ FILE: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/entity/Item.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.entity; import com.ctrip.framework.apollo.common.entity.BaseEntity; import org.hibernate.annotations.SQLDelete; import org.hibernate.annotations.Where; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.Lob; import jakarta.persistence.Table; @Entity @Table(name = "`Item`") @SQLDelete( sql = "Update `Item` set IsDeleted = true, DeletedAt = ROUND(UNIX_TIMESTAMP(NOW(4))*1000) where Id = ?") @Where(clause = "`IsDeleted` = false") public class Item extends BaseEntity { @Column(name = "`NamespaceId`", nullable = false) private long namespaceId; @Column(name = "`Key`", nullable = false) private String key; @Column(name = "`Type`") private int type; @Column(name = "`Value`") @Lob private String value; @Column(name = "`Comment`") private String comment; @Column(name = "`LineNum`") private Integer lineNum; public String getComment() { return comment; } public String getKey() { return key; } public long getNamespaceId() { return namespaceId; } public String getValue() { return value; } public void setComment(String comment) { this.comment = comment; } public void setKey(String key) { this.key = key; } public void setNamespaceId(long namespaceId) { this.namespaceId = namespaceId; } public void setValue(String value) { this.value = value; } public Integer getLineNum() { return lineNum; } public void setLineNum(Integer lineNum) { this.lineNum = lineNum; } public int getType() { return type; } public void setType(int type) { this.type = type; } @Override public String toString() { return toStringHelper().add("namespaceId", namespaceId).add("key", key).add("type", type) .add("value", value).add("lineNum", lineNum).add("comment", comment).toString(); } } ================================================ FILE: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/entity/JpaMapFieldJsonConverter.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.entity; import com.google.common.reflect.TypeToken; import com.google.gson.Gson; import java.lang.reflect.Type; import java.util.HashMap; import java.util.Map; import jakarta.persistence.AttributeConverter; import jakarta.persistence.Converter; @Converter(autoApply = true) class JpaMapFieldJsonConverter implements AttributeConverter, String> { private static final Gson GSON = new Gson(); private static final TypeToken> TYPE_TOKEN = new TypeToken>() {}; @SuppressWarnings("unchecked") private static final Type TYPE = TYPE_TOKEN.getType(); @Override public String convertToDatabaseColumn(Map attribute) { return GSON.toJson(attribute); } @Override public Map convertToEntityAttribute(String dbData) { return GSON.fromJson(dbData, TYPE); } } ================================================ FILE: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/entity/Namespace.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.entity; import com.ctrip.framework.apollo.common.entity.BaseEntity; import org.hibernate.annotations.SQLDelete; import org.hibernate.annotations.Where; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.Table; @Entity @Table(name = "`Namespace`") @SQLDelete( sql = "Update `Namespace` set IsDeleted = true, DeletedAt = ROUND(UNIX_TIMESTAMP(NOW(4))*1000) where Id = ?") @Where(clause = "`IsDeleted` = false") public class Namespace extends BaseEntity { @Column(name = "`AppId`", nullable = false) private String appId; @Column(name = "`ClusterName`", nullable = false) private String clusterName; @Column(name = "`NamespaceName`", nullable = false) private String namespaceName; public Namespace() { } public Namespace(String appId, String clusterName, String namespaceName) { this.appId = appId; this.clusterName = clusterName; this.namespaceName = namespaceName; } public String getAppId() { return appId; } public String getClusterName() { return clusterName; } public String getNamespaceName() { return namespaceName; } public void setAppId(String appId) { this.appId = appId; } public void setClusterName(String clusterName) { this.clusterName = clusterName; } public void setNamespaceName(String namespaceName) { this.namespaceName = namespaceName; } @Override public String toString() { return toStringHelper().add("appId", appId).add("clusterName", clusterName) .add("namespaceName", namespaceName).toString(); } } ================================================ FILE: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/entity/NamespaceLock.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.entity; import com.ctrip.framework.apollo.common.entity.BaseEntity; import org.hibernate.annotations.Where; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.Table; @Entity @Table(name = "`NamespaceLock`") @Where(clause = "`IsDeleted` = false") public class NamespaceLock extends BaseEntity { @Column(name = "`NamespaceId`") private long namespaceId; public long getNamespaceId() { return namespaceId; } public void setNamespaceId(long namespaceId) { this.namespaceId = namespaceId; } } ================================================ FILE: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/entity/Privilege.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.entity; import com.ctrip.framework.apollo.common.entity.BaseEntity; import org.hibernate.annotations.SQLDelete; import org.hibernate.annotations.Where; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.Table; @Entity @Table(name = "`Privilege`") @SQLDelete( sql = "Update `Privilege` set IsDeleted = true, DeletedAt = ROUND(UNIX_TIMESTAMP(NOW(4))*1000) where Id = ?") @Where(clause = "`IsDeleted` = false") public class Privilege extends BaseEntity { @Column(name = "`Name`", nullable = false) private String name; @Column(name = "`PrivilType`", nullable = false) private String privilType; @Column(name = "`NamespaceId`") private long namespaceId; public String getName() { return name; } public long getNamespaceId() { return namespaceId; } public String getPrivilType() { return privilType; } public void setName(String name) { this.name = name; } public void setNamespaceId(long namespaceId) { this.namespaceId = namespaceId; } public void setPrivilType(String privilType) { this.privilType = privilType; } @Override public String toString() { return toStringHelper().add("namespaceId", namespaceId).add("privilType", privilType) .add("name", name).toString(); } } ================================================ FILE: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/entity/Release.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.entity; import com.ctrip.framework.apollo.common.entity.BaseEntity; import org.hibernate.annotations.SQLDelete; import org.hibernate.annotations.Where; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.Lob; import jakarta.persistence.Table; /** * @author Jason Song(song_s@ctrip.com) */ @Entity @Table(name = "`Release`") @SQLDelete( sql = "Update `Release` set IsDeleted = true, DeletedAt = ROUND(UNIX_TIMESTAMP(NOW(4))*1000) where Id = ?") @Where(clause = "`IsDeleted` = false") public class Release extends BaseEntity { @Column(name = "`ReleaseKey`", nullable = false) private String releaseKey; @Column(name = "`Name`", nullable = false) private String name; @Column(name = "`AppId`", nullable = false) private String appId; @Column(name = "`ClusterName`", nullable = false) private String clusterName; @Column(name = "`NamespaceName`", nullable = false) private String namespaceName; @Column(name = "`Configurations`", nullable = false) @Lob private String configurations; @Column(name = "`Comment`", nullable = false) private String comment; @Column(name = "`IsAbandoned`", columnDefinition = "Bit default '0'") private boolean isAbandoned; public String getReleaseKey() { return releaseKey; } public String getAppId() { return appId; } public String getClusterName() { return clusterName; } public String getComment() { return comment; } public String getConfigurations() { return configurations; } public String getNamespaceName() { return namespaceName; } public String getName() { return name; } public void setReleaseKey(String releaseKey) { this.releaseKey = releaseKey; } public void setAppId(String appId) { this.appId = appId; } public void setClusterName(String clusterName) { this.clusterName = clusterName; } public void setComment(String comment) { this.comment = comment; } public void setConfigurations(String configurations) { this.configurations = configurations; } public void setNamespaceName(String namespaceName) { this.namespaceName = namespaceName; } public void setName(String name) { this.name = name; } public boolean isAbandoned() { return isAbandoned; } public void setAbandoned(boolean abandoned) { isAbandoned = abandoned; } @Override public String toString() { return toStringHelper().add("name", name).add("appId", appId).add("clusterName", clusterName) .add("namespaceName", namespaceName).add("configurations", configurations) .add("comment", comment).add("isAbandoned", isAbandoned).toString(); } } ================================================ FILE: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/entity/ReleaseHistory.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.entity; import com.ctrip.framework.apollo.common.entity.BaseEntity; import org.hibernate.annotations.SQLDelete; import org.hibernate.annotations.Where; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.Table; /** * @author Jason Song(song_s@ctrip.com) */ @Entity @Table(name = "`ReleaseHistory`") @SQLDelete( sql = "Update `ReleaseHistory` set IsDeleted = true, DeletedAt = ROUND(UNIX_TIMESTAMP(NOW(4))*1000) where Id = ?") @Where(clause = "`IsDeleted` = false") public class ReleaseHistory extends BaseEntity { @Column(name = "`AppId`", nullable = false) private String appId; @Column(name = "`ClusterName`", nullable = false) private String clusterName; @Column(name = "`NamespaceName`", nullable = false) private String namespaceName; @Column(name = "`BranchName`", nullable = false) private String branchName; @Column(name = "`ReleaseId`") private long releaseId; @Column(name = "`PreviousReleaseId`") private long previousReleaseId; @Column(name = "`Operation`") private int operation; @Column(name = "`OperationContext`", nullable = false) private String operationContext; public String getAppId() { return appId; } public void setAppId(String appId) { this.appId = appId; } public String getClusterName() { return clusterName; } public void setClusterName(String clusterName) { this.clusterName = clusterName; } public String getNamespaceName() { return namespaceName; } public void setNamespaceName(String namespaceName) { this.namespaceName = namespaceName; } public String getBranchName() { return branchName; } public void setBranchName(String branchName) { this.branchName = branchName; } public long getReleaseId() { return releaseId; } public void setReleaseId(long releaseId) { this.releaseId = releaseId; } public long getPreviousReleaseId() { return previousReleaseId; } public void setPreviousReleaseId(long previousReleaseId) { this.previousReleaseId = previousReleaseId; } public int getOperation() { return operation; } public void setOperation(int operation) { this.operation = operation; } public String getOperationContext() { return operationContext; } public void setOperationContext(String operationContext) { this.operationContext = operationContext; } @Override public String toString() { return toStringHelper().add("appId", appId).add("clusterName", clusterName) .add("namespaceName", namespaceName).add("branchName", branchName) .add("releaseId", releaseId).add("previousReleaseId", previousReleaseId) .add("operation", operation).toString(); } } ================================================ FILE: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/entity/ReleaseMessage.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.entity; import com.google.common.base.MoreObjects; import java.util.Date; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.PrePersist; import jakarta.persistence.Table; /** * @author Jason Song(song_s@ctrip.com) */ @Entity @Table(name = "`ReleaseMessage`") public class ReleaseMessage { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "`Id`") private long id; @Column(name = "`Message`", nullable = false) private String message; @Column(name = "`DataChange_LastTime`") private Date dataChangeLastModifiedTime; @PrePersist protected void prePersist() { if (this.dataChangeLastModifiedTime == null) { dataChangeLastModifiedTime = new Date(); } } public ReleaseMessage() {} public ReleaseMessage(String message) { this.message = message; } public long getId() { return id; } public void setId(long id) { this.id = id; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } @Override public String toString() { return MoreObjects.toStringHelper(this).omitNullValues().add("id", id).add("message", message) .add("dataChangeLastModifiedTime", dataChangeLastModifiedTime).toString(); } } ================================================ FILE: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/entity/ServerConfig.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.entity; import com.ctrip.framework.apollo.common.entity.BaseEntity; import org.hibernate.annotations.SQLDelete; import org.hibernate.annotations.Where; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.Table; /** * @author Jason Song(song_s@ctrip.com) */ @Entity @Table(name = "`ServerConfig`") @SQLDelete( sql = "Update `ServerConfig` set IsDeleted = true, DeletedAt = ROUND(UNIX_TIMESTAMP(NOW(4))*1000) where Id = ?") @Where(clause = "`IsDeleted` = false") public class ServerConfig extends BaseEntity { @Column(name = "`Key`", nullable = false) private String key; @Column(name = "`Cluster`", nullable = false) private String cluster; @Column(name = "`Value`", nullable = false) private String value; @Column(name = "`Comment`", nullable = false) private String comment; public String getKey() { return key; } public void setKey(String key) { this.key = key; } public String getValue() { return value; } public void setValue(String value) { this.value = value; } public String getComment() { return comment; } public void setComment(String comment) { this.comment = comment; } public String getCluster() { return cluster; } public void setCluster(String cluster) { this.cluster = cluster; } @Override public String toString() { return toStringHelper().add("key", key).add("value", value).add("cluster", cluster) .add("comment", comment).toString(); } } ================================================ FILE: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/entity/ServiceRegistry.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.entity; import com.ctrip.framework.apollo.biz.registry.ServiceInstance; import java.time.LocalDateTime; import java.util.Map; import jakarta.persistence.Column; import jakarta.persistence.Convert; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.PrePersist; import jakarta.persistence.Table; /** * use database as a registry instead of eureka, zookeeper, consul etc. *

* persist {@link ServiceInstance} */ @Entity @Table(name = "`ServiceRegistry`") public class ServiceRegistry { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "`Id`") private long id; @Column(name = "`ServiceName`", nullable = false) private String serviceName; /** * @see ServiceInstance#getUri() */ @Column(name = "`Uri`", nullable = false) private String uri; /** * @see ServiceInstance#getCluster() */ @Column(name = "`Cluster`", nullable = false) private String cluster; @Column(name = "`Metadata`", nullable = false) @Convert(converter = JpaMapFieldJsonConverter.class) private Map metadata; @Column(name = "`DataChange_CreatedTime`", nullable = false) private LocalDateTime dataChangeCreatedTime; /** * modify by heartbeat */ @Column(name = "`DataChange_LastTime`", nullable = false) private LocalDateTime dataChangeLastModifiedTime; @PrePersist protected void prePersist() { if (this.dataChangeCreatedTime == null) { dataChangeCreatedTime = LocalDateTime.now(); } if (this.dataChangeLastModifiedTime == null) { dataChangeLastModifiedTime = dataChangeCreatedTime; } } @Override public String toString() { return "Registry{" + "id=" + id + ", serviceName='" + serviceName + '\'' + ", uri='" + uri + '\'' + ", cluster='" + cluster + '\'' + ", metadata='" + metadata + '\'' + ", dataChangeCreatedTime=" + dataChangeCreatedTime + ", dataChangeLastModifiedTime=" + dataChangeLastModifiedTime + '}'; } public long getId() { return id; } public void setId(long id) { this.id = id; } public String getServiceName() { return serviceName; } public void setServiceName(String serviceName) { this.serviceName = serviceName; } public String getUri() { return uri; } public void setUri(String uri) { this.uri = uri; } public String getCluster() { return cluster; } public void setCluster(String cluster) { this.cluster = cluster; } public Map getMetadata() { return metadata; } public void setMetadata(Map metadata) { this.metadata = metadata; } public LocalDateTime getDataChangeCreatedTime() { return dataChangeCreatedTime; } public void setDataChangeCreatedTime(LocalDateTime dataChangeCreatedTime) { this.dataChangeCreatedTime = dataChangeCreatedTime; } public LocalDateTime getDataChangeLastModifiedTime() { return dataChangeLastModifiedTime; } public void setDataChangeLastModifiedTime(LocalDateTime dataChangeLastModifiedTime) { this.dataChangeLastModifiedTime = dataChangeLastModifiedTime; } } ================================================ FILE: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/eureka/ApolloEurekaClientConfig.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.eureka; import com.ctrip.framework.apollo.biz.config.BizConfig; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.cloud.context.scope.refresh.RefreshScope; import org.springframework.cloud.netflix.eureka.EurekaClientConfigBean; import org.springframework.context.annotation.Primary; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component; import org.springframework.util.CollectionUtils; import java.util.List; @Component @Primary @ConditionalOnProperty(value = {"eureka.client.enabled"}, havingValue = "true", matchIfMissing = true) public class ApolloEurekaClientConfig extends EurekaClientConfigBean { private final BizConfig bizConfig; private final RefreshScope refreshScope; private static final String EUREKA_CLIENT_BEAN_NAME = "eurekaClient"; public ApolloEurekaClientConfig(final BizConfig bizConfig, final RefreshScope refreshScope) { this.bizConfig = bizConfig; this.refreshScope = refreshScope; } /** * Assert only one zone: defaultZone, but multiple environments. */ @Override public List getEurekaServerServiceUrls(String myZone) { List urls = bizConfig.eurekaServiceUrls(); return CollectionUtils.isEmpty(urls) ? super.getEurekaServerServiceUrls(myZone) : urls; } @EventListener public void listenApplicationReadyEvent(ApplicationReadyEvent event) { this.refreshEurekaClient(); } private void refreshEurekaClient() { if (!super.isFetchRegistry()) { super.setFetchRegistry(true); super.setRegisterWithEureka(true); refreshScope.refresh(EUREKA_CLIENT_BEAN_NAME); } } @Override public boolean equals(Object o) { return super.equals(o); } } ================================================ FILE: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/grayReleaseRule/GrayReleaseRuleCache.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.grayReleaseRule; import com.ctrip.framework.apollo.common.dto.GrayReleaseRuleItemDTO; import java.util.Set; /** * @author Jason Song(song_s@ctrip.com) */ public class GrayReleaseRuleCache implements Comparable { private long ruleId; private String branchName; private String namespaceName; private long releaseId; private long loadVersion; private int branchStatus; private Set ruleItems; public GrayReleaseRuleCache(long ruleId, String branchName, String namespaceName, long releaseId, int branchStatus, long loadVersion, Set ruleItems) { this.ruleId = ruleId; this.branchName = branchName; this.namespaceName = namespaceName; this.releaseId = releaseId; this.branchStatus = branchStatus; this.loadVersion = loadVersion; this.ruleItems = ruleItems; } public long getRuleId() { return ruleId; } public Set getRuleItems() { return ruleItems; } public String getBranchName() { return branchName; } public int getBranchStatus() { return branchStatus; } public long getReleaseId() { return releaseId; } public long getLoadVersion() { return loadVersion; } public void setLoadVersion(long loadVersion) { this.loadVersion = loadVersion; } public String getNamespaceName() { return namespaceName; } public boolean matches(String clientAppId, String clientIp, String clientLabel) { for (GrayReleaseRuleItemDTO ruleItem : ruleItems) { if (ruleItem.matches(clientAppId, clientIp, clientLabel)) { return true; } } return false; } @Override public int compareTo(GrayReleaseRuleCache that) { return Long.compare(this.ruleId, that.ruleId); } } ================================================ FILE: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/grayReleaseRule/GrayReleaseRulesHolder.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.grayReleaseRule; import com.ctrip.framework.apollo.biz.utils.ReleaseMessageKeyGenerator; import com.google.common.base.Joiner; import com.google.common.base.Strings; import com.google.common.collect.Lists; import com.google.common.collect.Multimap; import com.google.common.collect.Multimaps; import com.google.common.collect.Ordering; import com.google.common.collect.Sets; import com.ctrip.framework.apollo.biz.config.BizConfig; import com.ctrip.framework.apollo.biz.entity.GrayReleaseRule; import com.ctrip.framework.apollo.biz.entity.ReleaseMessage; import com.ctrip.framework.apollo.biz.message.ReleaseMessageListener; import com.ctrip.framework.apollo.biz.message.Topics; import com.ctrip.framework.apollo.biz.repository.GrayReleaseRuleRepository; import com.ctrip.framework.apollo.common.constants.NamespaceBranchStatus; import com.ctrip.framework.apollo.common.dto.GrayReleaseRuleItemDTO; import com.ctrip.framework.apollo.common.utils.GrayReleaseRuleItemTransformer; import com.ctrip.framework.apollo.core.ConfigConsts; import com.ctrip.framework.apollo.core.utils.ApolloThreadFactory; import com.ctrip.framework.apollo.tracer.Tracer; import com.ctrip.framework.apollo.tracer.spi.Transaction; import com.google.common.collect.TreeMultimap; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.InitializingBean; import org.springframework.util.CollectionUtils; import java.util.List; import java.util.Set; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; /** * @author Jason Song(song_s@ctrip.com) */ public class GrayReleaseRulesHolder implements ReleaseMessageListener, InitializingBean { private static final Logger logger = LoggerFactory.getLogger(GrayReleaseRulesHolder.class); private static final Joiner STRING_JOINER = Joiner.on(ConfigConsts.CLUSTER_NAMESPACE_SEPARATOR); private final GrayReleaseRuleRepository grayReleaseRuleRepository; private final BizConfig bizConfig; private int databaseScanInterval; private ScheduledExecutorService executorService; // store configAppId+configCluster+configNamespace -> GrayReleaseRuleCache map private Multimap grayReleaseRuleCache; // store clientAppId+clientNamespace+ip -> ruleId map private Multimap reversedGrayReleaseRuleCache; // store clientAppId+clientNamespace+label -> ruleId map private Multimap reversedGrayReleaseRuleLabelCache; // an auto increment version to indicate the age of rules private AtomicLong loadVersion; public GrayReleaseRulesHolder(final GrayReleaseRuleRepository grayReleaseRuleRepository, final BizConfig bizConfig) { this.grayReleaseRuleRepository = grayReleaseRuleRepository; this.bizConfig = bizConfig; loadVersion = new AtomicLong(); grayReleaseRuleCache = Multimaps.synchronizedSetMultimap( TreeMultimap.create(String.CASE_INSENSITIVE_ORDER, Ordering.natural())); reversedGrayReleaseRuleCache = Multimaps.synchronizedSetMultimap( TreeMultimap.create(String.CASE_INSENSITIVE_ORDER, Ordering.natural())); reversedGrayReleaseRuleLabelCache = Multimaps.synchronizedSetMultimap( TreeMultimap.create(String.CASE_INSENSITIVE_ORDER, Ordering.natural())); executorService = Executors.newScheduledThreadPool(1, ApolloThreadFactory.create("GrayReleaseRulesHolder", true)); } @Override public void afterPropertiesSet() throws Exception { populateDataBaseInterval(); // force sync load for the first time periodicScanRules(); executorService.scheduleWithFixedDelay(this::periodicScanRules, getDatabaseScanIntervalSecond(), getDatabaseScanIntervalSecond(), getDatabaseScanTimeUnit()); } @Override public void handleMessage(ReleaseMessage message, String channel) { logger.info("message received - channel: {}, message: {}", channel, message); String releaseMessage = message.getMessage(); if (!Topics.APOLLO_RELEASE_TOPIC.equals(channel) || Strings.isNullOrEmpty(releaseMessage)) { return; } List keys = ReleaseMessageKeyGenerator.messageToList(releaseMessage); // message should be appId+cluster+namespace if (CollectionUtils.isEmpty(keys)) { return; } String appId = keys.get(0); String cluster = keys.get(1); String namespace = keys.get(2); List rules = grayReleaseRuleRepository .findByAppIdAndClusterNameAndNamespaceName(appId, cluster, namespace); mergeGrayReleaseRules(rules); } private void periodicScanRules() { Transaction transaction = Tracer.newTransaction("Apollo.GrayReleaseRulesScanner", "scanGrayReleaseRules"); try { loadVersion.incrementAndGet(); scanGrayReleaseRules(); transaction.setStatus(Transaction.SUCCESS); } catch (Throwable ex) { transaction.setStatus(ex); logger.error("Scan gray release rule failed", ex); } finally { transaction.complete(); } } public Long findReleaseIdFromGrayReleaseRule(String clientAppId, String clientIp, String clientLabel, String configAppId, String configCluster, String configNamespaceName) { String key = assembleGrayReleaseRuleKey(configAppId, configCluster, configNamespaceName); if (!grayReleaseRuleCache.containsKey(key)) { return null; } // create a new list to avoid ConcurrentModificationException List rules = Lists.newArrayList(grayReleaseRuleCache.get(key)); for (GrayReleaseRuleCache rule : rules) { // check branch status if (rule.getBranchStatus() != NamespaceBranchStatus.ACTIVE) { continue; } if (rule.matches(clientAppId, clientIp, clientLabel)) { return rule.getReleaseId(); } } return null; } /** * Check whether there are gray release rules for the clientAppId, clientIp, clientLabel, namespace combination. * Please note that even there are gray release rules, it doesn't mean it will always load gray * releases. Because gray release rules actually apply to one more dimension - cluster. */ public boolean hasGrayReleaseRule(String clientAppId, String clientIp, String clientLabel, String namespaceName) { // check ip gray rule if (reversedGrayReleaseRuleCache .containsKey(assembleReversedGrayReleaseRuleKey(clientAppId, namespaceName, clientIp)) || reversedGrayReleaseRuleCache.containsKey(assembleReversedGrayReleaseRuleKey(clientAppId, namespaceName, GrayReleaseRuleItemDTO.ALL_IP))) { return true; } // check label gray rule if (!Strings.isNullOrEmpty(clientLabel) && (reversedGrayReleaseRuleLabelCache .containsKey(assembleReversedGrayReleaseRuleKey(clientAppId, namespaceName, clientLabel)) || reversedGrayReleaseRuleLabelCache.containsKey(assembleReversedGrayReleaseRuleKey( clientAppId, namespaceName, GrayReleaseRuleItemDTO.ALL_Label)))) { return true; } return false; } private void scanGrayReleaseRules() { long maxIdScanned = 0; boolean hasMore = true; while (hasMore && !Thread.currentThread().isInterrupted()) { List grayReleaseRules = grayReleaseRuleRepository.findFirst500ByIdGreaterThanOrderByIdAsc(maxIdScanned); if (CollectionUtils.isEmpty(grayReleaseRules)) { break; } mergeGrayReleaseRules(grayReleaseRules); int rulesScanned = grayReleaseRules.size(); maxIdScanned = grayReleaseRules.get(rulesScanned - 1).getId(); // batch is 500 hasMore = rulesScanned == 500; } } private void mergeGrayReleaseRules(List grayReleaseRules) { if (CollectionUtils.isEmpty(grayReleaseRules)) { return; } for (GrayReleaseRule grayReleaseRule : grayReleaseRules) { if (grayReleaseRule.getReleaseId() == null || grayReleaseRule.getReleaseId() == 0) { // filter rules with no release id, i.e. never released continue; } String key = assembleGrayReleaseRuleKey(grayReleaseRule.getAppId(), grayReleaseRule.getClusterName(), grayReleaseRule.getNamespaceName()); // create a new list to avoid ConcurrentModificationException List rules = Lists.newArrayList(grayReleaseRuleCache.get(key)); GrayReleaseRuleCache oldRule = null; for (GrayReleaseRuleCache ruleCache : rules) { if (ruleCache.getBranchName().equals(grayReleaseRule.getBranchName())) { oldRule = ruleCache; break; } } // if old rule is null and new rule's branch status is not active, ignore if (oldRule == null && grayReleaseRule.getBranchStatus() != NamespaceBranchStatus.ACTIVE) { continue; } // use id comparison to avoid synchronization if (oldRule == null || grayReleaseRule.getId() > oldRule.getRuleId()) { addCache(key, transformRuleToRuleCache(grayReleaseRule)); if (oldRule != null) { removeCache(key, oldRule); } } else { if (oldRule.getBranchStatus() == NamespaceBranchStatus.ACTIVE) { // update load version oldRule.setLoadVersion(loadVersion.get()); } else if ((loadVersion.get() - oldRule.getLoadVersion()) > 1) { // remove outdated inactive branch rule after 2 update cycles removeCache(key, oldRule); } } } } private void addCache(String key, GrayReleaseRuleCache ruleCache) { if (ruleCache.getBranchStatus() == NamespaceBranchStatus.ACTIVE) { for (GrayReleaseRuleItemDTO ruleItemDTO : ruleCache.getRuleItems()) { for (String clientIp : ruleItemDTO.getClientIpList()) { reversedGrayReleaseRuleCache .put(assembleReversedGrayReleaseRuleKey(ruleItemDTO.getClientAppId(), ruleCache.getNamespaceName(), clientIp), ruleCache.getRuleId()); } for (String label : ruleItemDTO.getClientLabelList()) { reversedGrayReleaseRuleLabelCache .put(assembleReversedGrayReleaseRuleKey(ruleItemDTO.getClientAppId(), ruleCache.getNamespaceName(), label), ruleCache.getRuleId()); } } } grayReleaseRuleCache.put(key, ruleCache); } private void removeCache(String key, GrayReleaseRuleCache ruleCache) { grayReleaseRuleCache.remove(key, ruleCache); for (GrayReleaseRuleItemDTO ruleItemDTO : ruleCache.getRuleItems()) { for (String clientIp : ruleItemDTO.getClientIpList()) { reversedGrayReleaseRuleCache .remove(assembleReversedGrayReleaseRuleKey(ruleItemDTO.getClientAppId(), ruleCache.getNamespaceName(), clientIp), ruleCache.getRuleId()); } for (String label : ruleItemDTO.getClientLabelList()) { reversedGrayReleaseRuleLabelCache .remove(assembleReversedGrayReleaseRuleKey(ruleItemDTO.getClientAppId(), ruleCache.getNamespaceName(), label), ruleCache.getRuleId()); } } } private GrayReleaseRuleCache transformRuleToRuleCache(GrayReleaseRule grayReleaseRule) { Set ruleItems; try { ruleItems = GrayReleaseRuleItemTransformer.batchTransformFromJSON(grayReleaseRule.getRules()); } catch (Throwable ex) { ruleItems = Sets.newHashSet(); Tracer.logError(ex); logger.error("parse rule for gray release rule {} failed", grayReleaseRule.getId(), ex); } return new GrayReleaseRuleCache(grayReleaseRule.getId(), grayReleaseRule.getBranchName(), grayReleaseRule.getNamespaceName(), grayReleaseRule.getReleaseId(), grayReleaseRule.getBranchStatus(), loadVersion.get(), ruleItems); } private void populateDataBaseInterval() { databaseScanInterval = bizConfig.grayReleaseRuleScanInterval(); } private int getDatabaseScanIntervalSecond() { return databaseScanInterval; } private TimeUnit getDatabaseScanTimeUnit() { return TimeUnit.SECONDS; } private String assembleGrayReleaseRuleKey(String configAppId, String configCluster, String configNamespaceName) { return STRING_JOINER.join(configAppId, configCluster, configNamespaceName); } private String assembleReversedGrayReleaseRuleKey(String clientAppId, String clientNamespaceName, String clientIpOrLabel) { return STRING_JOINER.join(clientAppId, clientNamespaceName, clientIpOrLabel); } } ================================================ FILE: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/message/DatabaseMessageSender.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.message; import com.ctrip.framework.apollo.biz.entity.ReleaseMessage; import com.ctrip.framework.apollo.biz.repository.ReleaseMessageRepository; import com.ctrip.framework.apollo.core.utils.ApolloThreadFactory; import com.ctrip.framework.apollo.tracer.Tracer; import com.ctrip.framework.apollo.tracer.spi.Transaction; import com.google.common.collect.Queues; import jakarta.annotation.PreDestroy; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; import jakarta.annotation.PostConstruct; import java.util.List; import java.util.Objects; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; /** * @author Jason Song(song_s@ctrip.com) */ @Component public class DatabaseMessageSender implements MessageSender { private static final Logger logger = LoggerFactory.getLogger(DatabaseMessageSender.class); private static final int CLEAN_QUEUE_MAX_SIZE = 100; private final BlockingQueue toClean = Queues.newLinkedBlockingQueue(CLEAN_QUEUE_MAX_SIZE); private final ExecutorService cleanExecutorService; private final AtomicBoolean cleanStopped; private final ReleaseMessageRepository releaseMessageRepository; public DatabaseMessageSender(final ReleaseMessageRepository releaseMessageRepository) { cleanExecutorService = Executors .newSingleThreadExecutor(ApolloThreadFactory.create("DatabaseMessageSender", true)); cleanStopped = new AtomicBoolean(false); this.releaseMessageRepository = releaseMessageRepository; } @Override @Transactional public void sendMessage(String message, String channel) { logger.info("Sending message {} to channel {}", message, channel); if (!Objects.equals(channel, Topics.APOLLO_RELEASE_TOPIC)) { logger.warn("Channel {} not supported by DatabaseMessageSender!", channel); return; } Tracer.logEvent("Apollo.AdminService.ReleaseMessage", message); Transaction transaction = Tracer.newTransaction("Apollo.AdminService", "sendMessage"); try { ReleaseMessage newMessage = releaseMessageRepository.save(new ReleaseMessage(message)); if (!toClean.offer(newMessage.getId())) { logger.warn("Queue is full, Failed to add message {} to clean queue", newMessage.getId()); } transaction.setStatus(Transaction.SUCCESS); } catch (Throwable ex) { logger.error("Sending message to database failed", ex); transaction.setStatus(ex); throw ex; } finally { transaction.complete(); } } @PostConstruct private void initialize() { cleanExecutorService.submit(() -> { while (!cleanStopped.get() && !Thread.currentThread().isInterrupted()) { try { Long rm = toClean.poll(1, TimeUnit.SECONDS); if (rm != null) { cleanMessage(rm); } else { TimeUnit.SECONDS.sleep(5); } } catch (Throwable ex) { Tracer.logError(ex); } } }); } private void cleanMessage(Long id) { // double check in case the release message is rolled back ReleaseMessage releaseMessage = releaseMessageRepository.findById(id).orElse(null); if (releaseMessage == null) { return; } boolean hasMore = true; while (hasMore && !Thread.currentThread().isInterrupted()) { List messages = releaseMessageRepository.findFirst100ByMessageAndIdLessThanOrderByIdAsc( releaseMessage.getMessage(), releaseMessage.getId()); releaseMessageRepository.deleteAll(messages); hasMore = messages.size() == 100; messages.forEach(toRemove -> Tracer.logEvent( String.format("ReleaseMessage.Clean.%s", toRemove.getMessage()), String.valueOf(toRemove.getId()))); } } @PreDestroy void stopClean() { cleanStopped.set(true); } } ================================================ FILE: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/message/MessageSender.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.message; /** * @author Jason Song(song_s@ctrip.com) */ public interface MessageSender { void sendMessage(String message, String channel); } ================================================ FILE: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/message/ReleaseMessageListener.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.message; import com.ctrip.framework.apollo.biz.entity.ReleaseMessage; /** * @author Jason Song(song_s@ctrip.com) */ public interface ReleaseMessageListener { void handleMessage(ReleaseMessage message, String channel); } ================================================ FILE: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/message/ReleaseMessageScanner.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.message; import com.google.common.collect.Maps; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.InitializingBean; import org.springframework.util.CollectionUtils; import com.ctrip.framework.apollo.biz.config.BizConfig; import com.ctrip.framework.apollo.biz.entity.ReleaseMessage; import com.ctrip.framework.apollo.biz.repository.ReleaseMessageRepository; import com.ctrip.framework.apollo.core.utils.ApolloThreadFactory; import com.ctrip.framework.apollo.tracer.Tracer; import com.ctrip.framework.apollo.tracer.spi.Transaction; import com.google.common.collect.Lists; /** * @author Jason Song(song_s@ctrip.com) */ public class ReleaseMessageScanner implements InitializingBean { private static final Logger logger = LoggerFactory.getLogger(ReleaseMessageScanner.class); private static final int missingReleaseMessageMaxAge = 10; // hardcoded to 10, could be configured // via BizConfig if necessary private final BizConfig bizConfig; private final ReleaseMessageRepository releaseMessageRepository; private int databaseScanInterval; private final List listeners; private final ScheduledExecutorService executorService; private final Map missingReleaseMessages; // missing release message id => age // counter private long maxIdScanned; public ReleaseMessageScanner(final BizConfig bizConfig, final ReleaseMessageRepository releaseMessageRepository) { this.bizConfig = bizConfig; this.releaseMessageRepository = releaseMessageRepository; listeners = Lists.newCopyOnWriteArrayList(); executorService = Executors.newScheduledThreadPool(1, ApolloThreadFactory.create("ReleaseMessageScanner", true)); missingReleaseMessages = Maps.newHashMap(); } @Override public void afterPropertiesSet() throws Exception { databaseScanInterval = bizConfig.releaseMessageScanIntervalInMilli(); maxIdScanned = loadLargestMessageId(); executorService.scheduleWithFixedDelay(() -> { Transaction transaction = Tracer.newTransaction("Apollo.ReleaseMessageScanner", "scanMessage"); try { scanMissingMessages(); scanMessages(); transaction.setStatus(Transaction.SUCCESS); } catch (Throwable ex) { transaction.setStatus(ex); logger.error("Scan and send message failed", ex); } finally { transaction.complete(); } }, databaseScanInterval, databaseScanInterval, TimeUnit.MILLISECONDS); } /** * add message listeners for release message * @param listener */ public void addMessageListener(ReleaseMessageListener listener) { if (!listeners.contains(listener)) { listeners.add(listener); } } /** * Scan messages, continue scanning until there is no more messages */ private void scanMessages() { boolean hasMoreMessages = true; while (hasMoreMessages && !Thread.currentThread().isInterrupted()) { hasMoreMessages = scanAndSendMessages(); } } /** * scan messages and send * * @return whether there are more messages */ private boolean scanAndSendMessages() { // current batch is 500 List releaseMessages = releaseMessageRepository.findFirst500ByIdGreaterThanOrderByIdAsc(maxIdScanned); if (CollectionUtils.isEmpty(releaseMessages)) { return false; } fireMessageScanned(releaseMessages); int messageScanned = releaseMessages.size(); long newMaxIdScanned = releaseMessages.get(messageScanned - 1).getId(); // check id gaps, possible reasons are release message not committed yet or already rolled back if (newMaxIdScanned - maxIdScanned > messageScanned) { recordMissingReleaseMessageIds(releaseMessages, maxIdScanned); } maxIdScanned = newMaxIdScanned; return messageScanned == 500; } private void scanMissingMessages() { Set missingReleaseMessageIds = missingReleaseMessages.keySet(); Iterable releaseMessages = releaseMessageRepository.findAllById(missingReleaseMessageIds); fireMessageScanned(releaseMessages); releaseMessages.forEach(releaseMessage -> { missingReleaseMessageIds.remove(releaseMessage.getId()); }); growAndCleanMissingMessages(); } private void growAndCleanMissingMessages() { Iterator> iterator = missingReleaseMessages.entrySet().iterator(); while (iterator.hasNext()) { Entry entry = iterator.next(); if (entry.getValue() > missingReleaseMessageMaxAge) { iterator.remove(); } else { entry.setValue(entry.getValue() + 1); } } } private void recordMissingReleaseMessageIds(List messages, long startId) { for (ReleaseMessage message : messages) { long currentId = message.getId(); if (currentId - startId > 1) { for (long i = startId + 1; i < currentId; i++) { missingReleaseMessages.putIfAbsent(i, 1); } } startId = currentId; } } /** * find largest message id as the current start point * @return current largest message id */ private long loadLargestMessageId() { ReleaseMessage releaseMessage = releaseMessageRepository.findTopByOrderByIdDesc(); return releaseMessage == null ? 0 : releaseMessage.getId(); } /** * Notify listeners with messages loaded * @param messages */ private void fireMessageScanned(Iterable messages) { for (ReleaseMessage message : messages) { for (ReleaseMessageListener listener : listeners) { try { listener.handleMessage(message, Topics.APOLLO_RELEASE_TOPIC); } catch (Throwable ex) { Tracer.logError(ex); logger.error("Failed to invoke message listener {}", listener.getClass(), ex); } } } } } ================================================ FILE: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/message/Topics.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.message; /** * @author Jason Song(song_s@ctrip.com) */ public class Topics { public static final String APOLLO_RELEASE_TOPIC = "apollo-release"; } ================================================ FILE: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/registry/DatabaseDiscoveryClient.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.registry; import com.ctrip.framework.apollo.biz.registry.configuration.support.ApolloServiceRegistryProperties; import java.util.List; /** * @see org.springframework.cloud.client.discovery.DiscoveryClient */ public interface DatabaseDiscoveryClient { /** * find by {@link ApolloServiceRegistryProperties#getServiceName()}, * then filter by {@link ApolloServiceRegistryProperties#getCluster()} * * @return empty list if there is no instance */ List getInstances(String serviceName); } ================================================ FILE: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/registry/DatabaseDiscoveryClientAlwaysAddSelfInstanceDecoratorImpl.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.registry; import java.net.URI; import java.util.ArrayList; import java.util.List; import java.util.Objects; /** * decorator pattern *

* when database crash, even cannot register self instance to database, *

* this decorator will ensure return's result contains self instance. */ public class DatabaseDiscoveryClientAlwaysAddSelfInstanceDecoratorImpl implements DatabaseDiscoveryClient { private final DatabaseDiscoveryClient delegate; private final ServiceInstance selfInstance; public DatabaseDiscoveryClientAlwaysAddSelfInstanceDecoratorImpl(DatabaseDiscoveryClient delegate, ServiceInstance selfInstance) { this.delegate = delegate; this.selfInstance = selfInstance; } static boolean containSelf(List serviceInstances, ServiceInstance selfInstance) { final String selfServiceName = selfInstance.getServiceName(); final URI selfUri = selfInstance.getUri(); final String cluster = selfInstance.getCluster(); for (ServiceInstance serviceInstance : serviceInstances) { if (Objects.equals(selfServiceName, serviceInstance.getServiceName())) { if (Objects.equals(selfUri, serviceInstance.getUri())) { if (Objects.equals(cluster, serviceInstance.getCluster())) { return true; } } } } return false; } /** * if the serviceName is same with self, always return self's instance * @return never be empty list when serviceName is same with self */ @Override public List getInstances(String serviceName) { if (Objects.equals(serviceName, this.selfInstance.getServiceName())) { List serviceInstances = this.delegate.getInstances(serviceName); if (containSelf(serviceInstances, this.selfInstance)) { // contains self instance already return serviceInstances; } // add self instance to result List result = new ArrayList<>(serviceInstances.size() + 1); result.add(this.selfInstance); result.addAll(serviceInstances); return result; } else { return this.delegate.getInstances(serviceName); } } } ================================================ FILE: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/registry/DatabaseDiscoveryClientImpl.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.registry; import com.ctrip.framework.apollo.biz.entity.ServiceRegistry; import com.ctrip.framework.apollo.biz.registry.configuration.support.ApolloServiceDiscoveryProperties; import com.ctrip.framework.apollo.biz.registry.configuration.support.ApolloServiceRegistryProperties; import com.ctrip.framework.apollo.biz.service.ServiceRegistryService; import java.time.LocalDateTime; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; public class DatabaseDiscoveryClientImpl implements DatabaseDiscoveryClient { private final ServiceRegistryService serviceRegistryService; private final ApolloServiceDiscoveryProperties discoveryProperties; private final String cluster; public DatabaseDiscoveryClientImpl(ServiceRegistryService serviceRegistryService, ApolloServiceDiscoveryProperties discoveryProperties, String cluster) { this.serviceRegistryService = serviceRegistryService; this.discoveryProperties = discoveryProperties; this.cluster = cluster; } /** * find by {@link ApolloServiceRegistryProperties#getServiceName()} */ @Override public List getInstances(String serviceName) { final List serviceRegistryListFiltered; { LocalDateTime healthTime = LocalDateTime.now() .minusSeconds(this.discoveryProperties.getHealthCheckIntervalInSecond()); List filterByHealthCheck = this.serviceRegistryService .findByServiceNameDataChangeLastModifiedTimeGreaterThan(serviceName, healthTime); serviceRegistryListFiltered = filterByCluster(filterByHealthCheck, this.cluster); } return serviceRegistryListFiltered.stream().map(DatabaseDiscoveryClientImpl::convert) .collect(Collectors.toList()); } static ApolloServiceRegistryProperties convert(ServiceRegistry serviceRegistry) { ApolloServiceRegistryProperties registration = new ApolloServiceRegistryProperties(); registration.setServiceName(serviceRegistry.getServiceName()); registration.setUri(serviceRegistry.getUri()); registration.setCluster(serviceRegistry.getCluster()); return registration; } static List filterByCluster(List list, String cluster) { return list.stream() .filter(serviceRegistry -> Objects.equals(cluster, serviceRegistry.getCluster())) .collect(Collectors.toList()); } } ================================================ FILE: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/registry/DatabaseDiscoveryClientMemoryCacheDecoratorImpl.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.registry; import com.ctrip.framework.apollo.core.ServiceNameConsts; import com.ctrip.framework.apollo.core.utils.ApolloThreadFactory; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * decorator pattern *

* 1. use jvm memory as cache to decrease the read of database. *

* 2. when database happened failure, return the cache in jvm memory. */ public class DatabaseDiscoveryClientMemoryCacheDecoratorImpl implements DatabaseDiscoveryClient { private static final Logger log = LoggerFactory.getLogger(DatabaseDiscoveryClientMemoryCacheDecoratorImpl.class); private final DatabaseDiscoveryClient delegate; private final Map> serviceName2ServiceInstances = new ConcurrentHashMap<>(8); private volatile ScheduledExecutorService scheduledExecutorService; private static final long SYNC_TASK_PERIOD_IN_SECOND = 5; public DatabaseDiscoveryClientMemoryCacheDecoratorImpl(DatabaseDiscoveryClient delegate) { this.delegate = delegate; } public void init() { this.scheduledExecutorService = Executors.newSingleThreadScheduledExecutor( ApolloThreadFactory.create("DatabaseDiscoveryWithCache", true)); scheduledExecutorService.scheduleAtFixedRate(this::updateCacheTask, SYNC_TASK_PERIOD_IN_SECOND, SYNC_TASK_PERIOD_IN_SECOND, TimeUnit.SECONDS); // load them for init try { this.getInstances(ServiceNameConsts.APOLLO_CONFIGSERVICE); } catch (Throwable t) { log.error("fail to get instances of service name {}", ServiceNameConsts.APOLLO_CONFIGSERVICE, t); } try { this.getInstances(ServiceNameConsts.APOLLO_ADMINSERVICE); } catch (Throwable t) { log.error("fail to get instances of service name {}", ServiceNameConsts.APOLLO_ADMINSERVICE, t); } } void updateCacheTask() { try { // for each service name, update their service instances in memory this.serviceName2ServiceInstances .replaceAll((serviceName, serviceInstances) -> this.delegate.getInstances(serviceName)); } catch (Throwable t) { log.error("fail to read service instances from database", t); } } List readFromDatabase(String serviceName) { return this.delegate.getInstances(serviceName); } /** * never throw {@link Throwable}, read from memory cache */ @Override public List getInstances(String serviceName) { // put serviceName as key to map, // then the task use it to read service instances from database this.serviceName2ServiceInstances.computeIfAbsent(serviceName, this::readFromDatabase); // get from cache return this.serviceName2ServiceInstances.getOrDefault(serviceName, Collections.emptyList()); } } ================================================ FILE: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/registry/DatabaseServiceRegistry.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.registry; /** * @see org.springframework.cloud.client.serviceregistry.ServiceRegistry */ public interface DatabaseServiceRegistry { /** * register an instance to database */ void register(ServiceInstance instance); /** * remove an instance from database */ void deregister(ServiceInstance instance); } ================================================ FILE: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/registry/DatabaseServiceRegistryImpl.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.registry; import com.ctrip.framework.apollo.biz.entity.ServiceRegistry; import com.ctrip.framework.apollo.biz.service.ServiceRegistryService; public class DatabaseServiceRegistryImpl implements DatabaseServiceRegistry { private final ServiceRegistryService serviceRegistryService; public DatabaseServiceRegistryImpl(ServiceRegistryService serviceRegistryService) { this.serviceRegistryService = serviceRegistryService; } static ServiceRegistry convert(ServiceInstance instance) { ServiceRegistry serviceRegistry = new ServiceRegistry(); serviceRegistry.setServiceName(instance.getServiceName()); serviceRegistry.setUri(instance.getUri().toString()); serviceRegistry.setCluster(instance.getCluster()); serviceRegistry.setMetadata(instance.getMetadata()); return serviceRegistry; } @Override public void register(ServiceInstance instance) { ServiceRegistry serviceRegistry = convert(instance); this.serviceRegistryService.saveIfNotExistByServiceNameAndUri(serviceRegistry); } @Override public void deregister(ServiceInstance instance) { ServiceRegistry serviceRegistry = convert(instance); this.serviceRegistryService.delete(serviceRegistry); } } ================================================ FILE: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/registry/ServiceInstance.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.registry; import java.net.URI; import java.util.Map; /** * @see org.springframework.cloud.client.ServiceInstance */ public interface ServiceInstance { /** * @return The service ID as registered. */ String getServiceName(); /** * get the uri of a service instance, for example: *

* * @return The service URI address. */ URI getUri(); /** * Tag a service instance for service discovery. *

* so use cluster for service discovery. * * @return The cluster of the service instance. */ String getCluster(); /** * @return The key / value pair metadata associated with the service instance. * @see org.springframework.cloud.client.ServiceInstance#getMetadata() */ Map getMetadata(); } ================================================ FILE: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/registry/configuration/ApolloServiceDiscoveryAutoConfiguration.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.registry.configuration; import com.ctrip.framework.apollo.biz.registry.DatabaseDiscoveryClient; import com.ctrip.framework.apollo.biz.registry.DatabaseDiscoveryClientAlwaysAddSelfInstanceDecoratorImpl; import com.ctrip.framework.apollo.biz.registry.DatabaseDiscoveryClientImpl; import com.ctrip.framework.apollo.biz.registry.DatabaseDiscoveryClientMemoryCacheDecoratorImpl; import com.ctrip.framework.apollo.biz.registry.ServiceInstance; import com.ctrip.framework.apollo.biz.registry.configuration.support.ApolloServiceRegistryClearApplicationRunner; import com.ctrip.framework.apollo.biz.registry.configuration.support.ApolloServiceDiscoveryProperties; import com.ctrip.framework.apollo.biz.service.ServiceRegistryService; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration @ConditionalOnProperty(prefix = ApolloServiceDiscoveryProperties.PREFIX, value = "enabled") @EnableConfigurationProperties({ApolloServiceDiscoveryProperties.class,}) public class ApolloServiceDiscoveryAutoConfiguration { private static DatabaseDiscoveryClient wrapMemoryCache(DatabaseDiscoveryClient discoveryClient) { DatabaseDiscoveryClientMemoryCacheDecoratorImpl decorator = new DatabaseDiscoveryClientMemoryCacheDecoratorImpl(discoveryClient); decorator.init(); return decorator; } private static DatabaseDiscoveryClient wrapAlwaysAddSelfInstance( DatabaseDiscoveryClient discoveryClient, ServiceInstance selfInstance) { return new DatabaseDiscoveryClientAlwaysAddSelfInstanceDecoratorImpl(discoveryClient, selfInstance); } @Bean @ConditionalOnMissingBean public DatabaseDiscoveryClient databaseDiscoveryClient( ApolloServiceDiscoveryProperties discoveryProperties, ServiceInstance selfServiceInstance, ServiceRegistryService serviceRegistryService) { DatabaseDiscoveryClient discoveryClient = new DatabaseDiscoveryClientImpl( serviceRegistryService, discoveryProperties, selfServiceInstance.getCluster()); return wrapMemoryCache(wrapAlwaysAddSelfInstance(discoveryClient, selfServiceInstance)); } @Bean @ConditionalOnMissingBean public ApolloServiceRegistryClearApplicationRunner apolloServiceRegistryClearApplicationRunner( ServiceRegistryService serviceRegistryService) { return new ApolloServiceRegistryClearApplicationRunner(serviceRegistryService); } } ================================================ FILE: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/registry/configuration/ApolloServiceRegistryAutoConfiguration.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.registry.configuration; import com.ctrip.framework.apollo.biz.registry.DatabaseServiceRegistry; import com.ctrip.framework.apollo.biz.registry.DatabaseServiceRegistryImpl; import com.ctrip.framework.apollo.biz.registry.configuration.support.ApolloServiceRegistryDeregisterApplicationListener; import com.ctrip.framework.apollo.biz.registry.configuration.support.ApolloServiceRegistryHeartbeatApplicationRunner; import com.ctrip.framework.apollo.biz.registry.configuration.support.ApolloServiceRegistryProperties; import com.ctrip.framework.apollo.biz.repository.ServiceRegistryRepository; import com.ctrip.framework.apollo.biz.service.ServiceRegistryService; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration @ConditionalOnProperty(prefix = ApolloServiceRegistryProperties.PREFIX, value = "enabled") @EnableConfigurationProperties(ApolloServiceRegistryProperties.class) public class ApolloServiceRegistryAutoConfiguration { @Bean @ConditionalOnMissingBean public ServiceRegistryService registryService(ServiceRegistryRepository repository) { return new ServiceRegistryService(repository); } @Bean @ConditionalOnMissingBean public DatabaseServiceRegistry databaseServiceRegistry( ServiceRegistryService serviceRegistryService) { return new DatabaseServiceRegistryImpl(serviceRegistryService); } @Bean @ConditionalOnMissingBean public ApolloServiceRegistryHeartbeatApplicationRunner apolloServiceRegistryHeartbeatApplicationRunner( ApolloServiceRegistryProperties registration, DatabaseServiceRegistry serviceRegistry) { return new ApolloServiceRegistryHeartbeatApplicationRunner(registration, serviceRegistry); } @Bean @ConditionalOnMissingBean public ApolloServiceRegistryDeregisterApplicationListener apolloServiceRegistryDeregisterApplicationListener( ApolloServiceRegistryProperties registration, DatabaseServiceRegistry serviceRegistry) { return new ApolloServiceRegistryDeregisterApplicationListener(registration, serviceRegistry); } } ================================================ FILE: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/registry/configuration/support/ApolloServiceDiscoveryProperties.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.registry.configuration.support; import org.springframework.boot.context.properties.ConfigurationProperties; /** * @see org.springframework.cloud.consul.discovery.ConsulDiscoveryProperties * @see org.springframework.cloud.consul.ConsulProperties * @see org.springframework.cloud.netflix.eureka.EurekaInstanceConfigBean */ @ConfigurationProperties(prefix = ApolloServiceDiscoveryProperties.PREFIX) public class ApolloServiceDiscoveryProperties { public static final String PREFIX = "apollo.service.discovery"; /** * enable discovery of registry or not */ private boolean enabled = false; /** * health check interval. *

* if current time - the last time of instance's heartbeat < healthCheckInterval, *

* then this instance is healthy. */ private long healthCheckIntervalInSecond = 61; public long getHealthCheckIntervalInSecond() { return healthCheckIntervalInSecond; } public void setHealthCheckIntervalInSecond(long healthCheckIntervalInSecond) { this.healthCheckIntervalInSecond = healthCheckIntervalInSecond; } public boolean isEnabled() { return enabled; } public void setEnabled(boolean enabled) { this.enabled = enabled; } } ================================================ FILE: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/registry/configuration/support/ApolloServiceRegistryClearApplicationRunner.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.registry.configuration.support; import com.ctrip.framework.apollo.biz.entity.ServiceRegistry; import com.ctrip.framework.apollo.biz.service.ServiceRegistryService; import com.ctrip.framework.apollo.core.utils.ApolloThreadFactory; import java.time.Duration; import java.util.List; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationRunner; /** * clear the unhealthy instances. */ public class ApolloServiceRegistryClearApplicationRunner implements ApplicationRunner { private static final Logger log = LoggerFactory.getLogger(ApolloServiceRegistryClearApplicationRunner.class); /** * for {@link #clearUnhealthyInstances()} */ private final ScheduledExecutorService instanceClearScheduledExecutorService; private final ServiceRegistryService serviceRegistryService; public ApolloServiceRegistryClearApplicationRunner( ServiceRegistryService serviceRegistryService) { this.serviceRegistryService = serviceRegistryService; this.instanceClearScheduledExecutorService = Executors.newSingleThreadScheduledExecutor( ApolloThreadFactory.create("ApolloRegistryServerClearInstances", true)); } /** * clear instance */ void clearUnhealthyInstances() { try { List serviceRegistryListDeleted = this.serviceRegistryService.deleteTimeBefore(Duration.ofDays(1)); if (serviceRegistryListDeleted != null && !serviceRegistryListDeleted.isEmpty()) { log.info("clear {} unhealthy instances by scheduled task", serviceRegistryListDeleted.size()); } } catch (Throwable t) { log.error("fail to clear unhealthy instances by scheduled task", t); } } @Override public void run(ApplicationArguments args) throws Exception { this.instanceClearScheduledExecutorService.scheduleAtFixedRate(this::clearUnhealthyInstances, 0, 1, TimeUnit.DAYS); } } ================================================ FILE: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/registry/configuration/support/ApolloServiceRegistryDeregisterApplicationListener.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.registry.configuration.support; import com.ctrip.framework.apollo.biz.registry.DatabaseServiceRegistry; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.ApplicationListener; import org.springframework.context.event.ContextClosedEvent; /** * remove self before shutdown */ public class ApolloServiceRegistryDeregisterApplicationListener implements ApplicationListener { private static final Logger log = LoggerFactory.getLogger(ApolloServiceRegistryDeregisterApplicationListener.class); private final ApolloServiceRegistryProperties registration; private final DatabaseServiceRegistry serviceRegistry; public ApolloServiceRegistryDeregisterApplicationListener( ApolloServiceRegistryProperties registration, DatabaseServiceRegistry serviceRegistry) { this.registration = registration; this.serviceRegistry = serviceRegistry; } @Override public void onApplicationEvent(ContextClosedEvent event) { this.deregister(); } private void deregister() { try { this.serviceRegistry.deregister(this.registration); log.info("deregister success, '{}' uri '{}', cluster '{}'", this.registration.getServiceName(), this.registration.getUri(), this.registration.getCluster()); } catch (Throwable t) { log.error("deregister fail, '{}' uri '{}', cluster '{}'", this.registration.getServiceName(), this.registration.getUri(), this.registration.getCluster(), t); } } } ================================================ FILE: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/registry/configuration/support/ApolloServiceRegistryHeartbeatApplicationRunner.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.registry.configuration.support; import com.ctrip.framework.apollo.biz.registry.DatabaseServiceRegistry; import com.ctrip.framework.apollo.core.utils.ApolloThreadFactory; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationRunner; /** * send heartbeat on runtime. */ public class ApolloServiceRegistryHeartbeatApplicationRunner implements ApplicationRunner { private static final Logger log = LoggerFactory.getLogger(ApolloServiceRegistryHeartbeatApplicationRunner.class); private final ApolloServiceRegistryProperties registration; private final DatabaseServiceRegistry serviceRegistry; /** * for {@link #heartbeat()} */ private final ScheduledExecutorService heartbeatScheduledExecutorService; public ApolloServiceRegistryHeartbeatApplicationRunner( ApolloServiceRegistryProperties registration, DatabaseServiceRegistry serviceRegistry) { this.registration = registration; this.serviceRegistry = serviceRegistry; this.heartbeatScheduledExecutorService = Executors.newSingleThreadScheduledExecutor( ApolloThreadFactory.create("ApolloServiceRegistryHeartBeat", true)); } @Override public void run(ApplicationArguments args) throws Exception { // register log.info("register to database. '{}': uri '{}', cluster '{}' ", this.registration.getServiceName(), this.registration.getUri(), this.registration.getCluster()); // heartbeat as same as register this.heartbeatScheduledExecutorService.scheduleAtFixedRate(this::heartbeat, 0, this.registration.getHeartbeatIntervalInSecond(), TimeUnit.SECONDS); } private void heartbeat() { try { this.serviceRegistry.register(this.registration); } catch (Throwable t) { log.error("fail to send heartbeat by scheduled task", t); } } } ================================================ FILE: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/registry/configuration/support/ApolloServiceRegistryProperties.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.registry.configuration.support; import com.ctrip.framework.apollo.biz.registry.ServiceInstance; import com.google.common.base.Strings; import java.net.URI; import java.util.HashMap; import java.util.Map; import jakarta.annotation.PostConstruct; import jakarta.servlet.ServletContext; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.cloud.commons.util.InetUtils; import org.springframework.core.env.PropertyResolver; /** * config of register. * * @see com.ctrip.framework.apollo.core.dto.ServiceDTO * @see org.springframework.cloud.netflix.eureka.EurekaClientConfigBean * @see org.springframework.cloud.netflix.eureka.EurekaInstanceConfigBean */ @ConfigurationProperties(prefix = ApolloServiceRegistryProperties.PREFIX) public class ApolloServiceRegistryProperties implements ServiceInstance { public static final String PREFIX = "apollo.service.registry"; /** * register self to registry or not */ private boolean enabled; /** * @see com.ctrip.framework.apollo.core.ServiceNameConsts#APOLLO_CONFIGSERVICE * @see com.ctrip.framework.apollo.core.ServiceNameConsts#APOLLO_ADMINSERVICE */ private String serviceName; /** * @see ServiceInstance#getUri() */ private URI uri; /** * @see ServiceInstance#getCluster() */ private String cluster; private Map metadata = new HashMap<>(8); /** * heartbeat to registry in second. */ private long heartbeatIntervalInSecond = 10; @Autowired private PropertyResolver propertyResolver; @Autowired private InetUtils inetUtils; @Autowired private ServletContext servletContext; /** * if user doesn't config, then resolve them on the runtime. */ @PostConstruct public void postConstruct() { if (this.serviceName == null) { this.serviceName = propertyResolver.getRequiredProperty("spring.application.name"); } if (this.uri == null) { String host = this.inetUtils.findFirstNonLoopbackHostInfo().getIpAddress(); Integer port = propertyResolver.getRequiredProperty("server.port", Integer.class); String contextPath = Strings.isNullOrEmpty(this.servletContext.getContextPath()) ? "/" : this.servletContext.getContextPath(); String uriString = "http://" + host + ":" + port + contextPath; this.uri = URI.create(uriString); } } public boolean isEnabled() { return enabled; } public void setEnabled(boolean enabled) { this.enabled = enabled; } @Override public String getServiceName() { return serviceName; } @Override public URI getUri() { return this.uri; } /** * custom the uri * @see ServiceInstance#getUri() */ public void setUri(String uri) { this.uri = URI.create(uri); } public void setServiceName(String serviceName) { this.serviceName = serviceName; } @Override public String getCluster() { return cluster; } public void setCluster(String cluster) { this.cluster = cluster; } @Override public Map getMetadata() { return this.metadata; } public void setMetadata(Map metadata) { this.metadata = metadata; } public long getHeartbeatIntervalInSecond() { return heartbeatIntervalInSecond; } public void setHeartbeatIntervalInSecond(long heartbeatIntervalInSecond) { this.heartbeatIntervalInSecond = heartbeatIntervalInSecond; } } ================================================ FILE: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/registry/package-info.java ================================================ /* * Copyright 2024 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. * */ /** * Use database as a registry without spring cloud. *

* Maybe drop spring cloud in the feature. */ package com.ctrip.framework.apollo.biz.registry; ================================================ FILE: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/repository/AccessKeyRepository.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.repository; import com.ctrip.framework.apollo.biz.entity.AccessKey; import java.util.Date; import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; public interface AccessKeyRepository extends JpaRepository { long countByAppId(String appId); AccessKey findOneByAppIdAndId(String appId, long id); List findByAppId(String appId); List findFirst500ByDataChangeLastModifiedTimeGreaterThanOrderByDataChangeLastModifiedTimeAsc( Date date); List findFirst500ByDataChangeLastModifiedTimeGreaterThanEqualAndDataChangeLastModifiedTimeLessThanOrderByDataChangeLastModifiedTimeAsc( Date start, Date end); List findByDataChangeLastModifiedTime(Date date); } ================================================ FILE: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/repository/AppNamespaceRepository.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.repository; import com.ctrip.framework.apollo.common.entity.AppNamespace; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.JpaRepository; import java.util.List; import java.util.Set; public interface AppNamespaceRepository extends JpaRepository { AppNamespace findByAppIdAndName(String appId, String namespaceName); List findByAppIdAndNameIn(String appId, Set namespaceNames); AppNamespace findByNameAndIsPublicTrue(String namespaceName); List findByNameInAndIsPublicTrue(Set namespaceNames); List findByAppIdAndIsPublic(String appId, boolean isPublic); List findByAppIdOrderByIdAsc(String appId); List findFirst500ByIdGreaterThanOrderByIdAsc(long id); @Modifying @Query("UPDATE AppNamespace SET isDeleted = true, " + "deletedAt = :#{T(java.lang.System).currentTimeMillis()}, " + "dataChangeLastModifiedBy = ?2 WHERE appId=?1 and isDeleted = false") int batchDeleteByAppId(String appId, String operator); @Modifying @Query("UPDATE AppNamespace SET isDeleted = true, " + "deletedAt = :#{T(java.lang.System).currentTimeMillis()}, " + "dataChangeLastModifiedBy = ?3 WHERE appId=?1 and name = ?2 and isDeleted = false") int delete(String appId, String namespaceName, String operator); } ================================================ FILE: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/repository/AppRepository.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.repository; import com.ctrip.framework.apollo.common.entity.App; import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.repository.query.Param; import java.util.List; public interface AppRepository extends JpaRepository { @Query("SELECT a from App a WHERE a.name LIKE %:name%") List findByName(@Param("name") String name); App findByAppId(String appId); } ================================================ FILE: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/repository/AuditRepository.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.repository; import com.ctrip.framework.apollo.biz.entity.Audit; import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.repository.query.Param; import java.util.List; public interface AuditRepository extends JpaRepository { @Query("SELECT a from Audit a WHERE a.dataChangeCreatedBy = :owner") List findByOwner(@Param("owner") String owner); @Query("SELECT a from Audit a WHERE a.dataChangeCreatedBy = :owner AND a.entityName =:entity AND a.opName = :op") List findAudits(@Param("owner") String owner, @Param("entity") String entity, @Param("op") String op); } ================================================ FILE: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/repository/ClusterRepository.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.repository; import com.ctrip.framework.apollo.biz.entity.Cluster; import org.springframework.data.jpa.repository.JpaRepository; import java.util.List; public interface ClusterRepository extends JpaRepository { List findByAppIdAndParentClusterId(String appId, Long parentClusterId); List findByAppId(String appId); Cluster findByAppIdAndName(String appId, String name); List findByParentClusterId(Long parentClusterId); } ================================================ FILE: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/repository/CommitRepository.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.repository; import com.ctrip.framework.apollo.biz.entity.Commit; import java.util.Date; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.JpaRepository; import java.util.List; public interface CommitRepository extends JpaRepository { List findByAppIdAndClusterNameAndNamespaceNameOrderByIdDesc(String appId, String clusterName, String namespaceName, Pageable pageable); List findByAppIdAndClusterNameAndNamespaceNameAndDataChangeLastModifiedTimeGreaterThanEqualOrderByIdDesc( String appId, String clusterName, String namespaceName, Date dataChangeLastModifiedTime, Pageable pageable); @Modifying @Query("update Commit set isDeleted = true, " + "deletedAt = :#{T(java.lang.System).currentTimeMillis()}, " + "dataChangeLastModifiedBy = ?4 where appId=?1 and clusterName=?2 " + "and namespaceName = ?3 and isDeleted = false") int batchDelete(String appId, String clusterName, String namespaceName, String operator); List findByAppIdAndClusterNameAndNamespaceNameAndChangeSetsLikeOrderByIdDesc(String appId, String clusterName, String namespaceName, String changeSets, Pageable page); } ================================================ FILE: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/repository/GrayReleaseRuleRepository.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.repository; import com.ctrip.framework.apollo.biz.entity.GrayReleaseRule; import org.springframework.data.jpa.repository.JpaRepository; import java.util.List; public interface GrayReleaseRuleRepository extends JpaRepository { GrayReleaseRule findTopByAppIdAndClusterNameAndNamespaceNameAndBranchNameOrderByIdDesc( String appId, String clusterName, String namespaceName, String branchName); List findByAppIdAndClusterNameAndNamespaceName(String appId, String clusterName, String namespaceName); List findFirst500ByIdGreaterThanOrderByIdAsc(Long id); } ================================================ FILE: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/repository/InstanceConfigRepository.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.repository; import com.ctrip.framework.apollo.biz.entity.InstanceConfig; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.repository.query.Param; import java.util.Date; import java.util.List; import java.util.Set; public interface InstanceConfigRepository extends JpaRepository { InstanceConfig findByInstanceIdAndConfigAppIdAndConfigNamespaceName(long instanceId, String configAppId, String configNamespaceName); Page findByReleaseKeyAndDataChangeLastModifiedTimeAfter(String releaseKey, Date validDate, Pageable pageable); Page findByConfigAppIdAndConfigClusterNameAndConfigNamespaceNameAndDataChangeLastModifiedTimeAfter( String appId, String clusterName, String namespaceName, Date validDate, Pageable pageable); List findByConfigAppIdAndConfigClusterNameAndConfigNamespaceNameAndDataChangeLastModifiedTimeAfterAndReleaseKeyNotIn( String appId, String clusterName, String namespaceName, Date validDate, Set releaseKey); @Modifying @Query("delete from InstanceConfig where configAppId=?1 and configClusterName=?2 " + "and configNamespaceName = ?3") int batchDelete(String appId, String clusterName, String namespaceName); @Query( value = "select b.id from InstanceConfig a, Instance b where b.id = a.instanceId " + "and a.configAppId = :configAppId and a.configClusterName = :clusterName " + "and a.configNamespaceName = :namespaceName and a.dataChangeLastModifiedTime " + "> :validDate and b.appId = :instanceAppId", countQuery = "select count(b.id) from InstanceConfig a, Instance b where b.id = a.instanceId " + "and a.configAppId = :configAppId and a.configClusterName = :clusterName " + "and a.configNamespaceName = :namespaceName and a.dataChangeLastModifiedTime " + "> :validDate and b.appId = :instanceAppId") Page findInstanceIdsByNamespaceAndInstanceAppId( @Param("instanceAppId") String instanceAppId, @Param("configAppId") String configAppId, @Param("clusterName") String clusterName, @Param("namespaceName") String namespaceName, @Param("validDate") Date validDate, Pageable pageable); } ================================================ FILE: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/repository/InstanceRepository.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.repository; import com.ctrip.framework.apollo.biz.entity.Instance; import org.springframework.data.jpa.repository.JpaRepository; public interface InstanceRepository extends JpaRepository { Instance findByAppIdAndClusterNameAndDataCenterAndIp(String appId, String clusterName, String dataCenter, String ip); } ================================================ FILE: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/repository/ItemRepository.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.repository; import com.ctrip.framework.apollo.biz.entity.Item; import com.ctrip.framework.apollo.common.dto.ItemInfoDTO; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.repository.query.Param; import java.util.Date; import java.util.List; public interface ItemRepository extends JpaRepository { Item findByNamespaceIdAndKey(Long namespaceId, String key); List findByNamespaceIdOrderByLineNumAsc(Long namespaceId); List findByNamespaceId(Long namespaceId); List findByNamespaceIdAndDataChangeLastModifiedTimeGreaterThan(Long namespaceId, Date date); Page findByKey(String key, Pageable pageable); Page findByNamespaceId(Long namespaceId, Pageable pageable); Item findFirst1ByNamespaceIdOrderByLineNumDesc(Long namespaceId); @Query("SELECT new com.ctrip.framework.apollo.common.dto.ItemInfoDTO(n.appId, n.clusterName, n.namespaceName, i.key, i.value) " + "FROM Item i RIGHT JOIN Namespace n ON i.namespaceId = n.id " + "WHERE i.key LIKE %:key% AND i.value LIKE %:value% AND i.isDeleted = false") Page findItemsByKeyAndValueLike(@Param("key") String key, @Param("value") String value, Pageable pageable); @Query("SELECT new com.ctrip.framework.apollo.common.dto.ItemInfoDTO(n.appId, n.clusterName, n.namespaceName, i.key, i.value) " + "FROM Item i RIGHT JOIN Namespace n ON i.namespaceId = n.id " + "WHERE i.key LIKE %:key% AND i.isDeleted = false") Page findItemsByKeyLike(@Param("key") String key, Pageable pageable); @Query("SELECT new com.ctrip.framework.apollo.common.dto.ItemInfoDTO(n.appId, n.clusterName, n.namespaceName, i.key, i.value) " + "FROM Item i RIGHT JOIN Namespace n ON i.namespaceId = n.id " + "WHERE i.value LIKE %:value% AND i.isDeleted = false") Page findItemsByValueLike(@Param("value") String value, Pageable pageable); @Modifying @Query("update Item set isDeleted = true, " + "deletedAt = :#{T(java.lang.System).currentTimeMillis()}, " + "dataChangeLastModifiedBy = ?2 where namespaceId = ?1 and isDeleted = false") int deleteByNamespaceId(long namespaceId, String operator); @Query("select count(*) from Item where namespaceId = :namespaceId and key <>''") int countByNamespaceIdAndFilterKeyEmpty(@Param("namespaceId") long namespaceId); } ================================================ FILE: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/repository/NamespaceLockRepository.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.repository; import com.ctrip.framework.apollo.biz.entity.NamespaceLock; import org.springframework.data.jpa.repository.JpaRepository; public interface NamespaceLockRepository extends JpaRepository { NamespaceLock findByNamespaceId(Long namespaceId); Long deleteByNamespaceId(Long namespaceId); } ================================================ FILE: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/repository/NamespaceRepository.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.repository; import com.ctrip.framework.apollo.biz.entity.Namespace; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.JpaRepository; import java.util.List; import java.util.Set; public interface NamespaceRepository extends JpaRepository { List findByAppIdAndClusterNameOrderByIdAsc(String appId, String clusterName); Namespace findByAppIdAndClusterNameAndNamespaceName(String appId, String clusterName, String namespaceName); @Modifying @Query("update Namespace set isDeleted = true, " + "deletedAt = :#{T(java.lang.System).currentTimeMillis()}, " + "dataChangeLastModifiedBy = ?3 where appId=?1 and clusterName=?2 and isDeleted = false") int batchDelete(String appId, String clusterName, String operator); List findByAppIdAndNamespaceNameOrderByIdAsc(String appId, String namespaceName); List findByNamespaceName(String namespaceName, Pageable page); List findByIdIn(Set namespaceIds); int countByNamespaceNameAndAppIdNot(String namespaceName, String appId); int countByAppIdAndClusterName(String appId, String clusterName); } ================================================ FILE: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/repository/PrivilegeRepository.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.repository; import com.ctrip.framework.apollo.biz.entity.Privilege; import org.springframework.data.jpa.repository.JpaRepository; import java.util.List; public interface PrivilegeRepository extends JpaRepository { List findByNamespaceId(long namespaceId); List findByNamespaceIdAndPrivilType(long namespaceId, String privilType); Privilege findByNamespaceIdAndNameAndPrivilType(long namespaceId, String name, String privilType); } ================================================ FILE: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/repository/ReleaseHistoryRepository.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.repository; import com.ctrip.framework.apollo.biz.entity.ReleaseHistory; import java.util.List; import java.util.Set; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.JpaRepository; /** * @author Jason Song(song_s@ctrip.com) */ public interface ReleaseHistoryRepository extends JpaRepository { Page findByAppIdAndClusterNameAndNamespaceNameOrderByIdDesc(String appId, String clusterName, String namespaceName, Pageable pageable); Page findByReleaseIdAndOperationOrderByIdDesc(long releaseId, int operation, Pageable pageable); Page findByPreviousReleaseIdAndOperationOrderByIdDesc(long previousReleaseId, int operation, Pageable pageable); Page findByReleaseIdAndOperationInOrderByIdDesc(long releaseId, Set operations, Pageable pageable); @Modifying @Query("update ReleaseHistory set isDeleted = true, " + "deletedAt = :#{T(java.lang.System).currentTimeMillis()}, " + "dataChangeLastModifiedBy = ?4 where appId=?1 and clusterName=?2 " + "and namespaceName = ?3 and isDeleted = false") int batchDelete(String appId, String clusterName, String namespaceName, String operator); Page findByAppIdAndClusterNameAndNamespaceNameAndBranchNameOrderByIdDesc( String appId, String clusterName, String namespaceName, String branchName, Pageable pageable); List findFirst100ByAppIdAndClusterNameAndNamespaceNameAndBranchNameAndIdLessThanEqualOrderByIdAsc( String appId, String clusterName, String namespaceName, String branchName, long maxId); } ================================================ FILE: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/repository/ReleaseMessageRepository.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.repository; import com.ctrip.framework.apollo.biz.entity.ReleaseMessage; import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.repository.query.Param; import java.util.Collection; import java.util.List; /** * @author Jason Song(song_s@ctrip.com) */ public interface ReleaseMessageRepository extends JpaRepository { List findFirst500ByIdGreaterThanOrderByIdAsc(Long id); ReleaseMessage findTopByOrderByIdDesc(); ReleaseMessage findTopByMessageInOrderByIdDesc(Collection messages); List findFirst100ByMessageAndIdLessThanOrderByIdAsc(String message, Long id); @Query("select message, max(id) as id from ReleaseMessage where message in :messages group by message") List findLatestReleaseMessagesGroupByMessages( @Param("messages") Collection messages); } ================================================ FILE: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/repository/ReleaseRepository.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.repository; import com.ctrip.framework.apollo.biz.entity.Release; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.repository.query.Param; import java.util.List; import java.util.Set; /** * @author Jason Song(song_s@ctrip.com) */ public interface ReleaseRepository extends JpaRepository { Release findFirstByAppIdAndClusterNameAndNamespaceNameAndIsAbandonedFalseOrderByIdDesc( @Param("appId") String appId, @Param("clusterName") String clusterName, @Param("namespaceName") String namespaceName); Release findByIdAndIsAbandonedFalse(long id); Release findByReleaseKey(String releaseKey); List findByAppIdAndClusterNameAndNamespaceNameOrderByIdDesc(String appId, String clusterName, String namespaceName, Pageable page); List findByAppIdAndClusterNameAndNamespaceNameAndIsAbandonedFalseOrderByIdDesc( String appId, String clusterName, String namespaceName, Pageable page); List findByAppIdAndClusterNameAndNamespaceNameAndIsAbandonedFalseAndIdBetweenOrderByIdDesc( String appId, String clusterName, String namespaceName, long fromId, long toId); List findByReleaseKeyIn(Set releaseKeys); List findByIdIn(Set releaseIds); @Modifying @Query("update Release set isDeleted = true, " + "deletedAt = :#{T(java.lang.System).currentTimeMillis()}, " + "dataChangeLastModifiedBy = ?4 where appId=?1 and clusterName=?2 " + "and namespaceName = ?3 and isDeleted = false") int batchDelete(String appId, String clusterName, String namespaceName, String operator); // For release history conversion program, need to delete after conversion it done List findByAppIdAndClusterNameAndNamespaceNameOrderByIdAsc(String appId, String clusterName, String namespaceName); } ================================================ FILE: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/repository/ServerConfigRepository.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.repository; import com.ctrip.framework.apollo.biz.entity.ServerConfig; import org.springframework.data.jpa.repository.JpaRepository; /** * @author Jason Song(song_s@ctrip.com) */ public interface ServerConfigRepository extends JpaRepository { ServerConfig findTopByKeyAndCluster(String key, String cluster); ServerConfig findByKey(String key); } ================================================ FILE: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/repository/ServiceRegistryRepository.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.repository; import com.ctrip.framework.apollo.biz.entity.ServiceRegistry; import java.time.LocalDateTime; import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; public interface ServiceRegistryRepository extends JpaRepository { List findByServiceNameAndDataChangeLastModifiedTimeGreaterThan( String serviceName, LocalDateTime localDateTime); ServiceRegistry findByServiceNameAndUri(String serviceName, String uri); List deleteByDataChangeLastModifiedTimeLessThan(LocalDateTime localDateTime); int deleteByServiceNameAndUri(String serviceName, String uri); } ================================================ FILE: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/service/AccessKeyService.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.service; import com.ctrip.framework.apollo.biz.entity.AccessKey; import com.ctrip.framework.apollo.biz.entity.Audit; import com.ctrip.framework.apollo.biz.repository.AccessKeyRepository; import com.ctrip.framework.apollo.common.exception.BadRequestException; import java.util.List; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; /** * @author nisiyong */ @Service public class AccessKeyService { private static final int ACCESSKEY_COUNT_LIMIT = 5; private final AccessKeyRepository accessKeyRepository; private final AuditService auditService; public AccessKeyService(AccessKeyRepository accessKeyRepository, AuditService auditService) { this.accessKeyRepository = accessKeyRepository; this.auditService = auditService; } public List findByAppId(String appId) { return accessKeyRepository.findByAppId(appId); } @Transactional public AccessKey create(String appId, AccessKey entity) { long count = accessKeyRepository.countByAppId(appId); if (count >= ACCESSKEY_COUNT_LIMIT) { throw new BadRequestException("AccessKeys count limit exceeded"); } entity.setId(0L); entity.setAppId(appId); entity.setDataChangeLastModifiedBy(entity.getDataChangeCreatedBy()); AccessKey accessKey = accessKeyRepository.save(entity); auditService.audit(AccessKey.class.getSimpleName(), accessKey.getId(), Audit.OP.INSERT, accessKey.getDataChangeCreatedBy()); return accessKey; } @Transactional public AccessKey update(String appId, AccessKey entity) { long id = entity.getId(); String operator = entity.getDataChangeLastModifiedBy(); AccessKey accessKey = accessKeyRepository.findOneByAppIdAndId(appId, id); if (accessKey == null) { throw BadRequestException.accessKeyNotExists(); } accessKey.setMode(entity.getMode()); accessKey.setEnabled(entity.isEnabled()); accessKey.setDataChangeLastModifiedBy(operator); accessKeyRepository.save(accessKey); auditService.audit(AccessKey.class.getSimpleName(), id, Audit.OP.UPDATE, operator); return accessKey; } @Transactional public void delete(String appId, long id, String operator) { AccessKey accessKey = accessKeyRepository.findOneByAppIdAndId(appId, id); if (accessKey == null) { throw BadRequestException.accessKeyNotExists(); } if (accessKey.isEnabled()) { throw new BadRequestException("AccessKey should disable first"); } accessKey.setDeleted(Boolean.TRUE); accessKey.setDataChangeLastModifiedBy(operator); accessKeyRepository.save(accessKey); auditService.audit(AccessKey.class.getSimpleName(), id, Audit.OP.DELETE, operator); } } ================================================ FILE: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/service/AdminService.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.service; import com.ctrip.framework.apollo.biz.entity.Cluster; import com.ctrip.framework.apollo.common.entity.App; import com.ctrip.framework.apollo.core.ConfigConsts; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; import java.util.Objects; @Service public class AdminService { private final static Logger logger = LoggerFactory.getLogger(AdminService.class); private final AppService appService; private final AppNamespaceService appNamespaceService; private final ClusterService clusterService; private final NamespaceService namespaceService; public AdminService(final AppService appService, final @Lazy AppNamespaceService appNamespaceService, final @Lazy ClusterService clusterService, final @Lazy NamespaceService namespaceService) { this.appService = appService; this.appNamespaceService = appNamespaceService; this.clusterService = clusterService; this.namespaceService = namespaceService; } @Transactional public App createNewApp(App app) { String createBy = app.getDataChangeCreatedBy(); App createdApp = appService.save(app); String appId = createdApp.getAppId(); appNamespaceService.createDefaultAppNamespace(appId, createBy); clusterService.createDefaultCluster(appId, createBy); namespaceService.instanceOfAppNamespaces(appId, ConfigConsts.CLUSTER_NAME_DEFAULT, createBy); return app; } @Transactional public void deleteApp(App app, String operator) { String appId = app.getAppId(); logger.info("{} is deleting App:{}", operator, appId); List managedClusters = clusterService.findParentClusters(appId); // 1. delete clusters if (Objects.nonNull(managedClusters)) { for (Cluster cluster : managedClusters) { clusterService.delete(cluster.getId(), operator); } } // 2. delete appNamespace appNamespaceService.batchDelete(appId, operator); // 3. delete app appService.delete(app.getId(), operator); } } ================================================ FILE: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/service/AppNamespaceService.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.service; import com.ctrip.framework.apollo.audit.annotation.ApolloAuditLog; import com.ctrip.framework.apollo.audit.annotation.OpType; import com.ctrip.framework.apollo.biz.entity.Audit; import com.ctrip.framework.apollo.biz.entity.Cluster; import com.ctrip.framework.apollo.biz.entity.Namespace; import com.ctrip.framework.apollo.biz.repository.AppNamespaceRepository; import com.ctrip.framework.apollo.common.entity.AppNamespace; import com.ctrip.framework.apollo.common.exception.ServiceException; import com.ctrip.framework.apollo.common.utils.BeanUtils; import com.ctrip.framework.apollo.core.ConfigConsts; import com.ctrip.framework.apollo.core.enums.ConfigFileFormat; import com.ctrip.framework.apollo.core.utils.StringUtils; import com.google.common.base.Preconditions; import com.google.common.base.Strings; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.Set; @Service public class AppNamespaceService { private static final Logger logger = LoggerFactory.getLogger(AppNamespaceService.class); private final AppNamespaceRepository appNamespaceRepository; private final NamespaceService namespaceService; private final ClusterService clusterService; private final AuditService auditService; public AppNamespaceService(final AppNamespaceRepository appNamespaceRepository, final @Lazy NamespaceService namespaceService, final @Lazy ClusterService clusterService, final AuditService auditService) { this.appNamespaceRepository = appNamespaceRepository; this.namespaceService = namespaceService; this.clusterService = clusterService; this.auditService = auditService; } public boolean isAppNamespaceNameUnique(String appId, String namespaceName) { Objects.requireNonNull(appId, "AppId must not be null"); Objects.requireNonNull(namespaceName, "Namespace must not be null"); return Objects.isNull(appNamespaceRepository.findByAppIdAndName(appId, namespaceName)); } public AppNamespace findPublicNamespaceByName(String namespaceName) { Preconditions.checkArgument(namespaceName != null, "Namespace must not be null"); return appNamespaceRepository.findByNameAndIsPublicTrue(namespaceName); } public List findByAppId(String appId) { return appNamespaceRepository.findByAppIdOrderByIdAsc(appId); } public List findPublicNamespacesByNames(Set namespaceNames) { if (namespaceNames == null || namespaceNames.isEmpty()) { return Collections.emptyList(); } return appNamespaceRepository.findByNameInAndIsPublicTrue(namespaceNames); } public List findPrivateAppNamespace(String appId) { return appNamespaceRepository.findByAppIdAndIsPublic(appId, false); } public AppNamespace findOne(String appId, String namespaceName) { Preconditions.checkArgument(!StringUtils.isContainEmpty(appId, namespaceName), "appId or Namespace must not be null"); return appNamespaceRepository.findByAppIdAndName(appId, namespaceName); } public List findByAppIdAndNamespaces(String appId, Set namespaceNames) { Preconditions.checkArgument(!Strings.isNullOrEmpty(appId), "appId must not be null"); if (namespaceNames == null || namespaceNames.isEmpty()) { return Collections.emptyList(); } return appNamespaceRepository.findByAppIdAndNameIn(appId, namespaceNames); } @Transactional @ApolloAuditLog(type = OpType.CREATE, name = "AppNamespace.createDefault") public void createDefaultAppNamespace(String appId, String createBy) { if (!isAppNamespaceNameUnique(appId, ConfigConsts.NAMESPACE_APPLICATION)) { throw new ServiceException("appnamespace not unique"); } AppNamespace appNs = new AppNamespace(); appNs.setAppId(appId); appNs.setName(ConfigConsts.NAMESPACE_APPLICATION); appNs.setComment("default app namespace"); appNs.setFormat(ConfigFileFormat.Properties.getValue()); appNs.setDataChangeCreatedBy(createBy); appNs.setDataChangeLastModifiedBy(createBy); appNamespaceRepository.save(appNs); auditService.audit(AppNamespace.class.getSimpleName(), appNs.getId(), Audit.OP.INSERT, createBy); } @Transactional public AppNamespace createAppNamespace(AppNamespace appNamespace) { String createBy = appNamespace.getDataChangeCreatedBy(); if (!isAppNamespaceNameUnique(appNamespace.getAppId(), appNamespace.getName())) { throw new ServiceException("appnamespace not unique"); } appNamespace.setId(0);// protection appNamespace.setDataChangeCreatedBy(createBy); appNamespace.setDataChangeLastModifiedBy(createBy); appNamespace = appNamespaceRepository.save(appNamespace); createNamespaceForAppNamespaceInAllCluster(appNamespace.getAppId(), appNamespace.getName(), createBy); auditService.audit(AppNamespace.class.getSimpleName(), appNamespace.getId(), Audit.OP.INSERT, createBy); return appNamespace; } public AppNamespace update(AppNamespace appNamespace) { AppNamespace managedNs = appNamespaceRepository.findByAppIdAndName(appNamespace.getAppId(), appNamespace.getName()); BeanUtils.copyEntityProperties(appNamespace, managedNs); managedNs = appNamespaceRepository.save(managedNs); auditService.audit(AppNamespace.class.getSimpleName(), managedNs.getId(), Audit.OP.UPDATE, managedNs.getDataChangeLastModifiedBy()); return managedNs; } public void createNamespaceForAppNamespaceInAllCluster(String appId, String namespaceName, String createBy) { List clusters = clusterService.findParentClusters(appId); for (Cluster cluster : clusters) { // in case there is some dirty data, e.g. public namespace deleted in other app and now // created in this app if (!namespaceService.isNamespaceUnique(appId, cluster.getName(), namespaceName)) { continue; } Namespace namespace = new Namespace(); namespace.setClusterName(cluster.getName()); namespace.setAppId(appId); namespace.setNamespaceName(namespaceName); namespace.setDataChangeCreatedBy(createBy); namespace.setDataChangeLastModifiedBy(createBy); namespaceService.save(namespace); } } @Transactional public void batchDelete(String appId, String operator) { appNamespaceRepository.batchDeleteByAppId(appId, operator); } @Transactional public void deleteAppNamespace(AppNamespace appNamespace, String operator) { String appId = appNamespace.getAppId(); String namespaceName = appNamespace.getName(); logger.info("{} is deleting AppNamespace, appId: {}, namespace: {}", operator, appId, namespaceName); // 1. delete namespaces List namespaces = namespaceService.findByAppIdAndNamespaceName(appId, namespaceName); if (namespaces != null) { for (Namespace namespace : namespaces) { namespaceService.deleteNamespace(namespace, operator); } } // 2. delete app namespace appNamespaceRepository.delete(appId, namespaceName, operator); } } ================================================ FILE: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/service/AppService.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.service; import com.ctrip.framework.apollo.audit.annotation.ApolloAuditLog; import com.ctrip.framework.apollo.audit.annotation.OpType; import com.ctrip.framework.apollo.biz.entity.Audit; import com.ctrip.framework.apollo.biz.repository.AppRepository; import com.ctrip.framework.apollo.common.entity.App; import com.ctrip.framework.apollo.common.exception.BadRequestException; import com.ctrip.framework.apollo.common.exception.ServiceException; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; import java.util.Objects; @Service public class AppService { private final AppRepository appRepository; private final AuditService auditService; public AppService(final AppRepository appRepository, final AuditService auditService) { this.appRepository = appRepository; this.auditService = auditService; } public boolean isAppIdUnique(String appId) { Objects.requireNonNull(appId, "AppId must not be null"); return Objects.isNull(appRepository.findByAppId(appId)); } @Transactional @ApolloAuditLog(type = OpType.DELETE, name = "App.delete") public void delete(long id, String operator) { App app = appRepository.findById(id).orElse(null); if (app == null) { return; } app.setDeleted(true); app.setDataChangeLastModifiedBy(operator); appRepository.save(app); auditService.audit(App.class.getSimpleName(), id, Audit.OP.DELETE, operator); } public List findAll(Pageable pageable) { Page page = appRepository.findAll(pageable); return page.getContent(); } public List findByName(String name) { return appRepository.findByName(name); } public App findOne(String appId) { return appRepository.findByAppId(appId); } @Transactional @ApolloAuditLog(type = OpType.CREATE, name = "App.create") public App save(App entity) { if (!isAppIdUnique(entity.getAppId())) { throw new ServiceException("appId not unique"); } entity.setId(0);// protection App app = appRepository.save(entity); auditService.audit(App.class.getSimpleName(), app.getId(), Audit.OP.INSERT, app.getDataChangeCreatedBy()); return app; } @Transactional @ApolloAuditLog(type = OpType.UPDATE, name = "App.update") public void update(App app) { String appId = app.getAppId(); App managedApp = appRepository.findByAppId(appId); if (managedApp == null) { throw BadRequestException.appNotExists(appId); } managedApp.setName(app.getName()); managedApp.setOrgId(app.getOrgId()); managedApp.setOrgName(app.getOrgName()); managedApp.setOwnerName(app.getOwnerName()); managedApp.setOwnerEmail(app.getOwnerEmail()); managedApp.setDataChangeLastModifiedBy(app.getDataChangeLastModifiedBy()); managedApp = appRepository.save(managedApp); auditService.audit(App.class.getSimpleName(), managedApp.getId(), Audit.OP.UPDATE, managedApp.getDataChangeLastModifiedBy()); } } ================================================ FILE: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/service/AuditService.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.service; import com.ctrip.framework.apollo.biz.entity.Audit; import com.ctrip.framework.apollo.biz.repository.AuditRepository; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; @Service public class AuditService { private final AuditRepository auditRepository; public AuditService(final AuditRepository auditRepository) { this.auditRepository = auditRepository; } List findByOwner(String owner) { return auditRepository.findByOwner(owner); } List find(String owner, String entity, String op) { return auditRepository.findAudits(owner, entity, op); } @Transactional public void audit(String entityName, Long entityId, Audit.OP op, String owner) { Audit audit = new Audit(); audit.setEntityName(entityName); audit.setEntityId(entityId); audit.setOpName(op.name()); audit.setDataChangeCreatedBy(owner); auditRepository.save(audit); } @Transactional public void audit(Audit audit) { auditRepository.save(audit); } } ================================================ FILE: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/service/BizDBPropertySource.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.service; import com.google.common.base.Strings; import com.google.common.collect.Maps; import com.ctrip.framework.apollo.biz.entity.ServerConfig; import com.ctrip.framework.apollo.biz.repository.ServerConfigRepository; import com.ctrip.framework.apollo.common.config.RefreshablePropertySource; import com.ctrip.framework.apollo.core.ConfigConsts; import com.ctrip.framework.foundation.Foundation; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.env.Environment; import org.springframework.core.env.Profiles; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import org.springframework.jdbc.datasource.init.DatabasePopulatorUtils; import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator; import org.springframework.stereotype.Component; import jakarta.annotation.PostConstruct; import javax.sql.DataSource; import java.util.Map; import java.util.Objects; /** * @author Jason Song(song_s@ctrip.com) */ @Component public class BizDBPropertySource extends RefreshablePropertySource { private static final Logger logger = LoggerFactory.getLogger(BizDBPropertySource.class); private final ServerConfigRepository serverConfigRepository; private final DataSource dataSource; private final Environment env; @Autowired public BizDBPropertySource(final ServerConfigRepository serverConfigRepository, DataSource dataSource, final Environment env) { super("DBConfig", Maps.newConcurrentMap()); this.serverConfigRepository = serverConfigRepository; this.dataSource = dataSource; this.env = env; } @PostConstruct public void runSqlScript() throws Exception { if (env.acceptsProfiles(Profiles.of("h2")) && !env.acceptsProfiles(Profiles.of("assembly"))) { Resource resource = new ClassPathResource("jpa/configdb.init.h2.sql"); if (resource.exists()) { DatabasePopulatorUtils.execute(new ResourceDatabasePopulator(resource), dataSource); } } } String getCurrentDataCenter() { return Foundation.server().getDataCenter(); } @Override protected void refresh() { Iterable dbConfigs = serverConfigRepository.findAll(); Map newConfigs = Maps.newHashMap(); // default cluster's configs for (ServerConfig config : dbConfigs) { if (Objects.equals(ConfigConsts.CLUSTER_NAME_DEFAULT, config.getCluster())) { newConfigs.put(config.getKey(), config.getValue()); } } // data center's configs String dataCenter = getCurrentDataCenter(); for (ServerConfig config : dbConfigs) { if (Objects.equals(dataCenter, config.getCluster())) { newConfigs.put(config.getKey(), config.getValue()); } } // cluster's config if (!Strings.isNullOrEmpty(System.getProperty(ConfigConsts.APOLLO_CLUSTER_KEY))) { String cluster = System.getProperty(ConfigConsts.APOLLO_CLUSTER_KEY); for (ServerConfig config : dbConfigs) { if (Objects.equals(cluster, config.getCluster())) { newConfigs.put(config.getKey(), config.getValue()); } } } // put to environment for (Map.Entry config : newConfigs.entrySet()) { String key = config.getKey(); Object value = config.getValue(); if (this.source.get(key) == null) { logger.info("Load config from DB : {} = {}", key, value); } else if (!Objects.equals(this.source.get(key), value)) { logger.info("Load config from DB : {} = {}. Old value = {}", key, value, this.source.get(key)); } this.source.put(key, value); } } } ================================================ FILE: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/service/ClusterService.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.service; import com.ctrip.framework.apollo.biz.entity.Audit; import com.ctrip.framework.apollo.biz.entity.Cluster; import com.ctrip.framework.apollo.biz.repository.ClusterRepository; import com.ctrip.framework.apollo.common.exception.BadRequestException; import com.ctrip.framework.apollo.common.exception.ServiceException; import com.ctrip.framework.apollo.common.utils.BeanUtils; import com.ctrip.framework.apollo.core.ConfigConsts; import com.google.common.base.Strings; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.Collections; import java.util.List; import java.util.Objects; @Service public class ClusterService { private final ClusterRepository clusterRepository; private final AuditService auditService; private final NamespaceService namespaceService; public ClusterService(final ClusterRepository clusterRepository, final AuditService auditService, final @Lazy NamespaceService namespaceService) { this.clusterRepository = clusterRepository; this.auditService = auditService; this.namespaceService = namespaceService; } public boolean isClusterNameUnique(String appId, String clusterName) { Objects.requireNonNull(appId, "AppId must not be null"); Objects.requireNonNull(clusterName, "ClusterName must not be null"); return Objects.isNull(clusterRepository.findByAppIdAndName(appId, clusterName)); } public Cluster findOne(String appId, String name) { return clusterRepository.findByAppIdAndName(appId, name); } public Cluster findOne(long clusterId) { return clusterRepository.findById(clusterId).orElse(null); } public List findParentClusters(String appId) { if (Strings.isNullOrEmpty(appId)) { return Collections.emptyList(); } List clusters = clusterRepository.findByAppIdAndParentClusterId(appId, 0L); if (clusters == null) { return Collections.emptyList(); } Collections.sort(clusters); return clusters; } @Transactional public Cluster saveWithInstanceOfAppNamespaces(Cluster entity) { Cluster savedCluster = saveWithoutInstanceOfAppNamespaces(entity); namespaceService.instanceOfAppNamespaces(savedCluster.getAppId(), savedCluster.getName(), savedCluster.getDataChangeCreatedBy()); return savedCluster; } @Transactional public Cluster saveWithoutInstanceOfAppNamespaces(Cluster entity) { if (!isClusterNameUnique(entity.getAppId(), entity.getName())) { throw new BadRequestException("cluster not unique"); } entity.setId(0);// protection Cluster cluster = clusterRepository.save(entity); auditService.audit(Cluster.class.getSimpleName(), cluster.getId(), Audit.OP.INSERT, cluster.getDataChangeCreatedBy()); return cluster; } @Transactional public void delete(long id, String operator) { Cluster cluster = clusterRepository.findById(id).orElse(null); if (cluster == null) { throw BadRequestException.clusterNotExists(""); } // delete linked namespaces namespaceService.deleteByAppIdAndClusterName(cluster.getAppId(), cluster.getName(), operator); cluster.setDeleted(true); cluster.setDataChangeLastModifiedBy(operator); clusterRepository.save(cluster); auditService.audit(Cluster.class.getSimpleName(), id, Audit.OP.DELETE, operator); } @Transactional public Cluster update(Cluster cluster) { Cluster managedCluster = clusterRepository.findByAppIdAndName(cluster.getAppId(), cluster.getName()); BeanUtils.copyEntityProperties(cluster, managedCluster); managedCluster = clusterRepository.save(managedCluster); auditService.audit(Cluster.class.getSimpleName(), managedCluster.getId(), Audit.OP.UPDATE, managedCluster.getDataChangeLastModifiedBy()); return managedCluster; } @Transactional public void createDefaultCluster(String appId, String createBy) { if (!isClusterNameUnique(appId, ConfigConsts.CLUSTER_NAME_DEFAULT)) { throw new ServiceException("cluster not unique"); } Cluster cluster = new Cluster(); cluster.setName(ConfigConsts.CLUSTER_NAME_DEFAULT); cluster.setAppId(appId); cluster.setDataChangeCreatedBy(createBy); cluster.setDataChangeLastModifiedBy(createBy); clusterRepository.save(cluster); auditService.audit(Cluster.class.getSimpleName(), cluster.getId(), Audit.OP.INSERT, createBy); } public List findChildClusters(String appId, String parentClusterName) { Cluster parentCluster = findOne(appId, parentClusterName); if (parentCluster == null) { throw BadRequestException.clusterNotExists(parentClusterName); } return clusterRepository.findByParentClusterId(parentCluster.getId()); } public List findClusters(String appId) { List clusters = clusterRepository.findByAppId(appId); if (clusters == null) { return Collections.emptyList(); } // to make sure parent cluster is ahead of branch cluster Collections.sort(clusters); return clusters; } } ================================================ FILE: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/service/CommitService.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.service; import com.ctrip.framework.apollo.biz.entity.Commit; import com.ctrip.framework.apollo.biz.repository.CommitRepository; import java.util.Date; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; @Service public class CommitService { private final CommitRepository commitRepository; public CommitService(final CommitRepository commitRepository) { this.commitRepository = commitRepository; } public void createCommit(String appId, String clusterName, String namespaceName, String configChangeContent, String operator) { Commit commit = new Commit(); commit.setId(0);// protection commit.setAppId(appId); commit.setClusterName(clusterName); commit.setNamespaceName(namespaceName); commit.setChangeSets(configChangeContent); commit.setDataChangeCreatedBy(operator); commit.setDataChangeLastModifiedBy(operator); commitRepository.save(commit); } public List find(String appId, String clusterName, String namespaceName, Pageable page) { return commitRepository.findByAppIdAndClusterNameAndNamespaceNameOrderByIdDesc(appId, clusterName, namespaceName, page); } public List find(String appId, String clusterName, String namespaceName, Date lastModifiedTime, Pageable page) { return commitRepository .findByAppIdAndClusterNameAndNamespaceNameAndDataChangeLastModifiedTimeGreaterThanEqualOrderByIdDesc( appId, clusterName, namespaceName, lastModifiedTime, page); } public List findByKey(String appId, String clusterName, String namespaceName, String key, Pageable page) { String queryKey = "\"key\":\"" + key + "\""; return commitRepository.findByAppIdAndClusterNameAndNamespaceNameAndChangeSetsLikeOrderByIdDesc( appId, clusterName, namespaceName, "%" + queryKey + "%", page); } @Transactional public int batchDelete(String appId, String clusterName, String namespaceName, String operator) { return commitRepository.batchDelete(appId, clusterName, namespaceName, operator); } } ================================================ FILE: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/service/InstanceService.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.service; import com.ctrip.framework.apollo.biz.entity.Instance; import com.ctrip.framework.apollo.biz.entity.InstanceConfig; import com.ctrip.framework.apollo.biz.repository.InstanceConfigRepository; import com.ctrip.framework.apollo.biz.repository.InstanceRepository; import com.google.common.base.Preconditions; import com.google.common.collect.Lists; import java.util.Objects; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.CollectionUtils; import java.util.Calendar; import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Set; import java.util.stream.Collectors; /** * @author Jason Song(song_s@ctrip.com) */ @Service public class InstanceService { private final InstanceRepository instanceRepository; private final InstanceConfigRepository instanceConfigRepository; public InstanceService(final InstanceRepository instanceRepository, final InstanceConfigRepository instanceConfigRepository) { this.instanceRepository = instanceRepository; this.instanceConfigRepository = instanceConfigRepository; } public Instance findInstance(String appId, String clusterName, String dataCenter, String ip) { return instanceRepository.findByAppIdAndClusterNameAndDataCenterAndIp(appId, clusterName, dataCenter, ip); } public List findInstancesByIds(Set instanceIds) { Iterable instances = instanceRepository.findAllById(instanceIds); return Lists.newArrayList(instances); } @Transactional public Instance createInstance(Instance instance) { instance.setId(0); // protection return instanceRepository.save(instance); } public InstanceConfig findInstanceConfig(long instanceId, String configAppId, String configNamespaceName) { return instanceConfigRepository.findByInstanceIdAndConfigAppIdAndConfigNamespaceName(instanceId, configAppId, configNamespaceName); } public Page findActiveInstanceConfigsByReleaseKey(String releaseKey, Pageable pageable) { return instanceConfigRepository.findByReleaseKeyAndDataChangeLastModifiedTimeAfter(releaseKey, getValidInstanceConfigDate(), pageable); } public Page findInstancesByNamespace(String appId, String clusterName, String namespaceName, Pageable pageable) { Page instanceConfigs = instanceConfigRepository .findByConfigAppIdAndConfigClusterNameAndConfigNamespaceNameAndDataChangeLastModifiedTimeAfter( appId, clusterName, namespaceName, getValidInstanceConfigDate(), pageable); List instances = Collections.emptyList(); if (instanceConfigs.hasContent()) { Set instanceIds = instanceConfigs.getContent().stream() .map(InstanceConfig::getInstanceId).collect(Collectors.toSet()); instances = findInstancesByIds(instanceIds); } return new PageImpl<>(instances, pageable, instanceConfigs.getTotalElements()); } public Page findInstancesByNamespaceAndInstanceAppId(String instanceAppId, String appId, String clusterName, String namespaceName, Pageable pageable) { Page instanceIdResult = instanceConfigRepository.findInstanceIdsByNamespaceAndInstanceAppId(instanceAppId, appId, clusterName, namespaceName, getValidInstanceConfigDate(), pageable); List instances = Collections.emptyList(); if (instanceIdResult.hasContent()) { Set instanceIds = instanceIdResult.getContent().stream().filter(Objects::nonNull) .collect(Collectors.toSet()); instances = findInstancesByIds(instanceIds); } return new PageImpl<>(instances, pageable, instanceIdResult.getTotalElements()); } public List findInstanceConfigsByNamespaceWithReleaseKeysNotIn(String appId, String clusterName, String namespaceName, Set releaseKeysNotIn) { List instanceConfigs = instanceConfigRepository .findByConfigAppIdAndConfigClusterNameAndConfigNamespaceNameAndDataChangeLastModifiedTimeAfterAndReleaseKeyNotIn( appId, clusterName, namespaceName, getValidInstanceConfigDate(), releaseKeysNotIn); if (CollectionUtils.isEmpty(instanceConfigs)) { return Collections.emptyList(); } return instanceConfigs; } /** * Currently the instance config is expired by 1 day, add one more hour to avoid possible time * difference */ private Date getValidInstanceConfigDate() { Calendar cal = Calendar.getInstance(); cal.add(Calendar.DATE, -1); cal.add(Calendar.HOUR, -1); return cal.getTime(); } @Transactional public InstanceConfig createInstanceConfig(InstanceConfig instanceConfig) { instanceConfig.setId(0); // protection return instanceConfigRepository.save(instanceConfig); } @Transactional public InstanceConfig updateInstanceConfig(InstanceConfig instanceConfig) { InstanceConfig existedInstanceConfig = instanceConfigRepository.findById(instanceConfig.getId()).orElse(null); Preconditions.checkArgument(existedInstanceConfig != null, String.format("Instance config %d doesn't exist", instanceConfig.getId())); existedInstanceConfig.setConfigClusterName(instanceConfig.getConfigClusterName()); existedInstanceConfig.setReleaseKey(instanceConfig.getReleaseKey()); existedInstanceConfig.setReleaseDeliveryTime(instanceConfig.getReleaseDeliveryTime()); existedInstanceConfig .setDataChangeLastModifiedTime(instanceConfig.getDataChangeLastModifiedTime()); return instanceConfigRepository.save(existedInstanceConfig); } @Transactional public int batchDeleteInstanceConfig(String configAppId, String configClusterName, String configNamespaceName) { return instanceConfigRepository.batchDelete(configAppId, configClusterName, configNamespaceName); } } ================================================ FILE: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/service/ItemService.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.service; import com.ctrip.framework.apollo.biz.config.BizConfig; import com.ctrip.framework.apollo.biz.entity.Audit; import com.ctrip.framework.apollo.biz.entity.Item; import com.ctrip.framework.apollo.biz.entity.Namespace; import com.ctrip.framework.apollo.biz.repository.ItemRepository; import com.ctrip.framework.apollo.common.dto.ItemInfoDTO; import com.ctrip.framework.apollo.common.exception.BadRequestException; import com.ctrip.framework.apollo.common.exception.NotFoundException; import com.ctrip.framework.apollo.common.utils.BeanUtils; import com.ctrip.framework.apollo.core.utils.StringUtils; import org.springframework.context.annotation.Lazy; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; @Service public class ItemService { private static Pattern clusterPattern = Pattern.compile("[0-9]{14}-[a-zA-Z0-9]{16}"); private final ItemRepository itemRepository; private final NamespaceService namespaceService; private final AuditService auditService; private final BizConfig bizConfig; public ItemService(final ItemRepository itemRepository, final @Lazy NamespaceService namespaceService, final AuditService auditService, final BizConfig bizConfig) { this.itemRepository = itemRepository; this.namespaceService = namespaceService; this.auditService = auditService; this.bizConfig = bizConfig; } @Transactional public Item delete(long id, String operator) { Item item = itemRepository.findById(id).orElse(null); if (item == null) { throw new IllegalArgumentException("item not exist. ID:" + id); } item.setDeleted(true); item.setDataChangeLastModifiedBy(operator); Item deletedItem = itemRepository.save(item); auditService.audit(Item.class.getSimpleName(), id, Audit.OP.DELETE, operator); return deletedItem; } @Transactional public int batchDelete(long namespaceId, String operator) { return itemRepository.deleteByNamespaceId(namespaceId, operator); } public Item findOne(String appId, String clusterName, String namespaceName, String key) { Namespace namespace = findNamespaceByAppIdAndClusterNameAndNamespaceName(appId, clusterName, namespaceName); return itemRepository.findByNamespaceIdAndKey(namespace.getId(), key); } public Item findLastOne(String appId, String clusterName, String namespaceName) { Namespace namespace = findNamespaceByAppIdAndClusterNameAndNamespaceName(appId, clusterName, namespaceName); return findLastOne(namespace.getId()); } public Item findLastOne(long namespaceId) { return itemRepository.findFirst1ByNamespaceIdOrderByLineNumDesc(namespaceId); } public Item findOne(long itemId) { return itemRepository.findById(itemId).orElse(null); } public List findItemsWithoutOrdered(Long namespaceId) { List items = itemRepository.findByNamespaceId(namespaceId); if (items == null) { return Collections.emptyList(); } return items; } public List findItemsWithoutOrdered(String appId, String clusterName, String namespaceName) { Namespace namespace = namespaceService.findOne(appId, clusterName, namespaceName); if (namespace != null) { return findItemsWithoutOrdered(namespace.getId()); } return Collections.emptyList(); } public List findItemsWithOrdered(Long namespaceId) { List items = itemRepository.findByNamespaceIdOrderByLineNumAsc(namespaceId); if (items == null) { return Collections.emptyList(); } return items; } public List findItemsWithOrdered(String appId, String clusterName, String namespaceName) { Namespace namespace = namespaceService.findOne(appId, clusterName, namespaceName); if (namespace != null) { return findItemsWithOrdered(namespace.getId()); } return Collections.emptyList(); } public List findItemsModifiedAfterDate(long namespaceId, Date date) { return itemRepository.findByNamespaceIdAndDataChangeLastModifiedTimeGreaterThan(namespaceId, date); } public int findNonEmptyItemCount(long namespaceId) { return itemRepository.countByNamespaceIdAndFilterKeyEmpty(namespaceId); } public Page findItemsByKey(String key, Pageable pageable) { return itemRepository.findByKey(key, pageable); } public Page findItemsByNamespace(String appId, String clusterName, String namespaceName, Pageable pageable) { Namespace namespace = findNamespaceByAppIdAndClusterNameAndNamespaceName(appId, clusterName, namespaceName); return itemRepository.findByNamespaceId(namespace.getId(), pageable); } public Page getItemInfoBySearch(String key, String value, Pageable limit) { Page itemInfoDTOs; if (key.isEmpty() && !value.isEmpty()) { itemInfoDTOs = itemRepository.findItemsByValueLike(value, limit); } else if (value.isEmpty() && !key.isEmpty()) { itemInfoDTOs = itemRepository.findItemsByKeyLike(key, limit); } else { itemInfoDTOs = itemRepository.findItemsByKeyAndValueLike(key, value, limit); } return itemInfoDTOs; } @Transactional public Item save(Item entity) { checkItemKeyLength(entity.getKey()); checkItemType(entity.getType()); checkItemValueLength(entity.getNamespaceId(), entity.getValue()); entity.setId(0);// protection if (entity.getLineNum() == 0) { Item lastItem = findLastOne(entity.getNamespaceId()); int lineNum = lastItem == null ? 1 : lastItem.getLineNum() + 1; entity.setLineNum(lineNum); } Item item = itemRepository.save(entity); auditService.audit(Item.class.getSimpleName(), item.getId(), Audit.OP.INSERT, item.getDataChangeCreatedBy()); return item; } @Transactional public Item saveComment(Item entity) { entity.setKey(""); entity.setValue(""); entity.setId(0);// protection if (entity.getLineNum() == 0) { Item lastItem = findLastOne(entity.getNamespaceId()); int lineNum = lastItem == null ? 1 : lastItem.getLineNum() + 1; entity.setLineNum(lineNum); } Item item = itemRepository.save(entity); auditService.audit(Item.class.getSimpleName(), item.getId(), Audit.OP.INSERT, item.getDataChangeCreatedBy()); return item; } @Transactional public Item update(Item item) { checkItemType(item.getType()); checkItemValueLength(item.getNamespaceId(), item.getValue()); Item managedItem = itemRepository.findById(item.getId()).orElse(null); BeanUtils.copyEntityProperties(item, managedItem); managedItem = itemRepository.save(managedItem); auditService.audit(Item.class.getSimpleName(), managedItem.getId(), Audit.OP.UPDATE, managedItem.getDataChangeLastModifiedBy()); return managedItem; } private boolean checkItemValueLength(long namespaceId, String value) { Namespace currentNamespace = namespaceService.findOne(namespaceId); int limit = getItemValueLengthLimit(currentNamespace); if (currentNamespace != null) { Matcher m = clusterPattern.matcher(currentNamespace.getClusterName()); boolean isGray = m.matches(); if (isGray) { limit = getGrayNamespaceItemValueLengthLimit(currentNamespace, limit); } } if (!StringUtils.isEmpty(value) && value.length() > limit) { throw new BadRequestException("value too long. length limit:" + limit); } return true; } private int getGrayNamespaceItemValueLengthLimit(Namespace grayNamespace, int grayNamespaceLimit) { Namespace parentNamespace = namespaceService.findParentNamespace(grayNamespace); if (parentNamespace != null) { int parentLimit = getItemValueLengthLimit(grayNamespace); if (parentLimit > grayNamespaceLimit) { return parentLimit; } } return grayNamespaceLimit; } private boolean checkItemKeyLength(String key) { if (!StringUtils.isEmpty(key) && key.length() > bizConfig.itemKeyLengthLimit()) { throw new BadRequestException("key too long. length limit:" + bizConfig.itemKeyLengthLimit()); } return true; } private boolean checkItemType(int type) { if (type < 0 || type > 3) { throw new BadRequestException("type is invalid. type should be in [0, 3]. "); } return true; } private int getItemValueLengthLimit(Namespace namespace) { Map namespaceValueLengthOverride = bizConfig.namespaceValueLengthLimitOverride(); if (namespaceValueLengthOverride != null && namespaceValueLengthOverride.containsKey(namespace.getId())) { return namespaceValueLengthOverride.get(namespace.getId()); } Map appIdValueLengthOverride = bizConfig.appIdValueLengthLimitOverride(); if (appIdValueLengthOverride != null && appIdValueLengthOverride.containsKey(namespace.getAppId())) { return appIdValueLengthOverride.get(namespace.getAppId()); } return bizConfig.itemValueLengthLimit(); } private Namespace findNamespaceByAppIdAndClusterNameAndNamespaceName(String appId, String clusterName, String namespaceName) { Namespace namespace = namespaceService.findOne(appId, clusterName, namespaceName); if (namespace == null) { throw NotFoundException.namespaceNotFound(appId, clusterName, namespaceName); } return namespace; } } ================================================ FILE: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/service/ItemSetService.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.service; import com.ctrip.framework.apollo.biz.config.BizConfig; import com.ctrip.framework.apollo.biz.entity.Audit; import com.ctrip.framework.apollo.biz.entity.Item; import com.ctrip.framework.apollo.biz.entity.Namespace; import com.ctrip.framework.apollo.biz.utils.ConfigChangeContentBuilder; import com.ctrip.framework.apollo.common.dto.ItemChangeSets; import com.ctrip.framework.apollo.common.dto.ItemDTO; import com.ctrip.framework.apollo.common.exception.BadRequestException; import com.ctrip.framework.apollo.common.exception.NotFoundException; import com.ctrip.framework.apollo.common.utils.BeanUtils; import com.ctrip.framework.apollo.core.utils.StringUtils; import java.util.List; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.CollectionUtils; @Service public class ItemSetService { private final AuditService auditService; private final CommitService commitService; private final ItemService itemService; private final NamespaceService namespaceService; private final BizConfig bizConfig; public ItemSetService(final AuditService auditService, final CommitService commitService, final ItemService itemService, final NamespaceService namespaceService, final BizConfig bizConfig) { this.auditService = auditService; this.commitService = commitService; this.itemService = itemService; this.namespaceService = namespaceService; this.bizConfig = bizConfig; } @Transactional public ItemChangeSets updateSet(Namespace namespace, ItemChangeSets changeSets) { return updateSet(namespace.getAppId(), namespace.getClusterName(), namespace.getNamespaceName(), changeSets); } @Transactional public ItemChangeSets updateSet(String appId, String clusterName, String namespaceName, ItemChangeSets changeSet) { Namespace namespace = namespaceService.findOne(appId, clusterName, namespaceName); if (namespace == null) { throw NotFoundException.namespaceNotFound(appId, clusterName, namespaceName); } if (bizConfig.isItemNumLimitEnabled()) { int itemCount = itemService.findNonEmptyItemCount(namespace.getId()); int createItemCount = (int) changeSet.getCreateItems().stream() .filter(item -> !StringUtils.isEmpty(item.getKey())).count(); int deleteItemCount = (int) changeSet.getDeleteItems().stream() .filter(item -> !StringUtils.isEmpty(item.getKey())).count(); itemCount = itemCount + createItemCount - deleteItemCount; if (itemCount > bizConfig.itemNumLimit()) { throw new BadRequestException("The maximum number of items (" + bizConfig.itemNumLimit() + ") for this namespace has been reached. Current item count is " + itemCount + "."); } } String operator = changeSet.getDataChangeLastModifiedBy(); ConfigChangeContentBuilder configChangeContentBuilder = new ConfigChangeContentBuilder(); if (!CollectionUtils.isEmpty(changeSet.getCreateItems())) { this.doCreateItems(changeSet.getCreateItems(), namespace, operator, configChangeContentBuilder); auditService.audit("ItemSet", null, Audit.OP.INSERT, operator); } if (!CollectionUtils.isEmpty(changeSet.getUpdateItems())) { this.doUpdateItems(changeSet.getUpdateItems(), namespace, operator, configChangeContentBuilder); auditService.audit("ItemSet", null, Audit.OP.UPDATE, operator); } if (!CollectionUtils.isEmpty(changeSet.getDeleteItems())) { this.doDeleteItems(changeSet.getDeleteItems(), namespace, operator, configChangeContentBuilder); auditService.audit("ItemSet", null, Audit.OP.DELETE, operator); } if (configChangeContentBuilder.hasContent()) { commitService.createCommit(appId, clusterName, namespaceName, configChangeContentBuilder.build(), changeSet.getDataChangeLastModifiedBy()); } return changeSet; } private void doDeleteItems(List toDeleteItems, Namespace namespace, String operator, ConfigChangeContentBuilder configChangeContentBuilder) { for (ItemDTO item : toDeleteItems) { Item deletedItem = itemService.delete(item.getId(), operator); if (deletedItem.getNamespaceId() != namespace.getId()) { throw BadRequestException.namespaceNotMatch(); } configChangeContentBuilder.deleteItem(deletedItem); } } private void doUpdateItems(List toUpdateItems, Namespace namespace, String operator, ConfigChangeContentBuilder configChangeContentBuilder) { for (ItemDTO item : toUpdateItems) { Item entity = BeanUtils.transform(Item.class, item); Item managedItem = itemService.findOne(entity.getId()); if (managedItem == null) { throw NotFoundException.itemNotFound(entity.getKey()); } if (managedItem.getNamespaceId() != namespace.getId()) { throw BadRequestException.namespaceNotMatch(); } Item beforeUpdateItem = BeanUtils.transform(Item.class, managedItem); // protect. only value,type,comment,lastModifiedBy can be modified managedItem.setType(entity.getType()); managedItem.setValue(entity.getValue()); managedItem.setComment(entity.getComment()); managedItem.setLineNum(entity.getLineNum()); managedItem.setDataChangeLastModifiedBy(operator); Item updatedItem = itemService.update(managedItem); configChangeContentBuilder.updateItem(beforeUpdateItem, updatedItem); } } private void doCreateItems(List toCreateItems, Namespace namespace, String operator, ConfigChangeContentBuilder configChangeContentBuilder) { for (ItemDTO item : toCreateItems) { if (item.getNamespaceId() != namespace.getId()) { throw BadRequestException.namespaceNotMatch(); } Item entity = BeanUtils.transform(Item.class, item); entity.setDataChangeCreatedBy(operator); entity.setDataChangeLastModifiedBy(operator); Item createdItem = itemService.save(entity); configChangeContentBuilder.createItem(createdItem); } } } ================================================ FILE: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/service/NamespaceBranchService.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.service; import com.ctrip.framework.apollo.biz.entity.Audit; import com.ctrip.framework.apollo.biz.entity.Cluster; import com.ctrip.framework.apollo.biz.entity.GrayReleaseRule; import com.ctrip.framework.apollo.biz.entity.Namespace; import com.ctrip.framework.apollo.biz.entity.Release; import com.ctrip.framework.apollo.biz.repository.GrayReleaseRuleRepository; import com.ctrip.framework.apollo.common.constants.NamespaceBranchStatus; import com.ctrip.framework.apollo.common.constants.ReleaseOperation; import com.ctrip.framework.apollo.common.constants.ReleaseOperationContext; import com.ctrip.framework.apollo.common.exception.BadRequestException; import com.ctrip.framework.apollo.common.utils.GrayReleaseRuleItemTransformer; import com.ctrip.framework.apollo.common.utils.UniqueKeyGenerator; import com.google.common.collect.Maps; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.Map; @Service public class NamespaceBranchService { private final AuditService auditService; private final GrayReleaseRuleRepository grayReleaseRuleRepository; private final ClusterService clusterService; private final ReleaseService releaseService; private final NamespaceService namespaceService; private final ReleaseHistoryService releaseHistoryService; public NamespaceBranchService(final AuditService auditService, final GrayReleaseRuleRepository grayReleaseRuleRepository, final ClusterService clusterService, final @Lazy ReleaseService releaseService, final NamespaceService namespaceService, final ReleaseHistoryService releaseHistoryService) { this.auditService = auditService; this.grayReleaseRuleRepository = grayReleaseRuleRepository; this.clusterService = clusterService; this.releaseService = releaseService; this.namespaceService = namespaceService; this.releaseHistoryService = releaseHistoryService; } @Transactional public Namespace createBranch(String appId, String parentClusterName, String namespaceName, String operator) { Namespace childNamespace = findBranch(appId, parentClusterName, namespaceName); if (childNamespace != null) { throw BadRequestException.namespaceNotExists(appId, parentClusterName, namespaceName); } Cluster parentCluster = clusterService.findOne(appId, parentClusterName); if (parentCluster == null || parentCluster.getParentClusterId() != 0) { throw BadRequestException.clusterNotExists(parentClusterName); } // create child cluster Cluster childCluster = createChildCluster(appId, parentCluster, namespaceName, operator); Cluster createdChildCluster = clusterService.saveWithoutInstanceOfAppNamespaces(childCluster); // create child namespace childNamespace = createNamespaceBranch(appId, createdChildCluster.getName(), namespaceName, operator); return namespaceService.save(childNamespace); } public Namespace findBranch(String appId, String parentClusterName, String namespaceName) { return namespaceService.findChildNamespace(appId, parentClusterName, namespaceName); } public GrayReleaseRule findBranchGrayRules(String appId, String clusterName, String namespaceName, String branchName) { return grayReleaseRuleRepository .findTopByAppIdAndClusterNameAndNamespaceNameAndBranchNameOrderByIdDesc(appId, clusterName, namespaceName, branchName); } @Transactional public void updateBranchGrayRules(String appId, String clusterName, String namespaceName, String branchName, GrayReleaseRule newRules) { doUpdateBranchGrayRules(appId, clusterName, namespaceName, branchName, newRules, true, ReleaseOperation.APPLY_GRAY_RULES); } private void doUpdateBranchGrayRules(String appId, String clusterName, String namespaceName, String branchName, GrayReleaseRule newRules, boolean recordReleaseHistory, int releaseOperation) { GrayReleaseRule oldRules = grayReleaseRuleRepository .findTopByAppIdAndClusterNameAndNamespaceNameAndBranchNameOrderByIdDesc(appId, clusterName, namespaceName, branchName); Release latestBranchRelease = releaseService.findLatestActiveRelease(appId, branchName, namespaceName); long latestBranchReleaseId = latestBranchRelease != null ? latestBranchRelease.getId() : 0; newRules.setReleaseId(latestBranchReleaseId); grayReleaseRuleRepository.save(newRules); // delete old rules if (oldRules != null) { grayReleaseRuleRepository.delete(oldRules); } if (recordReleaseHistory) { Map releaseOperationContext = Maps.newHashMap(); releaseOperationContext.put(ReleaseOperationContext.RULES, GrayReleaseRuleItemTransformer.batchTransformFromJSON(newRules.getRules())); if (oldRules != null) { releaseOperationContext.put(ReleaseOperationContext.OLD_RULES, GrayReleaseRuleItemTransformer.batchTransformFromJSON(oldRules.getRules())); } releaseHistoryService.createReleaseHistory(appId, clusterName, namespaceName, branchName, latestBranchReleaseId, latestBranchReleaseId, releaseOperation, releaseOperationContext, newRules.getDataChangeLastModifiedBy()); } } @Transactional public GrayReleaseRule updateRulesReleaseId(String appId, String clusterName, String namespaceName, String branchName, long latestReleaseId, String operator) { GrayReleaseRule oldRules = grayReleaseRuleRepository .findTopByAppIdAndClusterNameAndNamespaceNameAndBranchNameOrderByIdDesc(appId, clusterName, namespaceName, branchName); if (oldRules == null) { return null; } GrayReleaseRule newRules = new GrayReleaseRule(); newRules.setBranchStatus(NamespaceBranchStatus.ACTIVE); newRules.setReleaseId(latestReleaseId); newRules.setRules(oldRules.getRules()); newRules.setAppId(oldRules.getAppId()); newRules.setClusterName(oldRules.getClusterName()); newRules.setNamespaceName(oldRules.getNamespaceName()); newRules.setBranchName(oldRules.getBranchName()); newRules.setDataChangeCreatedBy(operator); newRules.setDataChangeLastModifiedBy(operator); grayReleaseRuleRepository.save(newRules); grayReleaseRuleRepository.delete(oldRules); return newRules; } @Transactional public void deleteBranch(String appId, String clusterName, String namespaceName, String branchName, int branchStatus, String operator) { Cluster toDeleteCluster = clusterService.findOne(appId, branchName); if (toDeleteCluster == null) { return; } Release latestBranchRelease = releaseService.findLatestActiveRelease(appId, branchName, namespaceName); long latestBranchReleaseId = latestBranchRelease != null ? latestBranchRelease.getId() : 0; // update branch rules GrayReleaseRule deleteRule = new GrayReleaseRule(); deleteRule.setRules("[]"); deleteRule.setAppId(appId); deleteRule.setClusterName(clusterName); deleteRule.setNamespaceName(namespaceName); deleteRule.setBranchName(branchName); deleteRule.setBranchStatus(branchStatus); deleteRule.setDataChangeLastModifiedBy(operator); deleteRule.setDataChangeCreatedBy(operator); doUpdateBranchGrayRules(appId, clusterName, namespaceName, branchName, deleteRule, false, -1); // delete branch cluster clusterService.delete(toDeleteCluster.getId(), operator); int releaseOperation = branchStatus == NamespaceBranchStatus.MERGED ? ReleaseOperation.GRAY_RELEASE_DELETED_AFTER_MERGE : ReleaseOperation.ABANDON_GRAY_RELEASE; releaseHistoryService.createReleaseHistory(appId, clusterName, namespaceName, branchName, latestBranchReleaseId, latestBranchReleaseId, releaseOperation, null, operator); auditService.audit("Branch", toDeleteCluster.getId(), Audit.OP.DELETE, operator); } private Cluster createChildCluster(String appId, Cluster parentCluster, String namespaceName, String operator) { Cluster childCluster = new Cluster(); childCluster.setAppId(appId); childCluster.setParentClusterId(parentCluster.getId()); childCluster .setName(UniqueKeyGenerator.generate(appId, parentCluster.getName(), namespaceName)); childCluster.setDataChangeCreatedBy(operator); childCluster.setDataChangeLastModifiedBy(operator); return childCluster; } private Namespace createNamespaceBranch(String appId, String clusterName, String namespaceName, String operator) { Namespace childNamespace = new Namespace(); childNamespace.setAppId(appId); childNamespace.setClusterName(clusterName); childNamespace.setNamespaceName(namespaceName); childNamespace.setDataChangeLastModifiedBy(operator); childNamespace.setDataChangeCreatedBy(operator); return childNamespace; } } ================================================ FILE: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/service/NamespaceLockService.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.service; import com.ctrip.framework.apollo.biz.entity.NamespaceLock; import com.ctrip.framework.apollo.biz.repository.NamespaceLockRepository; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service public class NamespaceLockService { private final NamespaceLockRepository namespaceLockRepository; public NamespaceLockService(final NamespaceLockRepository namespaceLockRepository) { this.namespaceLockRepository = namespaceLockRepository; } public NamespaceLock findLock(Long namespaceId) { return namespaceLockRepository.findByNamespaceId(namespaceId); } @Transactional public NamespaceLock tryLock(NamespaceLock lock) { return namespaceLockRepository.save(lock); } @Transactional public void unlock(Long namespaceId) { namespaceLockRepository.deleteByNamespaceId(namespaceId); } } ================================================ FILE: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/service/NamespaceService.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.service; import com.ctrip.framework.apollo.biz.config.BizConfig; import com.ctrip.framework.apollo.biz.entity.Audit; import com.ctrip.framework.apollo.biz.entity.Cluster; import com.ctrip.framework.apollo.biz.entity.Item; import com.ctrip.framework.apollo.biz.entity.Namespace; import com.ctrip.framework.apollo.biz.entity.Release; import com.ctrip.framework.apollo.biz.message.MessageSender; import com.ctrip.framework.apollo.biz.message.Topics; import com.ctrip.framework.apollo.biz.repository.NamespaceRepository; import com.ctrip.framework.apollo.biz.utils.ReleaseMessageKeyGenerator; import com.ctrip.framework.apollo.common.constants.GsonType; import com.ctrip.framework.apollo.common.constants.NamespaceBranchStatus; import com.ctrip.framework.apollo.common.entity.AppNamespace; import com.ctrip.framework.apollo.common.exception.BadRequestException; import com.ctrip.framework.apollo.common.exception.ServiceException; import com.ctrip.framework.apollo.common.utils.BeanUtils; import com.ctrip.framework.apollo.core.ConfigConsts; import com.google.common.collect.Maps; import com.google.gson.Gson; import org.springframework.context.annotation.Lazy; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.CollectionUtils; import java.util.Collections; import java.util.Date; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; @Service public class NamespaceService { private static final Gson GSON = new Gson(); private final NamespaceRepository namespaceRepository; private final AuditService auditService; private final AppNamespaceService appNamespaceService; private final ItemService itemService; private final CommitService commitService; private final ReleaseService releaseService; private final ClusterService clusterService; private final NamespaceBranchService namespaceBranchService; private final ReleaseHistoryService releaseHistoryService; private final NamespaceLockService namespaceLockService; private final InstanceService instanceService; private final MessageSender messageSender; private final BizConfig bizConfig; public NamespaceService(final ReleaseHistoryService releaseHistoryService, final NamespaceRepository namespaceRepository, final AuditService auditService, final @Lazy AppNamespaceService appNamespaceService, final MessageSender messageSender, final @Lazy ItemService itemService, final CommitService commitService, final @Lazy ReleaseService releaseService, final @Lazy ClusterService clusterService, final @Lazy NamespaceBranchService namespaceBranchService, final NamespaceLockService namespaceLockService, final InstanceService instanceService, final BizConfig bizConfig) { this.releaseHistoryService = releaseHistoryService; this.namespaceRepository = namespaceRepository; this.auditService = auditService; this.appNamespaceService = appNamespaceService; this.messageSender = messageSender; this.itemService = itemService; this.commitService = commitService; this.releaseService = releaseService; this.clusterService = clusterService; this.namespaceBranchService = namespaceBranchService; this.namespaceLockService = namespaceLockService; this.instanceService = instanceService; this.bizConfig = bizConfig; } public Namespace findOne(Long namespaceId) { return namespaceRepository.findById(namespaceId).orElse(null); } public Namespace findOne(String appId, String clusterName, String namespaceName) { return namespaceRepository.findByAppIdAndClusterNameAndNamespaceName(appId, clusterName, namespaceName); } /** * the returned content's size is not fixed. so please carefully used. */ public Page findByItem(String itemKey, Pageable pageable) { Page items = itemService.findItemsByKey(itemKey, pageable); if (!items.hasContent()) { return Page.empty(); } Set namespaceIds = BeanUtils.toPropertySet("namespaceId", items.getContent()); return new PageImpl<>(namespaceRepository.findByIdIn(namespaceIds)); } public Namespace findPublicNamespaceForAssociatedNamespace(String clusterName, String namespaceName) { AppNamespace appNamespace = appNamespaceService.findPublicNamespaceByName(namespaceName); if (appNamespace == null) { throw BadRequestException.namespaceNotExists("", clusterName, namespaceName); } String appId = appNamespace.getAppId(); Namespace namespace = findOne(appId, clusterName, namespaceName); // default cluster's namespace if (Objects.equals(clusterName, ConfigConsts.CLUSTER_NAME_DEFAULT)) { return namespace; } // custom cluster's namespace not exist. // return default cluster's namespace if (namespace == null) { return findOne(appId, ConfigConsts.CLUSTER_NAME_DEFAULT, namespaceName); } // custom cluster's namespace exist and has published. // return custom cluster's namespace Release latestActiveRelease = releaseService.findLatestActiveRelease(namespace); if (latestActiveRelease != null) { return namespace; } Namespace defaultNamespace = findOne(appId, ConfigConsts.CLUSTER_NAME_DEFAULT, namespaceName); // custom cluster's namespace exist but never published. // and default cluster's namespace not exist. // return custom cluster's namespace if (defaultNamespace == null) { return namespace; } // custom cluster's namespace exist but never published. // and default cluster's namespace exist and has published. // return default cluster's namespace Release defaultNamespaceLatestActiveRelease = releaseService.findLatestActiveRelease(defaultNamespace); if (defaultNamespaceLatestActiveRelease != null) { return defaultNamespace; } // custom cluster's namespace exist but never published. // and default cluster's namespace exist but never published. // return custom cluster's namespace return namespace; } public List findPublicAppNamespaceAllNamespaces(String namespaceName, Pageable page) { AppNamespace publicAppNamespace = appNamespaceService.findPublicNamespaceByName(namespaceName); if (publicAppNamespace == null) { throw new BadRequestException( String.format("Public appNamespace not exists. NamespaceName = %s", namespaceName)); } List namespaces = namespaceRepository.findByNamespaceName(namespaceName, page); return filterChildNamespace(namespaces); } private List filterChildNamespace(List namespaces) { List result = new LinkedList<>(); if (CollectionUtils.isEmpty(namespaces)) { return result; } for (Namespace namespace : namespaces) { if (!isChildNamespace(namespace)) { result.add(namespace); } } return result; } public int countPublicAppNamespaceAssociatedNamespaces(String publicNamespaceName) { AppNamespace publicAppNamespace = appNamespaceService.findPublicNamespaceByName(publicNamespaceName); if (publicAppNamespace == null) { throw new BadRequestException( String.format("Public appNamespace not exists. NamespaceName = %s", publicNamespaceName)); } return namespaceRepository.countByNamespaceNameAndAppIdNot(publicNamespaceName, publicAppNamespace.getAppId()); } public List findNamespaces(String appId, String clusterName) { List namespaces = namespaceRepository.findByAppIdAndClusterNameOrderByIdAsc(appId, clusterName); if (namespaces == null) { return Collections.emptyList(); } return namespaces; } public List findByAppIdAndNamespaceName(String appId, String namespaceName) { return namespaceRepository.findByAppIdAndNamespaceNameOrderByIdAsc(appId, namespaceName); } public Namespace findChildNamespace(String appId, String parentClusterName, String namespaceName) { List namespaces = findByAppIdAndNamespaceName(appId, namespaceName); if (CollectionUtils.isEmpty(namespaces) || namespaces.size() == 1) { return null; } List childClusters = clusterService.findChildClusters(appId, parentClusterName); if (CollectionUtils.isEmpty(childClusters)) { return null; } Set childClusterNames = childClusters.stream().map(Cluster::getName).collect(Collectors.toSet()); // the child namespace is the intersection of the child clusters and child namespaces for (Namespace namespace : namespaces) { if (childClusterNames.contains(namespace.getClusterName())) { return namespace; } } return null; } public Namespace findChildNamespace(Namespace parentNamespace) { String appId = parentNamespace.getAppId(); String parentClusterName = parentNamespace.getClusterName(); String namespaceName = parentNamespace.getNamespaceName(); return findChildNamespace(appId, parentClusterName, namespaceName); } public Namespace findParentNamespace(String appId, String clusterName, String namespaceName) { return findParentNamespace(new Namespace(appId, clusterName, namespaceName)); } public Namespace findParentNamespace(Namespace namespace) { String appId = namespace.getAppId(); String namespaceName = namespace.getNamespaceName(); Cluster cluster = clusterService.findOne(appId, namespace.getClusterName()); if (cluster != null && cluster.getParentClusterId() > 0) { Cluster parentCluster = clusterService.findOne(cluster.getParentClusterId()); return findOne(appId, parentCluster.getName(), namespaceName); } return null; } public boolean isChildNamespace(String appId, String clusterName, String namespaceName) { return isChildNamespace(new Namespace(appId, clusterName, namespaceName)); } public boolean isChildNamespace(Namespace namespace) { return findParentNamespace(namespace) != null; } public boolean isNamespaceUnique(String appId, String cluster, String namespace) { Objects.requireNonNull(appId, "AppId must not be null"); Objects.requireNonNull(cluster, "Cluster must not be null"); Objects.requireNonNull(namespace, "Namespace must not be null"); return Objects.isNull( namespaceRepository.findByAppIdAndClusterNameAndNamespaceName(appId, cluster, namespace)); } @Transactional public void deleteByAppIdAndClusterName(String appId, String clusterName, String operator) { List toDeleteNamespaces = findNamespaces(appId, clusterName); for (Namespace namespace : toDeleteNamespaces) { deleteNamespace(namespace, operator); } } @Transactional public Namespace deleteNamespace(Namespace namespace, String operator) { String appId = namespace.getAppId(); String clusterName = namespace.getClusterName(); String namespaceName = namespace.getNamespaceName(); itemService.batchDelete(namespace.getId(), operator); commitService.batchDelete(appId, clusterName, namespace.getNamespaceName(), operator); // Child namespace releases should retain as long as the parent namespace exists, because parent // namespaces' release // histories need them if (!isChildNamespace(namespace)) { releaseService.batchDelete(appId, clusterName, namespace.getNamespaceName(), operator); } // delete child namespace Namespace childNamespace = findChildNamespace(namespace); if (childNamespace != null) { namespaceBranchService.deleteBranch(appId, clusterName, namespaceName, childNamespace.getClusterName(), NamespaceBranchStatus.DELETED, operator); // delete child namespace's releases. Notice: delete child namespace will not delete child // namespace's releases releaseService.batchDelete(appId, childNamespace.getClusterName(), namespaceName, operator); } releaseHistoryService.batchDelete(appId, clusterName, namespaceName, operator); instanceService.batchDeleteInstanceConfig(appId, clusterName, namespaceName); namespaceLockService.unlock(namespace.getId()); namespace.setDeleted(true); namespace.setDataChangeLastModifiedBy(operator); auditService.audit(Namespace.class.getSimpleName(), namespace.getId(), Audit.OP.DELETE, operator); Namespace deleted = namespaceRepository.save(namespace); // Publish release message to do some clean up in config service, such as updating the cache messageSender.sendMessage( ReleaseMessageKeyGenerator.generate(appId, clusterName, namespaceName), Topics.APOLLO_RELEASE_TOPIC); return deleted; } @Transactional public Namespace save(Namespace entity) { if (!isNamespaceUnique(entity.getAppId(), entity.getClusterName(), entity.getNamespaceName())) { throw new ServiceException("namespace not unique"); } if (bizConfig.isNamespaceNumLimitEnabled() && !bizConfig.namespaceNumLimitWhite().contains(entity.getAppId())) { int nowCount = namespaceRepository.countByAppIdAndClusterName(entity.getAppId(), entity.getClusterName()); if (nowCount >= bizConfig.namespaceNumLimit()) { throw new ServiceException( "namespace[appId = " + entity.getAppId() + ", cluster= " + entity.getClusterName() + "] nowCount= " + nowCount + ", maxCount =" + bizConfig.namespaceNumLimit()); } } entity.setId(0);// protection Namespace namespace = namespaceRepository.save(entity); auditService.audit(Namespace.class.getSimpleName(), namespace.getId(), Audit.OP.INSERT, namespace.getDataChangeCreatedBy()); return namespace; } @Transactional public Namespace update(Namespace namespace) { Namespace managedNamespace = namespaceRepository.findByAppIdAndClusterNameAndNamespaceName( namespace.getAppId(), namespace.getClusterName(), namespace.getNamespaceName()); BeanUtils.copyEntityProperties(namespace, managedNamespace); managedNamespace = namespaceRepository.save(managedNamespace); auditService.audit(Namespace.class.getSimpleName(), managedNamespace.getId(), Audit.OP.UPDATE, managedNamespace.getDataChangeLastModifiedBy()); return managedNamespace; } @Transactional public void instanceOfAppNamespaces(String appId, String clusterName, String createBy) { List appNamespaces = appNamespaceService.findByAppId(appId); for (AppNamespace appNamespace : appNamespaces) { Namespace ns = new Namespace(); ns.setAppId(appId); ns.setClusterName(clusterName); ns.setNamespaceName(appNamespace.getName()); ns.setDataChangeCreatedBy(createBy); ns.setDataChangeLastModifiedBy(createBy); namespaceRepository.save(ns); auditService.audit(Namespace.class.getSimpleName(), ns.getId(), Audit.OP.INSERT, createBy); } } public Map namespacePublishInfo(String appId) { List clusters = clusterService.findParentClusters(appId); if (CollectionUtils.isEmpty(clusters)) { throw BadRequestException.appNotExists(appId); } Map clusterHasNotPublishedItems = Maps.newHashMap(); for (Cluster cluster : clusters) { String clusterName = cluster.getName(); List namespaces = findNamespaces(appId, clusterName); for (Namespace namespace : namespaces) { boolean isNamespaceNotPublished = isNamespaceNotPublished(namespace); if (isNamespaceNotPublished) { clusterHasNotPublishedItems.put(clusterName, true); break; } } clusterHasNotPublishedItems.putIfAbsent(clusterName, false); } return clusterHasNotPublishedItems; } private boolean isNamespaceNotPublished(Namespace namespace) { Release latestRelease = releaseService.findLatestActiveRelease(namespace); long namespaceId = namespace.getId(); if (latestRelease == null) { Item lastItem = itemService.findLastOne(namespaceId); return lastItem != null; } Date lastPublishTime = latestRelease.getDataChangeLastModifiedTime(); List itemsModifiedAfterLastPublish = itemService.findItemsModifiedAfterDate(namespaceId, lastPublishTime); if (CollectionUtils.isEmpty(itemsModifiedAfterLastPublish)) { return false; } Map publishedConfiguration = GSON.fromJson(latestRelease.getConfigurations(), GsonType.CONFIG); for (Item item : itemsModifiedAfterLastPublish) { if (!Objects.equals(item.getValue(), publishedConfiguration.get(item.getKey()))) { return true; } } return false; } } ================================================ FILE: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/service/ReleaseHistoryService.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.service; import static com.ctrip.framework.apollo.biz.config.BizConfig.DEFAULT_RELEASE_HISTORY_RETENTION_SIZE; import com.ctrip.framework.apollo.biz.config.BizConfig; import com.ctrip.framework.apollo.biz.entity.Audit; import com.ctrip.framework.apollo.biz.entity.ReleaseHistory; import com.ctrip.framework.apollo.biz.repository.ReleaseHistoryRepository; import com.ctrip.framework.apollo.biz.repository.ReleaseRepository; import com.ctrip.framework.apollo.core.utils.ApolloThreadFactory; import com.ctrip.framework.apollo.tracer.Tracer; import com.google.common.collect.Queues; import com.google.gson.Gson; import java.util.List; import java.util.Optional; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Collectors; import jakarta.annotation.PostConstruct; import jakarta.annotation.PreDestroy; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.annotation.Transactional; import java.util.Date; import java.util.Map; import java.util.Set; import org.springframework.transaction.support.TransactionCallbackWithoutResult; import org.springframework.transaction.support.TransactionTemplate; /** * @author Jason Song(song_s@ctrip.com) */ @Service public class ReleaseHistoryService { private static final Logger logger = LoggerFactory.getLogger(ReleaseHistoryService.class); private static final Gson GSON = new Gson(); private static final int CLEAN_QUEUE_MAX_SIZE = 100; private final BlockingQueue releaseClearQueue = Queues.newLinkedBlockingQueue(CLEAN_QUEUE_MAX_SIZE); private final ExecutorService cleanExecutorService = Executors.newSingleThreadExecutor(ApolloThreadFactory.create("ReleaseHistoryService", true)); private final AtomicBoolean cleanStopped = new AtomicBoolean(false); private final ReleaseHistoryRepository releaseHistoryRepository; private final ReleaseRepository releaseRepository; private final AuditService auditService; private final BizConfig bizConfig; private final TransactionTemplate transactionManager; public ReleaseHistoryService(final ReleaseHistoryRepository releaseHistoryRepository, final ReleaseRepository releaseRepository, final AuditService auditService, final BizConfig bizConfig, final TransactionTemplate transactionManager) { this.releaseHistoryRepository = releaseHistoryRepository; this.releaseRepository = releaseRepository; this.auditService = auditService; this.bizConfig = bizConfig; this.transactionManager = transactionManager; } @PostConstruct private void initialize() { cleanExecutorService.submit(() -> { while (!cleanStopped.get() && !Thread.currentThread().isInterrupted()) { try { ReleaseHistory releaseHistory = releaseClearQueue.poll(1, TimeUnit.SECONDS); if (releaseHistory != null) { this.cleanReleaseHistory(releaseHistory); } else { TimeUnit.MINUTES.sleep(1); } } catch (Throwable ex) { logger.error("Clean releaseHistory failed", ex); Tracer.logError(ex); } } }); } public Page findReleaseHistoriesByNamespace(String appId, String clusterName, String namespaceName, Pageable pageable) { return releaseHistoryRepository.findByAppIdAndClusterNameAndNamespaceNameOrderByIdDesc(appId, clusterName, namespaceName, pageable); } public Page findByReleaseIdAndOperation(long releaseId, int operation, Pageable page) { return releaseHistoryRepository.findByReleaseIdAndOperationOrderByIdDesc(releaseId, operation, page); } public Page findByPreviousReleaseIdAndOperation(long previousReleaseId, int operation, Pageable page) { return releaseHistoryRepository .findByPreviousReleaseIdAndOperationOrderByIdDesc(previousReleaseId, operation, page); } public Page findByReleaseIdAndOperationInOrderByIdDesc(long releaseId, Set operations, Pageable page) { return releaseHistoryRepository.findByReleaseIdAndOperationInOrderByIdDesc(releaseId, operations, page); } @Transactional public ReleaseHistory createReleaseHistory(String appId, String clusterName, String namespaceName, String branchName, long releaseId, long previousReleaseId, int operation, Map operationContext, String operator) { ReleaseHistory releaseHistory = new ReleaseHistory(); releaseHistory.setAppId(appId); releaseHistory.setClusterName(clusterName); releaseHistory.setNamespaceName(namespaceName); releaseHistory.setBranchName(branchName); releaseHistory.setReleaseId(releaseId); releaseHistory.setPreviousReleaseId(previousReleaseId); releaseHistory.setOperation(operation); if (operationContext == null) { releaseHistory.setOperationContext("{}"); // default empty object } else { releaseHistory.setOperationContext(GSON.toJson(operationContext)); } releaseHistory.setDataChangeCreatedTime(new Date()); releaseHistory.setDataChangeCreatedBy(operator); releaseHistory.setDataChangeLastModifiedBy(operator); releaseHistoryRepository.save(releaseHistory); auditService.audit(ReleaseHistory.class.getSimpleName(), releaseHistory.getId(), Audit.OP.INSERT, releaseHistory.getDataChangeCreatedBy()); int releaseHistoryRetentionLimit = this.getReleaseHistoryRetentionLimit(releaseHistory); if (releaseHistoryRetentionLimit != DEFAULT_RELEASE_HISTORY_RETENTION_SIZE) { if (!releaseClearQueue.offer(releaseHistory)) { logger.warn("releaseClearQueue is full, failed to add task to clean queue, " + "clean queue max size:{}", CLEAN_QUEUE_MAX_SIZE); } } return releaseHistory; } @Transactional public int batchDelete(String appId, String clusterName, String namespaceName, String operator) { return releaseHistoryRepository.batchDelete(appId, clusterName, namespaceName, operator); } private Optional releaseHistoryRetentionMaxId(ReleaseHistory releaseHistory, int releaseHistoryRetentionSize) { Page releaseHistoryPage = releaseHistoryRepository .findByAppIdAndClusterNameAndNamespaceNameAndBranchNameOrderByIdDesc( releaseHistory.getAppId(), releaseHistory.getClusterName(), releaseHistory.getNamespaceName(), releaseHistory.getBranchName(), PageRequest.of(releaseHistoryRetentionSize, 1)); if (releaseHistoryPage.isEmpty()) { return Optional.empty(); } return Optional.of(releaseHistoryPage.getContent().get(0).getId()); } private void cleanReleaseHistory(ReleaseHistory cleanRelease) { String appId = cleanRelease.getAppId(); String clusterName = cleanRelease.getClusterName(); String namespaceName = cleanRelease.getNamespaceName(); String branchName = cleanRelease.getBranchName(); int retentionLimit = this.getReleaseHistoryRetentionLimit(cleanRelease); // Second check, if retentionLimit is default value, do not clean if (retentionLimit == DEFAULT_RELEASE_HISTORY_RETENTION_SIZE) { return; } Optional maxId = this.releaseHistoryRetentionMaxId(cleanRelease, retentionLimit); if (!maxId.isPresent()) { return; } boolean hasMore = true; while (hasMore && !Thread.currentThread().isInterrupted()) { List cleanReleaseHistoryList = releaseHistoryRepository .findFirst100ByAppIdAndClusterNameAndNamespaceNameAndBranchNameAndIdLessThanEqualOrderByIdAsc( appId, clusterName, namespaceName, branchName, maxId.get()); Set releaseIds = cleanReleaseHistoryList.stream().map(ReleaseHistory::getReleaseId) .collect(Collectors.toSet()); transactionManager.execute(new TransactionCallbackWithoutResult() { @Override protected void doInTransactionWithoutResult(TransactionStatus status) { releaseHistoryRepository.deleteAll(cleanReleaseHistoryList); releaseRepository.deleteAllById(releaseIds); } }); hasMore = cleanReleaseHistoryList.size() == 100; } } private int getReleaseHistoryRetentionLimit(ReleaseHistory releaseHistory) { String overrideKey = String.format("%s+%s+%s+%s", releaseHistory.getAppId(), releaseHistory.getClusterName(), releaseHistory.getNamespaceName(), releaseHistory.getBranchName()); Map overrideMap = bizConfig.releaseHistoryRetentionSizeOverride(); return overrideMap.getOrDefault(overrideKey, bizConfig.releaseHistoryRetentionSize()); } @PreDestroy void stopClean() { cleanStopped.set(true); } } ================================================ FILE: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/service/ReleaseMessageService.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.service; import com.ctrip.framework.apollo.biz.entity.ReleaseMessage; import com.ctrip.framework.apollo.biz.repository.ReleaseMessageRepository; import com.ctrip.framework.apollo.tracer.Tracer; import com.google.common.collect.Lists; import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; import java.util.Collection; import java.util.Collections; import java.util.List; /** * @author Jason Song(song_s@ctrip.com) */ @Service public class ReleaseMessageService { private final ReleaseMessageRepository releaseMessageRepository; public ReleaseMessageService(final ReleaseMessageRepository releaseMessageRepository) { this.releaseMessageRepository = releaseMessageRepository; } public ReleaseMessage findLatestReleaseMessageForMessages(Collection messages) { if (CollectionUtils.isEmpty(messages)) { return null; } return releaseMessageRepository.findTopByMessageInOrderByIdDesc(messages); } public List findLatestReleaseMessagesGroupByMessages( Collection messages) { if (CollectionUtils.isEmpty(messages)) { return Collections.emptyList(); } List result = releaseMessageRepository.findLatestReleaseMessagesGroupByMessages(messages); List releaseMessages = Lists.newArrayList(); for (Object[] o : result) { try { ReleaseMessage releaseMessage = new ReleaseMessage((String) o[0]); releaseMessage.setId((Long) o[1]); releaseMessages.add(releaseMessage); } catch (Exception ex) { Tracer.logError("Parsing LatestReleaseMessagesGroupByMessages failed", ex); } } return releaseMessages; } } ================================================ FILE: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/service/ReleaseService.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.service; import com.ctrip.framework.apollo.biz.entity.Audit; import com.ctrip.framework.apollo.biz.entity.GrayReleaseRule; import com.ctrip.framework.apollo.biz.entity.Item; import com.ctrip.framework.apollo.biz.entity.Namespace; import com.ctrip.framework.apollo.biz.entity.NamespaceLock; import com.ctrip.framework.apollo.biz.entity.Release; import com.ctrip.framework.apollo.biz.entity.ReleaseHistory; import com.ctrip.framework.apollo.biz.repository.ReleaseRepository; import com.ctrip.framework.apollo.biz.utils.ReleaseKeyGenerator; import com.ctrip.framework.apollo.common.constants.GsonType; import com.ctrip.framework.apollo.common.constants.ReleaseOperation; import com.ctrip.framework.apollo.common.constants.ReleaseOperationContext; import com.ctrip.framework.apollo.common.dto.ItemChangeSets; import com.ctrip.framework.apollo.common.exception.BadRequestException; import com.ctrip.framework.apollo.common.exception.NotFoundException; import com.ctrip.framework.apollo.common.utils.GrayReleaseRuleItemTransformer; import com.ctrip.framework.apollo.core.utils.StringUtils; import com.google.common.base.Strings; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; import java.lang.reflect.Type; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import org.apache.commons.lang.time.FastDateFormat; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.CollectionUtils; /** * @author Jason Song(song_s@ctrip.com) */ @Service public class ReleaseService { private static final FastDateFormat TIMESTAMP_FORMAT = FastDateFormat.getInstance("yyyyMMddHHmmss"); private static final Gson GSON = new Gson(); private static final Set BRANCH_RELEASE_OPERATIONS = Sets.newHashSet( ReleaseOperation.GRAY_RELEASE, ReleaseOperation.MASTER_NORMAL_RELEASE_MERGE_TO_GRAY, ReleaseOperation.MATER_ROLLBACK_MERGE_TO_GRAY); private static final Pageable FIRST_ITEM = PageRequest.of(0, 1); private static final Type OPERATION_CONTEXT_TYPE_REFERENCE = new TypeToken>() {}.getType(); private final ReleaseRepository releaseRepository; private final ItemService itemService; private final AuditService auditService; private final NamespaceLockService namespaceLockService; private final NamespaceService namespaceService; private final NamespaceBranchService namespaceBranchService; private final ReleaseHistoryService releaseHistoryService; private final ItemSetService itemSetService; public ReleaseService(final ReleaseRepository releaseRepository, final ItemService itemService, final AuditService auditService, final NamespaceLockService namespaceLockService, final NamespaceService namespaceService, final NamespaceBranchService namespaceBranchService, final ReleaseHistoryService releaseHistoryService, final ItemSetService itemSetService) { this.releaseRepository = releaseRepository; this.itemService = itemService; this.auditService = auditService; this.namespaceLockService = namespaceLockService; this.namespaceService = namespaceService; this.namespaceBranchService = namespaceBranchService; this.releaseHistoryService = releaseHistoryService; this.itemSetService = itemSetService; } public Release findOne(long releaseId) { return releaseRepository.findById(releaseId).orElse(null); } public Release findActiveOne(long releaseId) { return releaseRepository.findByIdAndIsAbandonedFalse(releaseId); } public List findByReleaseIds(Set releaseIds) { Iterable releases = releaseRepository.findAllById(releaseIds); if (releases == null) { return Collections.emptyList(); } return Lists.newArrayList(releases); } public List findByReleaseKeys(Set releaseKeys) { return releaseRepository.findByReleaseKeyIn(releaseKeys); } public Release findByReleaseKey(String releaseKey) { return releaseRepository.findByReleaseKey(releaseKey); } public Release findLatestActiveRelease(Namespace namespace) { return findLatestActiveRelease(namespace.getAppId(), namespace.getClusterName(), namespace.getNamespaceName()); } public Release findLatestActiveRelease(String appId, String clusterName, String namespaceName) { return releaseRepository .findFirstByAppIdAndClusterNameAndNamespaceNameAndIsAbandonedFalseOrderByIdDesc(appId, clusterName, namespaceName); } public List findAllReleases(String appId, String clusterName, String namespaceName, Pageable page) { List releases = releaseRepository.findByAppIdAndClusterNameAndNamespaceNameOrderByIdDesc(appId, clusterName, namespaceName, page); if (releases == null) { return Collections.emptyList(); } return releases; } public List findActiveReleases(String appId, String clusterName, String namespaceName, Pageable page) { List releases = releaseRepository.findByAppIdAndClusterNameAndNamespaceNameAndIsAbandonedFalseOrderByIdDesc( appId, clusterName, namespaceName, page); if (releases == null) { return Collections.emptyList(); } return releases; } private List findActiveReleasesBetween(String appId, String clusterName, String namespaceName, long fromReleaseId, long toReleaseId) { List releases = releaseRepository .findByAppIdAndClusterNameAndNamespaceNameAndIsAbandonedFalseAndIdBetweenOrderByIdDesc( appId, clusterName, namespaceName, fromReleaseId, toReleaseId); if (releases == null) { return Collections.emptyList(); } return releases; } @Transactional public Release mergeBranchChangeSetsAndRelease(Namespace namespace, String branchName, String releaseName, String releaseComment, boolean isEmergencyPublish, ItemChangeSets changeSets) { checkLock(namespace, isEmergencyPublish, changeSets.getDataChangeLastModifiedBy()); itemSetService.updateSet(namespace, changeSets); Release branchRelease = findLatestActiveRelease(namespace.getAppId(), branchName, namespace.getNamespaceName()); long branchReleaseId = branchRelease == null ? 0 : branchRelease.getId(); Map operateNamespaceItems = getNamespaceItems(namespace); Map operationContext = Maps.newLinkedHashMap(); operationContext.put(ReleaseOperationContext.SOURCE_BRANCH, branchName); operationContext.put(ReleaseOperationContext.BASE_RELEASE_ID, branchReleaseId); operationContext.put(ReleaseOperationContext.IS_EMERGENCY_PUBLISH, isEmergencyPublish); return masterRelease(namespace, releaseName, releaseComment, operateNamespaceItems, changeSets.getDataChangeLastModifiedBy(), ReleaseOperation.GRAY_RELEASE_MERGE_TO_MASTER, operationContext); } @Transactional public Release publish(Namespace namespace, String releaseName, String releaseComment, String operator, boolean isEmergencyPublish) { checkLock(namespace, isEmergencyPublish, operator); Map operateNamespaceItems = getNamespaceItems(namespace); Namespace parentNamespace = namespaceService.findParentNamespace(namespace); // branch release if (parentNamespace != null) { return publishBranchNamespace(parentNamespace, namespace, operateNamespaceItems, releaseName, releaseComment, operator, isEmergencyPublish); } Namespace childNamespace = namespaceService.findChildNamespace(namespace); Release previousRelease = null; if (childNamespace != null) { previousRelease = findLatestActiveRelease(namespace); } // master release Map operationContext = Maps.newLinkedHashMap(); operationContext.put(ReleaseOperationContext.IS_EMERGENCY_PUBLISH, isEmergencyPublish); Release release = masterRelease(namespace, releaseName, releaseComment, operateNamespaceItems, operator, ReleaseOperation.NORMAL_RELEASE, operationContext); // merge to branch and auto release if (childNamespace != null) { mergeFromMasterAndPublishBranch(namespace, childNamespace, operateNamespaceItems, releaseName, releaseComment, operator, previousRelease, release, isEmergencyPublish); } return release; } private Release publishBranchNamespace(Namespace parentNamespace, Namespace childNamespace, Map childNamespaceItems, String releaseName, String releaseComment, String operator, boolean isEmergencyPublish, Set grayDelKeys) { Release parentLatestRelease = findLatestActiveRelease(parentNamespace); Map parentConfigurations = parentLatestRelease != null ? GSON.fromJson(parentLatestRelease.getConfigurations(), GsonType.CONFIG) : new LinkedHashMap<>(); long baseReleaseId = parentLatestRelease == null ? 0 : parentLatestRelease.getId(); Map configsToPublish = mergeConfiguration(parentConfigurations, childNamespaceItems); if (!(grayDelKeys == null || grayDelKeys.isEmpty())) { for (String key : grayDelKeys) { configsToPublish.remove(key); } } return branchRelease(parentNamespace, childNamespace, releaseName, releaseComment, configsToPublish, baseReleaseId, operator, ReleaseOperation.GRAY_RELEASE, isEmergencyPublish, childNamespaceItems.keySet()); } @Transactional public Release grayDeletionPublish(Namespace namespace, String releaseName, String releaseComment, String operator, boolean isEmergencyPublish, Set grayDelKeys) { checkLock(namespace, isEmergencyPublish, operator); Map operateNamespaceItems = getNamespaceItems(namespace); Namespace parentNamespace = namespaceService.findParentNamespace(namespace); // branch release if (parentNamespace != null) { return publishBranchNamespace(parentNamespace, namespace, operateNamespaceItems, releaseName, releaseComment, operator, isEmergencyPublish, grayDelKeys); } throw new NotFoundException("Parent namespace not found"); } private void checkLock(Namespace namespace, boolean isEmergencyPublish, String operator) { if (!isEmergencyPublish) { NamespaceLock lock = namespaceLockService.findLock(namespace.getId()); if (lock != null && lock.getDataChangeCreatedBy().equals(operator)) { throw new BadRequestException("Config can not be published by yourself."); } } } private void mergeFromMasterAndPublishBranch(Namespace parentNamespace, Namespace childNamespace, Map parentNamespaceItems, String releaseName, String releaseComment, String operator, Release masterPreviousRelease, Release parentRelease, boolean isEmergencyPublish) { // create release for child namespace Release childNamespaceLatestActiveRelease = findLatestActiveRelease(childNamespace); Map childReleaseConfiguration; Collection branchReleaseKeys; if (childNamespaceLatestActiveRelease != null) { childReleaseConfiguration = GSON.fromJson(childNamespaceLatestActiveRelease.getConfigurations(), GsonType.CONFIG); branchReleaseKeys = getBranchReleaseKeys(childNamespaceLatestActiveRelease.getId()); } else { childReleaseConfiguration = Collections.emptyMap(); branchReleaseKeys = null; } Map parentNamespaceOldConfiguration = masterPreviousRelease == null ? null : GSON.fromJson(masterPreviousRelease.getConfigurations(), GsonType.CONFIG); Map childNamespaceToPublishConfigs = calculateChildNamespaceToPublishConfiguration(parentNamespaceOldConfiguration, parentNamespaceItems, childReleaseConfiguration, branchReleaseKeys); // compare if (!childNamespaceToPublishConfigs.equals(childReleaseConfiguration)) { branchRelease(parentNamespace, childNamespace, releaseName, releaseComment, childNamespaceToPublishConfigs, parentRelease.getId(), operator, ReleaseOperation.MASTER_NORMAL_RELEASE_MERGE_TO_GRAY, isEmergencyPublish, branchReleaseKeys); } } private Collection getBranchReleaseKeys(long releaseId) { Page releaseHistories = releaseHistoryService.findByReleaseIdAndOperationInOrderByIdDesc(releaseId, BRANCH_RELEASE_OPERATIONS, FIRST_ITEM); if (!releaseHistories.hasContent()) { return null; } String operationContextJson = releaseHistories.getContent().get(0).getOperationContext(); if (Strings.isNullOrEmpty(operationContextJson) || !operationContextJson.contains(ReleaseOperationContext.BRANCH_RELEASE_KEYS)) { return null; } Map operationContext = GSON.fromJson(operationContextJson, OPERATION_CONTEXT_TYPE_REFERENCE); if (operationContext == null) { return null; } return (Collection) operationContext.get(ReleaseOperationContext.BRANCH_RELEASE_KEYS); } private Release publishBranchNamespace(Namespace parentNamespace, Namespace childNamespace, Map childNamespaceItems, String releaseName, String releaseComment, String operator, boolean isEmergencyPublish) { return publishBranchNamespace(parentNamespace, childNamespace, childNamespaceItems, releaseName, releaseComment, operator, isEmergencyPublish, null); } private Release masterRelease(Namespace namespace, String releaseName, String releaseComment, Map configurations, String operator, int releaseOperation, Map operationContext) { Release lastActiveRelease = findLatestActiveRelease(namespace); long previousReleaseId = lastActiveRelease == null ? 0 : lastActiveRelease.getId(); Release release = createRelease(namespace, releaseName, releaseComment, configurations, operator); releaseHistoryService.createReleaseHistory(namespace.getAppId(), namespace.getClusterName(), namespace.getNamespaceName(), namespace.getClusterName(), release.getId(), previousReleaseId, releaseOperation, operationContext, operator); return release; } private Release branchRelease(Namespace parentNamespace, Namespace childNamespace, String releaseName, String releaseComment, Map configurations, long baseReleaseId, String operator, int releaseOperation, boolean isEmergencyPublish, Collection branchReleaseKeys) { Release previousRelease = findLatestActiveRelease(childNamespace.getAppId(), childNamespace.getClusterName(), childNamespace.getNamespaceName()); long previousReleaseId = previousRelease == null ? 0 : previousRelease.getId(); Map releaseOperationContext = Maps.newLinkedHashMap(); releaseOperationContext.put(ReleaseOperationContext.BASE_RELEASE_ID, baseReleaseId); releaseOperationContext.put(ReleaseOperationContext.IS_EMERGENCY_PUBLISH, isEmergencyPublish); releaseOperationContext.put(ReleaseOperationContext.BRANCH_RELEASE_KEYS, branchReleaseKeys); Release release = createRelease(childNamespace, releaseName, releaseComment, configurations, operator); // update gray release rules GrayReleaseRule grayReleaseRule = namespaceBranchService.updateRulesReleaseId(childNamespace.getAppId(), parentNamespace.getClusterName(), childNamespace.getNamespaceName(), childNamespace.getClusterName(), release.getId(), operator); if (grayReleaseRule != null) { releaseOperationContext.put(ReleaseOperationContext.RULES, GrayReleaseRuleItemTransformer.batchTransformFromJSON(grayReleaseRule.getRules())); } releaseHistoryService.createReleaseHistory(parentNamespace.getAppId(), parentNamespace.getClusterName(), parentNamespace.getNamespaceName(), childNamespace.getClusterName(), release.getId(), previousReleaseId, releaseOperation, releaseOperationContext, operator); return release; } private Map mergeConfiguration(Map baseConfigurations, Map coverConfigurations) { int expectedSize = baseConfigurations.size() + coverConfigurations.size(); Map result = Maps.newLinkedHashMapWithExpectedSize(expectedSize); // copy base configuration result.putAll(baseConfigurations); // update and publish result.putAll(coverConfigurations); return result; } private Map getNamespaceItems(Namespace namespace) { List items = itemService.findItemsWithOrdered(namespace.getId()); Map configurations = new LinkedHashMap<>(); for (Item item : items) { if (StringUtils.isEmpty(item.getKey())) { continue; } configurations.put(item.getKey(), item.getValue()); } return configurations; } private Release createRelease(Namespace namespace, String name, String comment, Map configurations, String operator) { Release release = new Release(); release.setReleaseKey(ReleaseKeyGenerator.generateReleaseKey(namespace)); release.setDataChangeCreatedTime(new Date()); release.setDataChangeCreatedBy(operator); release.setDataChangeLastModifiedBy(operator); release.setName(name); release.setComment(comment); release.setAppId(namespace.getAppId()); release.setClusterName(namespace.getClusterName()); release.setNamespaceName(namespace.getNamespaceName()); release.setConfigurations(GSON.toJson(configurations)); release = releaseRepository.save(release); namespaceLockService.unlock(namespace.getId()); auditService.audit(Release.class.getSimpleName(), release.getId(), Audit.OP.INSERT, release.getDataChangeCreatedBy()); return release; } @Transactional public Release rollback(long releaseId, String operator) { Release release = findOne(releaseId); if (release == null) { throw NotFoundException.releaseNotFound(releaseId); } if (release.isAbandoned()) { throw new BadRequestException("release is not active"); } String appId = release.getAppId(); String clusterName = release.getClusterName(); String namespaceName = release.getNamespaceName(); PageRequest page = PageRequest.of(0, 2); List twoLatestActiveReleases = findActiveReleases(appId, clusterName, namespaceName, page); if (twoLatestActiveReleases == null || twoLatestActiveReleases.size() < 2) { throw new BadRequestException( "Can't rollback namespace(appId=%s, clusterName=%s, namespaceName=%s) because there is only one active release", appId, clusterName, namespaceName); } release.setAbandoned(true); release.setDataChangeLastModifiedBy(operator); releaseRepository.save(release); releaseHistoryService.createReleaseHistory(appId, clusterName, namespaceName, clusterName, twoLatestActiveReleases.get(1).getId(), release.getId(), ReleaseOperation.ROLLBACK, null, operator); // publish child namespace if namespace has child rollbackChildNamespace(appId, clusterName, namespaceName, twoLatestActiveReleases, operator); return release; } @Transactional public Release rollbackTo(long releaseId, long toReleaseId, String operator) { if (releaseId == toReleaseId) { throw new BadRequestException("current release equal to target release"); } Release release = findOne(releaseId); Release toRelease = findOne(toReleaseId); if (release == null) { throw NotFoundException.releaseNotFound(releaseId); } if (toRelease == null) { throw NotFoundException.releaseNotFound(toReleaseId); } if (release.isAbandoned() || toRelease.isAbandoned()) { throw new BadRequestException("release is not active"); } String appId = release.getAppId(); String clusterName = release.getClusterName(); String namespaceName = release.getNamespaceName(); List releases = findActiveReleasesBetween(appId, clusterName, namespaceName, toReleaseId, releaseId); for (int i = 0; i < releases.size() - 1; i++) { releases.get(i).setAbandoned(true); releases.get(i).setDataChangeLastModifiedBy(operator); } releaseRepository.saveAll(releases); releaseHistoryService.createReleaseHistory(appId, clusterName, namespaceName, clusterName, toReleaseId, release.getId(), ReleaseOperation.ROLLBACK, null, operator); // publish child namespace if namespace has child rollbackChildNamespace(appId, clusterName, namespaceName, Lists.newArrayList(release, toRelease), operator); return release; } private void rollbackChildNamespace(String appId, String clusterName, String namespaceName, List parentNamespaceTwoLatestActiveRelease, String operator) { Namespace parentNamespace = namespaceService.findOne(appId, clusterName, namespaceName); Namespace childNamespace = namespaceService.findChildNamespace(appId, clusterName, namespaceName); if (parentNamespace == null || childNamespace == null) { return; } Release childNamespaceLatestActiveRelease = findLatestActiveRelease(childNamespace); Map childReleaseConfiguration; Collection branchReleaseKeys; if (childNamespaceLatestActiveRelease != null) { childReleaseConfiguration = GSON.fromJson(childNamespaceLatestActiveRelease.getConfigurations(), GsonType.CONFIG); branchReleaseKeys = getBranchReleaseKeys(childNamespaceLatestActiveRelease.getId()); } else { childReleaseConfiguration = Collections.emptyMap(); branchReleaseKeys = null; } Release abandonedRelease = parentNamespaceTwoLatestActiveRelease.get(0); Release parentNamespaceNewLatestRelease = parentNamespaceTwoLatestActiveRelease.get(1); Map parentNamespaceAbandonedConfiguration = GSON.fromJson(abandonedRelease.getConfigurations(), GsonType.CONFIG); Map parentNamespaceNewLatestConfiguration = GSON.fromJson(parentNamespaceNewLatestRelease.getConfigurations(), GsonType.CONFIG); Map childNamespaceNewConfiguration = calculateChildNamespaceToPublishConfiguration(parentNamespaceAbandonedConfiguration, parentNamespaceNewLatestConfiguration, childReleaseConfiguration, branchReleaseKeys); // compare if (!childNamespaceNewConfiguration.equals(childReleaseConfiguration)) { branchRelease(parentNamespace, childNamespace, TIMESTAMP_FORMAT.format(new Date()) + "-master-rollback-merge-to-gray", "", childNamespaceNewConfiguration, parentNamespaceNewLatestRelease.getId(), operator, ReleaseOperation.MATER_ROLLBACK_MERGE_TO_GRAY, false, branchReleaseKeys); } } private Map calculateChildNamespaceToPublishConfiguration( Map parentNamespaceOldConfiguration, Map parentNamespaceNewConfiguration, Map childNamespaceLatestActiveConfiguration, Collection branchReleaseKeys) { // first. calculate child namespace modified configs Map childNamespaceModifiedConfiguration = calculateBranchModifiedItemsAccordingToRelease(parentNamespaceOldConfiguration, childNamespaceLatestActiveConfiguration, branchReleaseKeys); // second. append child namespace modified configs to parent namespace new latest configuration return mergeConfiguration(parentNamespaceNewConfiguration, childNamespaceModifiedConfiguration); } private Map calculateBranchModifiedItemsAccordingToRelease( Map masterReleaseConfigs, Map branchReleaseConfigs, Collection branchReleaseKeys) { Map modifiedConfigs = new LinkedHashMap<>(); if (CollectionUtils.isEmpty(branchReleaseConfigs)) { return modifiedConfigs; } // new logic, retrieve modified configurations based on branch release keys if (branchReleaseKeys != null) { for (String branchReleaseKey : branchReleaseKeys) { if (branchReleaseConfigs.containsKey(branchReleaseKey)) { modifiedConfigs.put(branchReleaseKey, branchReleaseConfigs.get(branchReleaseKey)); } } return modifiedConfigs; } // old logic, retrieve modified configurations by comparing branchReleaseConfigs with // masterReleaseConfigs if (CollectionUtils.isEmpty(masterReleaseConfigs)) { return branchReleaseConfigs; } for (Map.Entry entry : branchReleaseConfigs.entrySet()) { if (!Objects.equals(entry.getValue(), masterReleaseConfigs.get(entry.getKey()))) { modifiedConfigs.put(entry.getKey(), entry.getValue()); } } return modifiedConfigs; } @Transactional public int batchDelete(String appId, String clusterName, String namespaceName, String operator) { return releaseRepository.batchDelete(appId, clusterName, namespaceName, operator); } } ================================================ FILE: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/service/ServerConfigService.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.service; import com.ctrip.framework.apollo.biz.entity.ServerConfig; import com.ctrip.framework.apollo.biz.repository.ServerConfigRepository; import com.google.common.collect.Lists; import java.util.List; import java.util.Objects; import jakarta.transaction.Transactional; import org.springframework.stereotype.Service; /** * @author kl (http://kailing.pub) * @since 2022/12/13 */ @Service public class ServerConfigService { private final ServerConfigRepository serverConfigRepository; public ServerConfigService(ServerConfigRepository serverConfigRepository) { this.serverConfigRepository = serverConfigRepository; } public List findAll() { Iterable serverConfigs = serverConfigRepository.findAll(); return Lists.newArrayList(serverConfigs); } @Transactional public ServerConfig createOrUpdateConfig(ServerConfig serverConfig) { ServerConfig storedConfig = serverConfigRepository.findByKey(serverConfig.getKey()); if (Objects.isNull(storedConfig)) {// create serverConfig.setId(0L);// 为空,设置ID 为0,jpa执行新增操作 if (Objects.isNull(serverConfig.getCluster())) { serverConfig.setCluster("default"); } return serverConfigRepository.save(serverConfig); } // update storedConfig.setComment(serverConfig.getComment()); storedConfig.setDataChangeLastModifiedBy(serverConfig.getDataChangeLastModifiedBy()); storedConfig.setValue(serverConfig.getValue()); return serverConfigRepository.save(storedConfig); } } ================================================ FILE: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/service/ServiceRegistryService.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.service; import com.ctrip.framework.apollo.biz.entity.ServiceRegistry; import com.ctrip.framework.apollo.biz.repository.ServiceRegistryRepository; import java.time.Duration; import java.time.LocalDateTime; import java.util.List; import org.springframework.transaction.annotation.Transactional; public class ServiceRegistryService { private final ServiceRegistryRepository repository; public ServiceRegistryService(ServiceRegistryRepository repository) { this.repository = repository; } public ServiceRegistry saveIfNotExistByServiceNameAndUri(ServiceRegistry serviceRegistry) { ServiceRegistry serviceRegistrySaved = this.repository .findByServiceNameAndUri(serviceRegistry.getServiceName(), serviceRegistry.getUri()); final LocalDateTime now = LocalDateTime.now(); if (null == serviceRegistrySaved) { serviceRegistrySaved = serviceRegistry; serviceRegistrySaved.setDataChangeCreatedTime(now); serviceRegistrySaved.setDataChangeLastModifiedTime(now); } else { // update serviceRegistrySaved.setCluster(serviceRegistry.getCluster()); serviceRegistrySaved.setMetadata(serviceRegistry.getMetadata()); serviceRegistrySaved.setDataChangeLastModifiedTime(now); } return this.repository.save(serviceRegistrySaved); } @Transactional public void delete(ServiceRegistry serviceRegistry) { this.repository.deleteByServiceNameAndUri(serviceRegistry.getServiceName(), serviceRegistry.getUri()); } public List findByServiceNameDataChangeLastModifiedTimeGreaterThan( String serviceName, LocalDateTime localDateTime) { return this.repository.findByServiceNameAndDataChangeLastModifiedTimeGreaterThan(serviceName, localDateTime); } @Transactional public List deleteTimeBefore(Duration duration) { LocalDateTime time = LocalDateTime.now().minus(duration); return this.repository.deleteByDataChangeLastModifiedTimeLessThan(time); } } ================================================ FILE: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/utils/ConfigChangeContentBuilder.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.utils; import com.ctrip.framework.apollo.biz.entity.Item; import com.ctrip.framework.apollo.core.utils.StringUtils; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import java.util.Date; import java.util.LinkedList; import java.util.List; import org.springframework.beans.BeanUtils; public class ConfigChangeContentBuilder { private static final Gson GSON = new GsonBuilder().setDateFormat("yyyy-MM-dd HH:mm:ss").create(); private final List createItems = new LinkedList<>(); private final List updateItems = new LinkedList<>(); private final List deleteItems = new LinkedList<>(); public ConfigChangeContentBuilder createItem(Item item) { if (!StringUtils.isEmpty(item.getKey())) { createItems.add(cloneItem(item)); } return this; } public ConfigChangeContentBuilder updateItem(Item oldItem, Item newItem) { if (!oldItem.getValue().equals(newItem.getValue())) { ItemPair itemPair = new ItemPair(cloneItem(oldItem), cloneItem(newItem)); updateItems.add(itemPair); } return this; } public ConfigChangeContentBuilder deleteItem(Item item) { if (!StringUtils.isEmpty(item.getKey())) { deleteItems.add(cloneItem(item)); } return this; } public boolean hasContent() { return !createItems.isEmpty() || !updateItems.isEmpty() || !deleteItems.isEmpty(); } public String build() { // Because there is no update time for the first commit to the transaction, // it is updated uniformly during building. Date now = new Date(); for (Item item : createItems) { item.setDataChangeLastModifiedTime(now); } for (ItemPair item : updateItems) { item.newItem.setDataChangeLastModifiedTime(now); } for (Item item : deleteItems) { item.setDataChangeLastModifiedTime(now); } return GSON.toJson(this); } static class ItemPair { Item oldItem; Item newItem; public ItemPair(Item oldItem, Item newItem) { this.oldItem = oldItem; this.newItem = newItem; } } Item cloneItem(Item source) { Item target = new Item(); BeanUtils.copyProperties(source, target); return target; } public static ConfigChangeContentBuilder convertJsonString(String content) { return GSON.fromJson(content, ConfigChangeContentBuilder.class); } public List getCreateItems() { return createItems; } public List getUpdateItems() { return updateItems; } public List getDeleteItems() { return deleteItems; } } ================================================ FILE: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/utils/EntityManagerUtil.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.utils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.orm.jpa.EntityManagerFactoryAccessor; import org.springframework.orm.jpa.EntityManagerFactoryUtils; import org.springframework.orm.jpa.EntityManagerHolder; import org.springframework.stereotype.Component; import org.springframework.transaction.support.TransactionSynchronizationManager; /** * @author Jason Song(song_s@ctrip.com) */ @Component public class EntityManagerUtil extends EntityManagerFactoryAccessor { private static final Logger logger = LoggerFactory.getLogger(EntityManagerUtil.class); /** * close the entity manager. * Use it with caution! This is only intended for use with async request, which Spring won't * close the entity manager until the async request is finished. */ public void closeEntityManager() { EntityManagerHolder emHolder = (EntityManagerHolder) TransactionSynchronizationManager .getResource(getEntityManagerFactory()); if (emHolder == null) { return; } logger.debug("Closing JPA EntityManager in EntityManagerUtil"); EntityManagerFactoryUtils.closeEntityManager(emHolder.getEntityManager()); } } ================================================ FILE: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/utils/ReleaseKeyGenerator.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.utils; import com.ctrip.framework.apollo.biz.entity.Namespace; import com.ctrip.framework.apollo.common.utils.UniqueKeyGenerator; /** * @author Jason Song(song_s@ctrip.com) */ public class ReleaseKeyGenerator extends UniqueKeyGenerator { /** * Generate the release key in the format: timestamp+appId+cluster+namespace+hash(ipAsInt+counter) * * @param namespace the namespace of the release * @return the unique release key */ public static String generateReleaseKey(Namespace namespace) { return generate(namespace.getAppId(), namespace.getClusterName(), namespace.getNamespaceName()); } } ================================================ FILE: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/utils/ReleaseMessageKeyGenerator.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.utils; import com.google.common.base.Joiner; import com.ctrip.framework.apollo.core.ConfigConsts; import com.google.common.base.Splitter; import java.util.Collections; import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class ReleaseMessageKeyGenerator { private static final Logger logger = LoggerFactory.getLogger(ReleaseMessageKeyGenerator.class); private static final Joiner STRING_JOINER = Joiner.on(ConfigConsts.CLUSTER_NAMESPACE_SEPARATOR); private static final Splitter STRING_SPLITTER = Splitter.on(ConfigConsts.CLUSTER_NAMESPACE_SEPARATOR).omitEmptyStrings(); public static String generate(String appId, String cluster, String namespace) { return STRING_JOINER.join(appId, cluster, namespace); } public static List messageToList(String message) { List keys = STRING_SPLITTER.splitToList(message); // message should be appId+cluster+namespace if (keys.size() != 3) { logger.error("message format invalid - {}", message); return Collections.emptyList(); } return keys; } } ================================================ FILE: apollo-biz/src/test/java/com/ctrip/framework/apollo/biz/AbstractIntegrationTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz; import org.junit.runner.RunWith; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.test.annotation.Rollback; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.transaction.annotation.Transactional; @RunWith(SpringJUnit4ClassRunner.class) @Rollback @Transactional @SpringBootTest(classes = BizTestConfiguration.class, webEnvironment = WebEnvironment.RANDOM_PORT) public abstract class AbstractIntegrationTest { protected static final String APP_ID = "kl-app"; protected static final String CLUSTER_NAME = "default"; protected static final String NAMESPACE_NAME = "application"; protected static final String BRANCH_NAME = "default"; } ================================================ FILE: apollo-biz/src/test/java/com/ctrip/framework/apollo/biz/AbstractUnitTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz; import org.junit.runner.RunWith; import org.mockito.junit.MockitoJUnitRunner; @RunWith(MockitoJUnitRunner.class) public abstract class AbstractUnitTest { } ================================================ FILE: apollo-biz/src/test/java/com/ctrip/framework/apollo/biz/BizTestConfiguration.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz; import com.ctrip.framework.apollo.common.ApolloCommonConfig; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; @Configuration @EnableAutoConfiguration @ComponentScan(basePackageClasses = {ApolloCommonConfig.class, ApolloBizConfig.class}) public class BizTestConfiguration { } ================================================ FILE: apollo-biz/src/test/java/com/ctrip/framework/apollo/biz/MockBeanFactory.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz; import com.ctrip.framework.apollo.biz.entity.Item; import com.ctrip.framework.apollo.biz.entity.Namespace; import com.ctrip.framework.apollo.biz.entity.Release; import com.ctrip.framework.apollo.biz.entity.ServerConfig; import com.ctrip.framework.apollo.common.entity.AppNamespace; public class MockBeanFactory { public static Namespace mockNamespace(String appId, String clusterName, String namespaceName) { Namespace instance = new Namespace(); instance.setAppId(appId); instance.setClusterName(clusterName); instance.setNamespaceName(namespaceName); return instance; } public static AppNamespace mockAppNamespace(String appId, String name, boolean isPublic) { AppNamespace instance = new AppNamespace(); instance.setAppId(appId); instance.setName(name); instance.setPublic(isPublic); return instance; } public static ServerConfig mockServerConfig(String key, String value, String cluster) { ServerConfig instance = new ServerConfig(); instance.setKey(key); instance.setValue(value); instance.setCluster(cluster); return instance; } public static Release mockRelease(long releaseId, String releaseKey, String appId, String clusterName, String groupName, String configurations) { Release instance = new Release(); instance.setId(releaseId); instance.setReleaseKey(releaseKey); instance.setAppId(appId); instance.setClusterName(clusterName); instance.setNamespaceName(groupName); instance.setConfigurations(configurations); return instance; } public static Item mockItem(long id, long namespaceId, String itemKey, String itemValue, int lineNum) { Item item = new Item(); item.setId(id); item.setKey(itemKey); item.setValue(itemValue); item.setLineNum(lineNum); item.setNamespaceId(namespaceId); return item; } } ================================================ FILE: apollo-biz/src/test/java/com/ctrip/framework/apollo/biz/config/BizConfigTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.config; import com.ctrip.framework.apollo.biz.repository.ServerConfigRepository; import com.ctrip.framework.apollo.biz.service.BizDBPropertySource; import java.util.Map; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.test.util.ReflectionTestUtils; import javax.sql.DataSource; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.when; /** * @author Jason Song(song_s@ctrip.com) */ @RunWith(MockitoJUnitRunner.class) public class BizConfigTest { @Mock private ConfigurableEnvironment environment; @Mock private ServerConfigRepository serverConfigRepository; @Mock private DataSource dataSource; private BizConfig bizConfig; @Before public void setUp() throws Exception { bizConfig = new BizConfig(new BizDBPropertySource(serverConfigRepository, dataSource, environment)); ReflectionTestUtils.setField(bizConfig, "environment", environment); } @Test public void testReleaseMessageNotificationBatch() throws Exception { int someBatch = 20; when(environment.getProperty("apollo.release-message.notification.batch")) .thenReturn(String.valueOf(someBatch)); assertEquals(someBatch, bizConfig.releaseMessageNotificationBatch()); } @Test public void testReleaseMessageNotificationBatchWithDefaultValue() throws Exception { int defaultBatch = 100; assertEquals(defaultBatch, bizConfig.releaseMessageNotificationBatch()); } @Test public void testReleaseMessageNotificationBatchWithInvalidNumber() throws Exception { int someBatch = -20; int defaultBatch = 100; when(environment.getProperty("apollo.release-message.notification.batch")) .thenReturn(String.valueOf(someBatch)); assertEquals(defaultBatch, bizConfig.releaseMessageNotificationBatch()); } @Test public void testReleaseHistoryRetentionSize() { int someLimit = 20; when(environment.getProperty("apollo.release-history.retention.size")) .thenReturn(String.valueOf(someLimit)); assertEquals(someLimit, bizConfig.releaseHistoryRetentionSize()); } @Test public void testReleaseHistoryRetentionSizeOverride() { int someOverrideLimit = 10; String overrideValueString = "{'a+b+c+b':10}"; when(environment.getProperty("apollo.release-history.retention.size.override")) .thenReturn(overrideValueString); int overrideValue = bizConfig.releaseHistoryRetentionSizeOverride().get("a+b+c+b"); assertEquals(someOverrideLimit, overrideValue); overrideValueString = "{'a+b+c+b':0,'a+b+d+b':2}"; when(environment.getProperty("apollo.release-history.retention.size.override")) .thenReturn(overrideValueString); assertEquals(1, bizConfig.releaseHistoryRetentionSizeOverride().size()); overrideValue = bizConfig.releaseHistoryRetentionSizeOverride().get("a+b+d+b"); assertEquals(2, overrideValue); overrideValueString = "{}"; when(environment.getProperty("apollo.release-history.retention.size.override")) .thenReturn(overrideValueString); assertEquals(0, bizConfig.releaseHistoryRetentionSizeOverride().size()); } @Test public void testAppIdValueLengthLimitOverride() { when(environment.getProperty("appid.value.length.limit.override")).thenReturn(null); Map result = bizConfig.appIdValueLengthLimitOverride(); assertTrue(result.isEmpty()); String input = "{}"; when(environment.getProperty("appid.value.length.limit.override")).thenReturn(input); result = bizConfig.appIdValueLengthLimitOverride(); assertTrue(result.isEmpty()); input = "invalid json"; when(environment.getProperty("appid.value.length.limit.override")).thenReturn(input); result = bizConfig.appIdValueLengthLimitOverride(); assertTrue(result.isEmpty()); input = "{'appid1':555}"; when(environment.getProperty("appid.value.length.limit.override")).thenReturn(input); int overrideValue = bizConfig.appIdValueLengthLimitOverride().get("appid1"); assertEquals(1, bizConfig.appIdValueLengthLimitOverride().size()); assertEquals(555, overrideValue); input = "{'appid1':555,'appid2':666}"; when(environment.getProperty("appid.value.length.limit.override")).thenReturn(input); overrideValue = bizConfig.appIdValueLengthLimitOverride().get("appid2"); assertEquals(2, bizConfig.appIdValueLengthLimitOverride().size()); assertEquals(666, overrideValue); input = "{'appid1':555,'appid2':666,'appid3':0,'appid4':-1}"; when(environment.getProperty("appid.value.length.limit.override")).thenReturn(input); result = bizConfig.appIdValueLengthLimitOverride(); assertTrue(result.containsKey("appid1")); assertTrue(result.containsKey("appid2")); assertFalse(result.containsKey("appid3")); assertFalse(result.containsKey("appid4")); assertEquals(2, result.size()); overrideValue = result.get("appid2"); assertEquals(666, overrideValue); } @Test public void testReleaseMessageNotificationBatchWithNAN() throws Exception { String someNAN = "someNAN"; int defaultBatch = 100; when(environment.getProperty("apollo.release-message.notification.batch")).thenReturn(someNAN); assertEquals(defaultBatch, bizConfig.releaseMessageNotificationBatch()); } @Test public void testCheckInt() throws Exception { int someInvalidValue = 1; int anotherInvalidValue = 2; int someValidValue = 3; int someDefaultValue = 10; int someMin = someInvalidValue + 1; int someMax = anotherInvalidValue - 1; assertEquals(someDefaultValue, bizConfig.checkInt(someInvalidValue, someMin, Integer.MAX_VALUE, someDefaultValue)); assertEquals(someDefaultValue, bizConfig.checkInt(anotherInvalidValue, Integer.MIN_VALUE, someMax, someDefaultValue)); assertEquals(someValidValue, bizConfig.checkInt(someValidValue, Integer.MIN_VALUE, Integer.MAX_VALUE, someDefaultValue)); } @Test public void testIsConfigServiceCacheKeyIgnoreCase() { assertFalse(bizConfig.isConfigServiceCacheKeyIgnoreCase()); when(environment.getProperty("config-service.cache.key.ignore-case")).thenReturn("true"); assertTrue(bizConfig.isConfigServiceCacheKeyIgnoreCase()); } } ================================================ FILE: apollo-biz/src/test/java/com/ctrip/framework/apollo/biz/entity/JpaMapFieldJsonConverterTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.entity; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; import org.junit.jupiter.api.Test; import org.springframework.core.io.ClassPathResource; class JpaMapFieldJsonConverterTest { private final JpaMapFieldJsonConverter converter = new JpaMapFieldJsonConverter(); static String readAllContentOf(String path) throws IOException { ClassPathResource classPathResource = new ClassPathResource(path); byte[] bytes = Files.readAllBytes(classPathResource.getFile().toPath()); return new String(bytes, StandardCharsets.UTF_8); } @Test void convertToDatabaseColumn_null() { assertEquals("null", this.converter.convertToDatabaseColumn(null)); } @Test void convertToDatabaseColumn_empty() { assertEquals("{}", this.converter.convertToDatabaseColumn(new HashMap<>(4))); } @Test void convertToDatabaseColumn_oneElement() throws IOException { Map map = new HashMap<>(8); map.put("a", "1"); String expected = readAllContentOf("json/converter/element.1.json"); assertEquals(expected, this.converter.convertToDatabaseColumn(map)); } @Test void convertToDatabaseColumn_twoElement() throws IOException { Map map = new LinkedHashMap<>(8); map.put("a", "1"); map.put("disableCheck", "true"); String expected = readAllContentOf("json/converter/element.2.json"); assertEquals(expected, this.converter.convertToDatabaseColumn(map)); } @Test void convertToEntityAttribute_null() { assertNull(this.converter.convertToEntityAttribute(null)); assertNull(this.converter.convertToEntityAttribute("null")); } @Test void convertToEntityAttribute_null_oneElement() throws IOException { Map map = new HashMap<>(8); map.put("a", "1"); String content = readAllContentOf("json/converter/element.1.json"); assertEquals(map, this.converter.convertToEntityAttribute(content)); } @Test void convertToEntityAttribute_null_twoElement() throws IOException { Map map = new HashMap<>(8); map.put("a", "1"); map.put("disableCheck", "true"); String content = readAllContentOf("json/converter/element.2.json"); assertEquals(map, this.converter.convertToEntityAttribute(content)); } } ================================================ FILE: apollo-biz/src/test/java/com/ctrip/framework/apollo/biz/grayReleaseRule/GrayReleaseRulesHolderTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.grayReleaseRule; import com.google.common.base.Joiner; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import com.google.gson.Gson; import com.ctrip.framework.apollo.biz.config.BizConfig; import com.ctrip.framework.apollo.biz.entity.GrayReleaseRule; import com.ctrip.framework.apollo.biz.entity.ReleaseMessage; import com.ctrip.framework.apollo.biz.message.Topics; import com.ctrip.framework.apollo.biz.repository.GrayReleaseRuleRepository; import com.ctrip.framework.apollo.common.constants.NamespaceBranchStatus; import com.ctrip.framework.apollo.common.dto.GrayReleaseRuleItemDTO; import com.ctrip.framework.apollo.core.ConfigConsts; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import java.util.List; import java.util.Set; import java.util.concurrent.atomic.AtomicLong; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; /** * @author Jason Song(song_s@ctrip.com) */ @RunWith(MockitoJUnitRunner.class) public class GrayReleaseRulesHolderTest { private static final Joiner STRING_JOINER = Joiner.on(ConfigConsts.CLUSTER_NAMESPACE_SEPARATOR); private GrayReleaseRulesHolder grayReleaseRulesHolder; @Mock private BizConfig bizConfig; @Mock private GrayReleaseRuleRepository grayReleaseRuleRepository; private static final Gson GSON = new Gson(); private AtomicLong idCounter; @Before public void setUp() throws Exception { grayReleaseRulesHolder = spy(new GrayReleaseRulesHolder(grayReleaseRuleRepository, bizConfig)); idCounter = new AtomicLong(); } @Test public void testScanGrayReleaseRules() throws Exception { String someAppId = "someAppId"; String someClusterName = "someClusterName"; String someNamespaceName = "someNamespaceName"; String anotherNamespaceName = "anotherNamespaceName"; Long someReleaseId = 1L; int activeBranchStatus = NamespaceBranchStatus.ACTIVE; String someClientAppId = "clientAppId1"; String someClientIp = "1.1.1.1"; String someClientLabel = "myLabel"; String anotherClientAppId = "clientAppId2"; String anotherClientIp = "2.2.2.2"; String anotherClientLabel = "testLabel"; GrayReleaseRule someRule = assembleGrayReleaseRule(someAppId, someClusterName, someNamespaceName, Lists.newArrayList(assembleRuleItem(someClientAppId, Sets.newHashSet(someClientIp), Sets.newHashSet(someClientLabel))), someReleaseId, activeBranchStatus); when(bizConfig.grayReleaseRuleScanInterval()).thenReturn(30); when(grayReleaseRuleRepository.findFirst500ByIdGreaterThanOrderByIdAsc(0L)) .thenReturn(Lists.newArrayList(someRule)); // scan rules grayReleaseRulesHolder.afterPropertiesSet(); assertEquals(someReleaseId, grayReleaseRulesHolder.findReleaseIdFromGrayReleaseRule(someClientAppId, someClientIp, anotherClientLabel, someAppId, someClusterName, someNamespaceName)); assertEquals(someReleaseId, grayReleaseRulesHolder.findReleaseIdFromGrayReleaseRule(someClientAppId, anotherClientIp, someClientLabel, someAppId, someClusterName, someNamespaceName)); assertEquals(someReleaseId, grayReleaseRulesHolder.findReleaseIdFromGrayReleaseRule(someClientAppId.toUpperCase(), someClientIp, someClientLabel, someAppId.toUpperCase(), someClusterName, someNamespaceName.toUpperCase())); assertNull(grayReleaseRulesHolder.findReleaseIdFromGrayReleaseRule(someClientAppId, anotherClientIp, anotherClientLabel, someAppId, someClusterName, someNamespaceName)); assertNull(grayReleaseRulesHolder.findReleaseIdFromGrayReleaseRule(anotherClientAppId, someClientIp, someClientLabel, someAppId, someClusterName, someNamespaceName)); assertNull(grayReleaseRulesHolder.findReleaseIdFromGrayReleaseRule(anotherClientAppId, anotherClientIp, anotherClientLabel, someAppId, someClusterName, someNamespaceName)); assertTrue(grayReleaseRulesHolder.hasGrayReleaseRule(someClientAppId, someClientIp, someClientLabel, someNamespaceName)); assertTrue(grayReleaseRulesHolder.hasGrayReleaseRule(someClientAppId.toUpperCase(), someClientIp, someClientLabel, someNamespaceName.toUpperCase())); assertTrue(grayReleaseRulesHolder.hasGrayReleaseRule(someClientAppId, anotherClientIp, someClientLabel, someNamespaceName)); assertTrue(grayReleaseRulesHolder.hasGrayReleaseRule(someClientAppId.toUpperCase(), anotherClientIp, someClientLabel, someNamespaceName.toUpperCase())); assertFalse(grayReleaseRulesHolder.hasGrayReleaseRule(someClientAppId, anotherClientIp, anotherClientLabel, someNamespaceName)); assertFalse(grayReleaseRulesHolder.hasGrayReleaseRule(someClientAppId, someClientIp, someClientLabel, anotherNamespaceName)); assertFalse(grayReleaseRulesHolder.hasGrayReleaseRule(anotherClientAppId, anotherClientIp, anotherClientLabel, someNamespaceName)); assertFalse(grayReleaseRulesHolder.hasGrayReleaseRule(anotherClientAppId, anotherClientIp, anotherClientLabel, anotherNamespaceName)); GrayReleaseRule anotherRule = assembleGrayReleaseRule(someAppId, someClusterName, someNamespaceName, Lists.newArrayList(assembleRuleItem(anotherClientAppId, Sets.newHashSet(anotherClientIp), Sets.newHashSet(anotherClientLabel))), someReleaseId, activeBranchStatus); when(grayReleaseRuleRepository.findByAppIdAndClusterNameAndNamespaceName(someAppId, someClusterName, someNamespaceName)).thenReturn(Lists.newArrayList(anotherRule)); // send message grayReleaseRulesHolder.handleMessage( assembleReleaseMessage(someAppId, someClusterName, someNamespaceName), Topics.APOLLO_RELEASE_TOPIC); assertNull(grayReleaseRulesHolder.findReleaseIdFromGrayReleaseRule(someClientAppId, someClientIp, someClientLabel, someAppId, someClusterName, someNamespaceName)); assertEquals(someReleaseId, grayReleaseRulesHolder.findReleaseIdFromGrayReleaseRule(anotherClientAppId, anotherClientIp, someClientLabel, someAppId, someClusterName, someNamespaceName)); assertEquals(someReleaseId, grayReleaseRulesHolder.findReleaseIdFromGrayReleaseRule(anotherClientAppId, someClientIp, anotherClientLabel, someAppId, someClusterName, someNamespaceName)); assertEquals(someReleaseId, grayReleaseRulesHolder.findReleaseIdFromGrayReleaseRule(anotherClientAppId, anotherClientIp, anotherClientLabel, someAppId, someClusterName, someNamespaceName)); assertFalse(grayReleaseRulesHolder.hasGrayReleaseRule(someClientAppId, someClientIp, someClientLabel, someNamespaceName)); assertFalse(grayReleaseRulesHolder.hasGrayReleaseRule(someClientAppId, someClientIp, someClientLabel, anotherNamespaceName)); assertTrue(grayReleaseRulesHolder.hasGrayReleaseRule(anotherClientAppId, anotherClientIp, anotherClientLabel, someNamespaceName)); assertTrue(grayReleaseRulesHolder.hasGrayReleaseRule(anotherClientAppId, anotherClientIp, someClientLabel, someNamespaceName)); assertTrue(grayReleaseRulesHolder.hasGrayReleaseRule(anotherClientAppId, someClientIp, anotherClientLabel, someNamespaceName)); assertFalse(grayReleaseRulesHolder.hasGrayReleaseRule(anotherClientAppId, someClientIp, someClientLabel, someNamespaceName)); assertFalse(grayReleaseRulesHolder.hasGrayReleaseRule(anotherClientAppId, anotherClientIp, anotherClientLabel, anotherNamespaceName)); } private GrayReleaseRule assembleGrayReleaseRule(String appId, String clusterName, String namespaceName, List ruleItems, long releaseId, int branchStatus) { GrayReleaseRule rule = new GrayReleaseRule(); rule.setId(idCounter.incrementAndGet()); rule.setAppId(appId); rule.setClusterName(clusterName); rule.setNamespaceName(namespaceName); rule.setBranchName("someBranch"); rule.setRules(GSON.toJson(ruleItems)); rule.setReleaseId(releaseId); rule.setBranchStatus(branchStatus); return rule; } private GrayReleaseRuleItemDTO assembleRuleItem(String clientAppId, Set clientIpList, Set clientLabelList) { return new GrayReleaseRuleItemDTO(clientAppId, clientIpList, clientLabelList); } private ReleaseMessage assembleReleaseMessage(String appId, String clusterName, String namespaceName) { String message = STRING_JOINER.join(appId, clusterName, namespaceName); return new ReleaseMessage(message); } } ================================================ FILE: apollo-biz/src/test/java/com/ctrip/framework/apollo/biz/message/DatabaseMessageSenderTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.message; import com.ctrip.framework.apollo.biz.AbstractUnitTest; import com.ctrip.framework.apollo.biz.entity.ReleaseMessage; import com.ctrip.framework.apollo.biz.repository.ReleaseMessageRepository; import org.junit.Before; import org.junit.Test; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.*; /** * @author Jason Song(song_s@ctrip.com) */ public class DatabaseMessageSenderTest extends AbstractUnitTest { private DatabaseMessageSender messageSender; @Mock private ReleaseMessageRepository releaseMessageRepository; @Before public void setUp() throws Exception { messageSender = new DatabaseMessageSender(releaseMessageRepository); } @Test public void testSendMessage() throws Exception { String someMessage = "some-message"; long someId = 1; ReleaseMessage someReleaseMessage = mock(ReleaseMessage.class); when(someReleaseMessage.getId()).thenReturn(someId); when(releaseMessageRepository.save(any(ReleaseMessage.class))).thenReturn(someReleaseMessage); ArgumentCaptor captor = ArgumentCaptor.forClass(ReleaseMessage.class); messageSender.sendMessage(someMessage, Topics.APOLLO_RELEASE_TOPIC); verify(releaseMessageRepository, times(1)).save(captor.capture()); assertEquals(someMessage, captor.getValue().getMessage()); } @Test public void testSendUnsupportedMessage() throws Exception { String someMessage = "some-message"; String someUnsupportedTopic = "some-invalid-topic"; messageSender.sendMessage(someMessage, someUnsupportedTopic); verify(releaseMessageRepository, never()).save(any(ReleaseMessage.class)); } @Test(expected = RuntimeException.class) public void testSendMessageFailed() throws Exception { String someMessage = "some-message"; when(releaseMessageRepository.save(any(ReleaseMessage.class))) .thenThrow(new RuntimeException()); messageSender.sendMessage(someMessage, Topics.APOLLO_RELEASE_TOPIC); } } ================================================ FILE: apollo-biz/src/test/java/com/ctrip/framework/apollo/biz/message/ReleaseMessageScannerTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.message; import com.ctrip.framework.apollo.biz.config.BizConfig; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import com.google.common.util.concurrent.SettableFuture; import com.ctrip.framework.apollo.biz.AbstractUnitTest; import com.ctrip.framework.apollo.biz.entity.ReleaseMessage; import com.ctrip.framework.apollo.biz.repository.ReleaseMessageRepository; import java.util.ArrayList; import org.awaitility.Awaitility; import org.junit.Before; import org.junit.Test; import org.mockito.Mock; import java.util.concurrent.TimeUnit; import static org.awaitility.Awaitility.await; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertSame; import static org.mockito.Mockito.when; /** * @author Jason Song(song_s@ctrip.com) */ public class ReleaseMessageScannerTest extends AbstractUnitTest { private ReleaseMessageScanner releaseMessageScanner; @Mock private ReleaseMessageRepository releaseMessageRepository; @Mock private BizConfig bizConfig; private int databaseScanInterval; @Before public void setUp() throws Exception { releaseMessageScanner = new ReleaseMessageScanner(bizConfig, releaseMessageRepository); databaseScanInterval = 100; // 100 ms when(bizConfig.releaseMessageScanIntervalInMilli()).thenReturn(databaseScanInterval); releaseMessageScanner.afterPropertiesSet(); Awaitility.reset(); Awaitility.setDefaultTimeout(databaseScanInterval * 5, TimeUnit.MILLISECONDS); Awaitility.setDefaultPollInterval(databaseScanInterval, TimeUnit.MILLISECONDS); } @Test public void testScanMessageAndNotifyMessageListener() throws Exception { SettableFuture someListenerFuture = SettableFuture.create(); ReleaseMessageListener someListener = (message, channel) -> someListenerFuture.set(message); releaseMessageScanner.addMessageListener(someListener); String someMessage = "someMessage"; long someId = 100; ReleaseMessage someReleaseMessage = assembleReleaseMessage(someId, someMessage); when(releaseMessageRepository.findFirst500ByIdGreaterThanOrderByIdAsc(0L)) .thenReturn(Lists.newArrayList(someReleaseMessage)); ReleaseMessage someListenerMessage = someListenerFuture.get(5000, TimeUnit.MILLISECONDS); assertEquals(someMessage, someListenerMessage.getMessage()); assertEquals(someId, someListenerMessage.getId()); SettableFuture anotherListenerFuture = SettableFuture.create(); ReleaseMessageListener anotherListener = (message, channel) -> anotherListenerFuture.set(message); releaseMessageScanner.addMessageListener(anotherListener); String anotherMessage = "anotherMessage"; long anotherId = someId + 1; ReleaseMessage anotherReleaseMessage = assembleReleaseMessage(anotherId, anotherMessage); when(releaseMessageRepository.findFirst500ByIdGreaterThanOrderByIdAsc(someId)) .thenReturn(Lists.newArrayList(anotherReleaseMessage)); ReleaseMessage anotherListenerMessage = anotherListenerFuture.get(5000, TimeUnit.MILLISECONDS); assertEquals(anotherMessage, anotherListenerMessage.getMessage()); assertEquals(anotherId, anotherListenerMessage.getId()); } @Test public void testScanMessageWithGapAndNotifyMessageListener() throws Exception { String someMessage = "someMessage"; long someId = 1; ReleaseMessage someReleaseMessage = assembleReleaseMessage(someId, someMessage); String someMissingMessage = "someMissingMessage"; long someMissingId = 2; ReleaseMessage someMissingReleaseMessage = assembleReleaseMessage(someMissingId, someMissingMessage); String anotherMessage = "anotherMessage"; long anotherId = 3; ReleaseMessage anotherReleaseMessage = assembleReleaseMessage(anotherId, anotherMessage); String anotherMissingMessage = "anotherMissingMessage"; long anotherMissingId = 4; ReleaseMessage anotherMissingReleaseMessage = assembleReleaseMessage(anotherMissingId, anotherMissingMessage); long someRolledBackId = 5; String yetAnotherMessage = "yetAnotherMessage"; long yetAnotherId = 6; ReleaseMessage yetAnotherReleaseMessage = assembleReleaseMessage(yetAnotherId, yetAnotherMessage); ArrayList receivedMessage = Lists.newArrayList(); SettableFuture someListenerFuture = SettableFuture.create(); ReleaseMessageListener someListener = (message, channel) -> receivedMessage.add(message); releaseMessageScanner.addMessageListener(someListener); when(releaseMessageRepository.findFirst500ByIdGreaterThanOrderByIdAsc(0L)) .thenReturn(Lists.newArrayList(someReleaseMessage)); await().untilAsserted(() -> { assertEquals(1, receivedMessage.size()); assertSame(someReleaseMessage, receivedMessage.get(0)); }); when(releaseMessageRepository.findFirst500ByIdGreaterThanOrderByIdAsc(someId)) .thenReturn(Lists.newArrayList(anotherReleaseMessage)); await().untilAsserted(() -> { assertEquals(2, receivedMessage.size()); assertSame(someReleaseMessage, receivedMessage.get(0)); assertSame(anotherReleaseMessage, receivedMessage.get(1)); }); when(releaseMessageRepository.findAllById(Sets.newHashSet(someMissingId))) .thenReturn(Lists.newArrayList(someMissingReleaseMessage)); await().untilAsserted(() -> { assertEquals(3, receivedMessage.size()); assertSame(someReleaseMessage, receivedMessage.get(0)); assertSame(anotherReleaseMessage, receivedMessage.get(1)); assertSame(someMissingReleaseMessage, receivedMessage.get(2)); }); when(releaseMessageRepository.findFirst500ByIdGreaterThanOrderByIdAsc(anotherId)) .thenReturn(Lists.newArrayList(yetAnotherReleaseMessage)); await().untilAsserted(() -> { assertEquals(4, receivedMessage.size()); assertSame(someReleaseMessage, receivedMessage.get(0)); assertSame(anotherReleaseMessage, receivedMessage.get(1)); assertSame(someMissingReleaseMessage, receivedMessage.get(2)); assertSame(yetAnotherReleaseMessage, receivedMessage.get(3)); }); when(releaseMessageRepository.findAllById(Sets.newHashSet(anotherMissingId, someRolledBackId))) .thenReturn(Lists.newArrayList(anotherMissingReleaseMessage)); await().untilAsserted(() -> { assertEquals(5, receivedMessage.size()); assertSame(someReleaseMessage, receivedMessage.get(0)); assertSame(anotherReleaseMessage, receivedMessage.get(1)); assertSame(someMissingReleaseMessage, receivedMessage.get(2)); assertSame(yetAnotherReleaseMessage, receivedMessage.get(3)); assertSame(anotherMissingReleaseMessage, receivedMessage.get(4)); }); } private ReleaseMessage assembleReleaseMessage(long id, String message) { ReleaseMessage releaseMessage = new ReleaseMessage(); releaseMessage.setId(id); releaseMessage.setMessage(message); return releaseMessage; } } ================================================ FILE: apollo-biz/src/test/java/com/ctrip/framework/apollo/biz/registry/DatabaseDiscoveryClientAlwaysAddSelfInstanceDecoratorImplTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.registry; import static com.ctrip.framework.apollo.biz.registry.ServiceInstanceFactory.newServiceInstance; import static org.junit.jupiter.api.Assertions.*; import java.util.Arrays; import java.util.Collections; import java.util.List; import org.junit.jupiter.api.Test; import org.mockito.Mockito; class DatabaseDiscoveryClientAlwaysAddSelfInstanceDecoratorImplTest { @Test void getInstances_other_service_name() { final String otherServiceName = "other-service"; DatabaseDiscoveryClient client = Mockito.mock(DatabaseDiscoveryClient.class); Mockito.when(client.getInstances(otherServiceName)).thenReturn(Collections.singletonList( newServiceInstance(otherServiceName, "http://10.240.34.56:8081/", "beijing"))); final String selfServiceName = "self-service"; ServiceInstance selfInstance = newServiceInstance(selfServiceName, "http://10.240.34.56:8081/", "beijing"); DatabaseDiscoveryClient decorator = new DatabaseDiscoveryClientAlwaysAddSelfInstanceDecoratorImpl(client, selfInstance); List serviceInstances = decorator.getInstances(otherServiceName); assertEquals(1, serviceInstances.size()); ServiceInstance otherServiceNameInstance = serviceInstances.get(0); assertEquals(otherServiceName, otherServiceNameInstance.getServiceName()); Mockito.verify(client, Mockito.times(1)).getInstances(Mockito.eq(otherServiceName)); Mockito.verify(client, Mockito.never()).getInstances(Mockito.eq(selfServiceName)); } @Test void getInstances_contain_self() { final String otherServiceName = "other-service"; DatabaseDiscoveryClient client = Mockito.mock(DatabaseDiscoveryClient.class); Mockito.when(client.getInstances(otherServiceName)).thenReturn(Collections.singletonList( newServiceInstance(otherServiceName, "http://10.240.34.56:8081/", "beijing"))); final String selfServiceName = "self-service"; ServiceInstance selfInstance = newServiceInstance(selfServiceName, "http://10.240.34.56:8081/", "beijing"); Mockito.when(client.getInstances(selfServiceName)).thenReturn(Arrays.asList(selfInstance, // same service name but different service instance newServiceInstance(selfServiceName, "http://10.240.34.56:8082/", "beijing"), newServiceInstance(selfServiceName, "http://10.240.34.56:8083/", "beijing"))); DatabaseDiscoveryClient decorator = new DatabaseDiscoveryClientAlwaysAddSelfInstanceDecoratorImpl(client, selfInstance); List serviceInstances = decorator.getInstances(selfServiceName); assertEquals(3, serviceInstances.size()); Mockito.verify(client, Mockito.times(1)).getInstances(Mockito.eq(selfServiceName)); Mockito.verify(client, Mockito.never()).getInstances(Mockito.eq(otherServiceName)); } /** * will add self */ @Test void getInstances_same_service_name_without_self() { final String otherServiceName = "other-service"; DatabaseDiscoveryClient client = Mockito.mock(DatabaseDiscoveryClient.class); Mockito.when(client.getInstances(otherServiceName)).thenReturn(Collections.singletonList( newServiceInstance(otherServiceName, "http://10.240.34.56:8081/", "beijing"))); final String selfServiceName = "self-service"; ServiceInstance selfInstance = newServiceInstance(selfServiceName, "http://10.240.34.56:8081/", "beijing"); Mockito.when(client.getInstances(selfServiceName)).thenReturn(Arrays.asList( // same service name but different service instance newServiceInstance(selfServiceName, "http://10.240.34.56:8082/", "beijing"), newServiceInstance(selfServiceName, "http://10.240.34.56:8083/", "beijing"))); DatabaseDiscoveryClient decorator = new DatabaseDiscoveryClientAlwaysAddSelfInstanceDecoratorImpl(client, selfInstance); List serviceInstances = decorator.getInstances(selfServiceName); // because mocked data don't contain self instance // after add self instance, there are 3 instances now assertEquals(3, serviceInstances.size()); Mockito.verify(client, Mockito.times(1)).getInstances(Mockito.eq(selfServiceName)); Mockito.verify(client, Mockito.never()).getInstances(Mockito.eq(otherServiceName)); } } ================================================ FILE: apollo-biz/src/test/java/com/ctrip/framework/apollo/biz/registry/DatabaseDiscoveryClientImplTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.registry; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import com.ctrip.framework.apollo.biz.entity.ServiceRegistry; import com.ctrip.framework.apollo.biz.registry.configuration.support.ApolloServiceDiscoveryProperties; import com.ctrip.framework.apollo.biz.service.ServiceRegistryService; import java.time.LocalDateTime; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import org.junit.jupiter.api.Test; import org.mockito.Mockito; class DatabaseDiscoveryClientImplTest { private static ServiceRegistry newServiceRegistry(String serviceName, String uri, String cluster, LocalDateTime dataChangeLastModifiedTime) { ServiceRegistry serviceRegistry = new ServiceRegistry(); serviceRegistry.setServiceName(serviceName); serviceRegistry.setUri(uri); serviceRegistry.setCluster(cluster); serviceRegistry.setMetadata(new HashMap<>()); serviceRegistry.setDataChangeCreatedTime(LocalDateTime.now()); serviceRegistry.setDataChangeLastModifiedTime(dataChangeLastModifiedTime); return serviceRegistry; } private static ServiceRegistry newServiceRegistry(String serviceName, String uri, String cluster) { return newServiceRegistry(serviceName, uri, cluster, LocalDateTime.now()); } @Test void getInstances_filterByCluster() { final String serviceName = "a-service"; ServiceRegistryService serviceRegistryService = Mockito.mock(ServiceRegistryService.class); { List serviceRegistryList = Arrays.asList(newServiceRegistry(serviceName, "http://localhost:8081/", "1"), newServiceRegistry("b-service", "http://localhost:8082/", "2"), newServiceRegistry("c-service", "http://localhost:8082/", "3")); Mockito.when(serviceRegistryService.findByServiceNameDataChangeLastModifiedTimeGreaterThan( eq(serviceName), any(LocalDateTime.class))).thenReturn(serviceRegistryList); } DatabaseDiscoveryClient discoveryClient = new DatabaseDiscoveryClientImpl( serviceRegistryService, new ApolloServiceDiscoveryProperties(), "1"); List serviceInstances = discoveryClient.getInstances(serviceName); assertEquals(1, serviceInstances.size()); assertEquals(serviceName, serviceInstances.get(0).getServiceName()); assertEquals("1", serviceInstances.get(0).getCluster()); } @Test void getInstances_filterByHealthCheck() { final String serviceName = "a-service"; ServiceRegistryService serviceRegistryService = Mockito.mock(ServiceRegistryService.class); ServiceRegistry healthy = newServiceRegistry(serviceName, "http://localhost:8081/", "1", LocalDateTime.now()); Mockito .when(serviceRegistryService.findByServiceNameDataChangeLastModifiedTimeGreaterThan( eq(serviceName), any(LocalDateTime.class))) .thenReturn(Collections.singletonList(healthy)); DatabaseDiscoveryClient discoveryClient = new DatabaseDiscoveryClientImpl( serviceRegistryService, new ApolloServiceDiscoveryProperties(), "1"); List serviceInstances = discoveryClient.getInstances(serviceName); assertEquals(1, serviceInstances.size()); assertEquals(serviceName, serviceInstances.get(0).getServiceName()); assertEquals("http://localhost:8081/", serviceInstances.get(0).getUri().toString()); assertEquals("1", serviceInstances.get(0).getCluster()); } } ================================================ FILE: apollo-biz/src/test/java/com/ctrip/framework/apollo/biz/registry/DatabaseDiscoveryClientMemoryCacheDecoratorImplTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.registry; import static com.ctrip.framework.apollo.biz.registry.ServiceInstanceFactory.newServiceInstance; import static org.junit.jupiter.api.Assertions.*; import java.util.Arrays; import java.util.Collections; import java.util.List; import org.junit.jupiter.api.Test; import org.mockito.Mockito; class DatabaseDiscoveryClientMemoryCacheDecoratorImplTest { @Test void init() { DatabaseDiscoveryClient client = Mockito.mock(DatabaseDiscoveryClient.class); DatabaseDiscoveryClientMemoryCacheDecoratorImpl decorator = new DatabaseDiscoveryClientMemoryCacheDecoratorImpl(client); decorator.init(); } @Test void updateCacheTask_empty() { DatabaseDiscoveryClient client = Mockito.mock(DatabaseDiscoveryClient.class); DatabaseDiscoveryClientMemoryCacheDecoratorImpl decorator = new DatabaseDiscoveryClientMemoryCacheDecoratorImpl(client); decorator.updateCacheTask(); Mockito.verify(client, Mockito.never()).getInstances(Mockito.any()); } @Test void updateCacheTask_exception() { final String serviceName = "a-service"; DatabaseDiscoveryClient client = Mockito.mock(DatabaseDiscoveryClient.class); Mockito.when(client.getInstances(serviceName)).thenReturn( Arrays.asList(newServiceInstance(serviceName, "http://10.240.34.56:8080/", "beijing"), newServiceInstance(serviceName, "http://10.240.34.56:8081/", "beijing"), newServiceInstance(serviceName, "http://10.240.34.56:8082/", "beijing"))); DatabaseDiscoveryClientMemoryCacheDecoratorImpl decorator = new DatabaseDiscoveryClientMemoryCacheDecoratorImpl(client); List list = decorator.getInstances(serviceName); assertEquals(3, list.size()); // if database error Mockito.when(client.getInstances(serviceName)).thenThrow(OutOfMemoryError.class); assertThrows(OutOfMemoryError.class, () -> decorator.readFromDatabase(serviceName)); // task won't be interrupted by Throwable decorator.updateCacheTask(); Mockito.verify(client, Mockito.times(3)).getInstances(serviceName); } @Test void getInstances_from_cache() { DatabaseDiscoveryClient client = Mockito.mock(DatabaseDiscoveryClient.class); Mockito.when(client.getInstances("a-service")).thenReturn( Arrays.asList(newServiceInstance("a-service", "http://10.240.34.56:8080/", "beijing"), newServiceInstance("a-service", "http://10.240.34.56:8081/", "beijing"))); Mockito.when(client.getInstances("b-service")).thenReturn( Arrays.asList(newServiceInstance("b-service", "http://10.240.56.78:8080/", "shanghai"), newServiceInstance("b-service", "http://10.240.56.78:8081/", "shanghai"), newServiceInstance("b-service", "http://10.240.56.78:8082/", "shanghai"))); DatabaseDiscoveryClientMemoryCacheDecoratorImpl decorator = new DatabaseDiscoveryClientMemoryCacheDecoratorImpl(client); assertEquals(2, decorator.getInstances("a-service").size()); assertEquals(2, decorator.getInstances("a-service").size()); assertEquals(3, decorator.getInstances("b-service").size()); assertEquals(3, decorator.getInstances("b-service").size()); // only invoke 1 times because always read from cache Mockito.verify(client, Mockito.times(1)).getInstances("a-service"); Mockito.verify(client, Mockito.times(1)).getInstances("b-service"); } @Test void getInstances_from_cache_when_database_updated() { DatabaseDiscoveryClient client = Mockito.mock(DatabaseDiscoveryClient.class); Mockito.when(client.getInstances("a-service")).thenReturn( Arrays.asList(newServiceInstance("a-service", "http://10.240.34.56:8080/", "beijing"), newServiceInstance("a-service", "http://10.240.34.56:8081/", "beijing"))); Mockito.when(client.getInstances("b-service")).thenReturn( Arrays.asList(newServiceInstance("b-service", "http://10.240.56.78:8080/", "shanghai"), newServiceInstance("b-service", "http://10.240.56.78:8081/", "shanghai"), newServiceInstance("b-service", "http://10.240.56.78:8082/", "shanghai"))); DatabaseDiscoveryClientMemoryCacheDecoratorImpl decorator = new DatabaseDiscoveryClientMemoryCacheDecoratorImpl(client); assertEquals(2, decorator.getInstances("a-service").size()); assertEquals(2, decorator.getInstances("a-service").size()); assertEquals(3, decorator.getInstances("b-service").size()); assertEquals(3, decorator.getInstances("b-service").size()); // only invoke 1 times because always read from cache Mockito.verify(client, Mockito.times(1)).getInstances("a-service"); Mockito.verify(client, Mockito.times(1)).getInstances("b-service"); // instances in database are changed Mockito.when(client.getInstances("b-service")).thenReturn(Collections .singletonList(newServiceInstance("b-service", "http://10.240.56.78:8080/", "shanghai"))); // read again assertEquals(2, decorator.getInstances("a-service").size()); // cache doesn't update yet, so we still get 3 instances assertEquals(3, decorator.getInstances("b-service").size()); // only invoke 1 times because always read from cache Mockito.verify(client, Mockito.times(1)).getInstances("a-service"); Mockito.verify(client, Mockito.times(1)).getInstances("b-service"); decorator.updateCacheTask(); // read again assertEquals(2, decorator.getInstances("a-service").size()); // cache updated already, so we still get 1 instances assertEquals(1, decorator.getInstances("b-service").size()); // invoke 2 times because always read from database again by task Mockito.verify(client, Mockito.times(2)).getInstances("a-service"); Mockito.verify(client, Mockito.times(2)).getInstances("b-service"); } @Test void getInstances_from_cache_when_database_crash() { DatabaseDiscoveryClient client = Mockito.mock(DatabaseDiscoveryClient.class); Mockito.when(client.getInstances("a-service")).thenReturn( Arrays.asList(newServiceInstance("a-service", "http://10.240.34.56:8080/", "beijing"), newServiceInstance("a-service", "http://10.240.34.56:8081/", "beijing"))); Mockito.when(client.getInstances("b-service")).thenReturn( Arrays.asList(newServiceInstance("b-service", "http://10.240.56.78:8080/", "shanghai"), newServiceInstance("b-service", "http://10.240.56.78:8081/", "shanghai"), newServiceInstance("b-service", "http://10.240.56.78:8082/", "shanghai"))); DatabaseDiscoveryClientMemoryCacheDecoratorImpl decorator = new DatabaseDiscoveryClientMemoryCacheDecoratorImpl(client); assertEquals(2, decorator.getInstances("a-service").size()); assertEquals(2, decorator.getInstances("a-service").size()); assertEquals(3, decorator.getInstances("b-service").size()); assertEquals(3, decorator.getInstances("b-service").size()); // only invoke 1 times because always read from cache Mockito.verify(client, Mockito.times(1)).getInstances("a-service"); Mockito.verify(client, Mockito.times(1)).getInstances("b-service"); // database crash Mockito.when(client.getInstances(Mockito.any())).thenThrow(OutOfMemoryError.class); assertThrows(OutOfMemoryError.class, () -> decorator.readFromDatabase("a-service")); assertThrows(OutOfMemoryError.class, () -> decorator.readFromDatabase("b-service")); // read again assertEquals(2, decorator.getInstances("a-service").size()); assertEquals(3, decorator.getInstances("b-service").size()); Mockito.verify(client, Mockito.times(2)).getInstances("a-service"); Mockito.verify(client, Mockito.times(2)).getInstances("b-service"); } } ================================================ FILE: apollo-biz/src/test/java/com/ctrip/framework/apollo/biz/registry/DatabaseDiscoveryIntegrationTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.registry; import static com.ctrip.framework.apollo.biz.registry.ServiceInstanceFactory.newServiceInstance; import static org.junit.jupiter.api.Assertions.assertEquals; import com.ctrip.framework.apollo.biz.AbstractIntegrationTest; import com.ctrip.framework.apollo.biz.registry.configuration.ApolloServiceDiscoveryAutoConfiguration; import com.ctrip.framework.apollo.biz.registry.configuration.ApolloServiceRegistryAutoConfiguration; import java.util.List; import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.TestPropertySource; /** * test when {@link DatabaseDiscoveryClient} is warped by decorator. */ @TestPropertySource(properties = {"apollo.service.registry.enabled=true", "apollo.service.registry.cluster=default", "apollo.service.discovery.enabled=true", "spring.application.name=for-test-service", "server.port=10000",}) @ContextConfiguration(classes = {ApolloServiceRegistryAutoConfiguration.class, ApolloServiceDiscoveryAutoConfiguration.class,}) public class DatabaseDiscoveryIntegrationTest extends AbstractIntegrationTest { private final Logger log = LoggerFactory.getLogger(this.getClass()); @Autowired private DatabaseServiceRegistry serviceRegistry; @Autowired private DatabaseDiscoveryClient discoveryClient; /** * discover one after register, and delete it */ @Test public void registerThenDiscoveryThenDelete() { // register it String serviceName = "a-service"; String uri = "http://192.168.1.20:8080/"; String cluster = "default"; ServiceInstance instance = newServiceInstance(serviceName, uri, cluster); this.serviceRegistry.register(instance); // find it List serviceInstances = this.discoveryClient.getInstances(serviceName); assertEquals(1, serviceInstances.size()); ServiceInstance actual = serviceInstances.get(0); assertEquals(serviceName, actual.getServiceName()); assertEquals(uri, actual.getUri().toString()); assertEquals(cluster, actual.getCluster()); assertEquals(0, actual.getMetadata().size()); // delete it this.serviceRegistry.deregister(instance); // because it save in memory, so we can still find it assertEquals(1, this.discoveryClient.getInstances(serviceName).size()); } /** * diff cluster so cannot be discover */ @Test public void registerThenDiscoveryNone() { // register it String serviceName = "b-service"; ServiceInstance instance = newServiceInstance(serviceName, "http://192.168.1.20:8080/", "cannot-be-discovery"); this.serviceRegistry.register(instance); // find none List serviceInstances = this.discoveryClient.getInstances(serviceName); assertEquals(0, serviceInstances.size()); } @Test public void registerTwice() { String serviceName = "c-service"; ServiceInstance instance = newServiceInstance(serviceName, "http://192.168.1.20:8080/", "default"); // register it this.serviceRegistry.register(instance); // register again this.serviceRegistry.register(instance); // only discover one List serviceInstances = this.discoveryClient.getInstances(serviceName); assertEquals(1, serviceInstances.size()); } @Test public void registerTwoInstancesThenDeleteOne() { final String serviceName = "d-service"; final String cluster = "default"; this.serviceRegistry .register(newServiceInstance(serviceName, "http://192.168.1.20:8080/", cluster)); this.serviceRegistry .register(newServiceInstance(serviceName, "http://192.168.1.20:10000/", cluster)); final List serviceInstances = this.discoveryClient.getInstances(serviceName); assertEquals(2, serviceInstances.size()); for (ServiceInstance serviceInstance : serviceInstances) { assertEquals(serviceName, serviceInstance.getServiceName()); assertEquals(cluster, serviceInstance.getCluster()); assertEquals(0, serviceInstance.getMetadata().size()); } // delete one this.serviceRegistry .deregister(newServiceInstance(serviceName, "http://192.168.1.20:10000/", cluster)); // because it save in memory, so we can still find it assertEquals(2, this.discoveryClient.getInstances(serviceName).size()); } } ================================================ FILE: apollo-biz/src/test/java/com/ctrip/framework/apollo/biz/registry/DatabaseDiscoveryWithoutDecoratorIntegrationTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.registry; import static com.ctrip.framework.apollo.biz.registry.ServiceInstanceFactory.newServiceInstance; import static org.junit.jupiter.api.Assertions.assertEquals; import com.ctrip.framework.apollo.biz.AbstractIntegrationTest; import com.ctrip.framework.apollo.biz.registry.DatabaseDiscoveryWithoutDecoratorIntegrationTest.ApolloServiceDiscoveryWithoutDecoratorAutoConfiguration; import com.ctrip.framework.apollo.biz.registry.configuration.ApolloServiceDiscoveryAutoConfiguration; import com.ctrip.framework.apollo.biz.registry.configuration.ApolloServiceRegistryAutoConfiguration; import com.ctrip.framework.apollo.biz.registry.configuration.support.ApolloServiceDiscoveryProperties; import com.ctrip.framework.apollo.biz.service.ServiceRegistryService; import java.util.List; import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.AutoConfigureBefore; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.TestPropertySource; /** * test when {@link DatabaseDiscoveryClient} doesn't warp by decorator. */ @TestPropertySource(properties = {"apollo.service.registry.enabled=true", "apollo.service.registry.cluster=default", "apollo.service.discovery.enabled=true", "spring.application.name=for-test-service", "server.port=10000", // close decorator "ApolloServiceDiscoveryWithoutDecoratorAutoConfiguration.enabled=true",}) @ContextConfiguration(classes = {ApolloServiceRegistryAutoConfiguration.class, // notice that the order of classes is import // @AutoConfigureBefore(ApolloServiceDiscoveryAutoConfiguration.class) won't work when run test ApolloServiceDiscoveryWithoutDecoratorAutoConfiguration.class, ApolloServiceDiscoveryAutoConfiguration.class,}) public class DatabaseDiscoveryWithoutDecoratorIntegrationTest extends AbstractIntegrationTest { private final Logger log = LoggerFactory.getLogger(this.getClass()); @Autowired private DatabaseServiceRegistry serviceRegistry; @Autowired private DatabaseDiscoveryClient discoveryClient; /** * discover one after register, and delete it */ @Test public void registerThenDiscoveryThenDelete() { // register it String serviceName = "a-service"; String uri = "http://192.168.1.20:8080/"; String cluster = "default"; ServiceInstance instance = newServiceInstance(serviceName, uri, cluster); this.serviceRegistry.register(instance); // find it List serviceInstances = this.discoveryClient.getInstances(serviceName); assertEquals(1, serviceInstances.size()); ServiceInstance actual = serviceInstances.get(0); assertEquals(serviceName, actual.getServiceName()); assertEquals(uri, actual.getUri().toString()); assertEquals(cluster, actual.getCluster()); assertEquals(0, actual.getMetadata().size()); // delete it this.serviceRegistry.deregister(instance); // find none assertEquals(0, this.discoveryClient.getInstances(serviceName).size()); } /** * diff cluster so cannot be discover */ @Test public void registerThenDiscoveryNone() { // register it String serviceName = "b-service"; ServiceInstance instance = newServiceInstance(serviceName, "http://192.168.1.20:8080/", "cannot-be-discovery"); this.serviceRegistry.register(instance); // find none List serviceInstances = this.discoveryClient.getInstances(serviceName); assertEquals(0, serviceInstances.size()); } @Test public void registerTwice() { String serviceName = "c-service"; ServiceInstance instance = newServiceInstance(serviceName, "http://192.168.1.20:8080/", "default"); // register it this.serviceRegistry.register(instance); // register again this.serviceRegistry.register(instance); // only discover one List serviceInstances = this.discoveryClient.getInstances(serviceName); assertEquals(1, serviceInstances.size()); } @Test public void registerTwoInstancesThenDeleteOne() { final String serviceName = "d-service"; final String cluster = "default"; this.serviceRegistry .register(newServiceInstance(serviceName, "http://192.168.1.20:8080/", cluster)); this.serviceRegistry .register(newServiceInstance(serviceName, "http://192.168.1.20:10000/", cluster)); final List serviceInstances = this.discoveryClient.getInstances(serviceName); assertEquals(2, serviceInstances.size()); for (ServiceInstance serviceInstance : serviceInstances) { assertEquals(serviceName, serviceInstance.getServiceName()); assertEquals(cluster, serviceInstance.getCluster()); assertEquals(0, serviceInstance.getMetadata().size()); } // delete one this.serviceRegistry .deregister(newServiceInstance(serviceName, "http://192.168.1.20:10000/", cluster)); assertEquals(1, this.discoveryClient.getInstances(serviceName).size()); assertEquals("http://192.168.1.20:8080/", this.discoveryClient.getInstances(serviceName).get(0).getUri().toString()); } /** * only use in {@link DatabaseDiscoveryWithoutDecoratorIntegrationTest} */ @Configuration @ConditionalOnProperty(prefix = "ApolloServiceDiscoveryWithoutDecoratorAutoConfiguration", value = "enabled") @ConditionalOnBean(ApolloServiceDiscoveryAutoConfiguration.class) @AutoConfigureBefore(ApolloServiceDiscoveryAutoConfiguration.class) @EnableConfigurationProperties({ApolloServiceDiscoveryProperties.class,}) static class ApolloServiceDiscoveryWithoutDecoratorAutoConfiguration { @Bean public DatabaseDiscoveryClient databaseDiscoveryClient( ApolloServiceDiscoveryProperties discoveryProperties, ServiceInstance selfServiceInstance, ServiceRegistryService serviceRegistryService) { return new DatabaseDiscoveryClientImpl(serviceRegistryService, discoveryProperties, selfServiceInstance.getCluster()); } } } ================================================ FILE: apollo-biz/src/test/java/com/ctrip/framework/apollo/biz/registry/ServiceInstanceFactory.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.registry; import com.ctrip.framework.apollo.biz.registry.configuration.support.ApolloServiceRegistryProperties; public class ServiceInstanceFactory { static ServiceInstance newServiceInstance(String serviceName, String uri, String cluster) { ApolloServiceRegistryProperties instance = new ApolloServiceRegistryProperties(); instance.setServiceName(serviceName); instance.setUri(uri); instance.setCluster(cluster); return instance; } } ================================================ FILE: apollo-biz/src/test/java/com/ctrip/framework/apollo/biz/registry/configuration/ApolloServiceRegistryAutoConfigurationNotEnabledTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.registry.configuration; import com.ctrip.framework.apollo.biz.registry.DatabaseDiscoveryClient; import com.ctrip.framework.apollo.biz.registry.DatabaseServiceRegistry; import com.ctrip.framework.apollo.biz.registry.configuration.support.ApolloServiceRegistryClearApplicationRunner; import com.ctrip.framework.apollo.biz.registry.configuration.support.ApolloServiceRegistryDeregisterApplicationListener; import com.ctrip.framework.apollo.biz.registry.configuration.support.ApolloServiceRegistryHeartbeatApplicationRunner; import com.ctrip.framework.apollo.biz.repository.ServiceRegistryRepository; import com.ctrip.framework.apollo.biz.service.ServiceRegistryService; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.ApplicationContext; import org.springframework.test.context.ContextConfiguration; /** * ensure that this feature, i.e. database discovery won't cause configservice or adminservice * startup fail when it doesn't enable. */ @SpringBootTest @ContextConfiguration(classes = {ApolloServiceRegistryAutoConfiguration.class, ApolloServiceDiscoveryAutoConfiguration.class}) class ApolloServiceRegistryAutoConfigurationNotEnabledTest { @Autowired private ApplicationContext context; private void assertNoSuchBean(Class requiredType) { Assertions.assertThrows(NoSuchBeanDefinitionException.class, () -> context.getBean(requiredType)); } @Test void ensureNoSuchBeans() { assertNoSuchBean(ServiceRegistryRepository.class); assertNoSuchBean(ServiceRegistryService.class); assertNoSuchBean(DatabaseServiceRegistry.class); assertNoSuchBean(ApolloServiceRegistryHeartbeatApplicationRunner.class); assertNoSuchBean(ApolloServiceRegistryDeregisterApplicationListener.class); assertNoSuchBean(DatabaseDiscoveryClient.class); assertNoSuchBean(ApolloServiceRegistryClearApplicationRunner.class); } } ================================================ FILE: apollo-biz/src/test/java/com/ctrip/framework/apollo/biz/registry/configuration/support/ApolloServiceRegistryClearApplicationRunnerIntegrationTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.registry.configuration.support; import static org.junit.jupiter.api.Assertions.assertEquals; import com.ctrip.framework.apollo.biz.AbstractIntegrationTest; import com.ctrip.framework.apollo.biz.entity.ServiceRegistry; import com.ctrip.framework.apollo.biz.registry.configuration.ApolloServiceDiscoveryAutoConfiguration; import com.ctrip.framework.apollo.biz.registry.configuration.ApolloServiceRegistryAutoConfiguration; import com.ctrip.framework.apollo.biz.repository.ServiceRegistryRepository; import java.time.LocalDateTime; import java.util.List; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.TestPropertySource; @TestPropertySource(properties = {"apollo.service.registry.enabled=true", "apollo.service.registry.cluster=default", "apollo.service.discovery.enabled=true", "spring.application.name=for-test-service", "server.port=10000",}) @ContextConfiguration(classes = {ApolloServiceRegistryAutoConfiguration.class, ApolloServiceDiscoveryAutoConfiguration.class,}) public class ApolloServiceRegistryClearApplicationRunnerIntegrationTest extends AbstractIntegrationTest { @Autowired private ServiceRegistryRepository repository; @Autowired private ApolloServiceRegistryClearApplicationRunner runner; @Test public void clearUnhealthyInstances() { final String serviceName = "h-service"; final String healthUri = "http://10.240.11.22:8080/"; ServiceRegistry healthy = new ServiceRegistry(); healthy.setServiceName(serviceName); healthy.setCluster("c-1"); healthy.setUri(healthUri); healthy.setDataChangeCreatedTime(LocalDateTime.now()); healthy.setDataChangeLastModifiedTime(LocalDateTime.now()); this.repository.save(healthy); LocalDateTime unhealthyTime = LocalDateTime.now().minusDays(2L); ServiceRegistry unhealthy = new ServiceRegistry(); unhealthy.setServiceName("h-service"); unhealthy.setCluster("c-2"); unhealthy.setUri("http://10.240.33.44:9090/"); unhealthy.setDataChangeCreatedTime(unhealthyTime); unhealthy.setDataChangeLastModifiedTime(unhealthyTime); this.repository.save(unhealthy); { List serviceRegistryList = this.repository.findByServiceNameAndDataChangeLastModifiedTimeGreaterThan(serviceName, LocalDateTime.now().minusDays(3L)); assertEquals(2, serviceRegistryList.size()); } runner.clearUnhealthyInstances(); { List serviceRegistryList = this.repository.findByServiceNameAndDataChangeLastModifiedTimeGreaterThan(serviceName, LocalDateTime.now().minusDays(3L)); assertEquals(1, serviceRegistryList.size()); ServiceRegistry registry = serviceRegistryList.get(0); assertEquals(serviceName, registry.getServiceName()); assertEquals(healthUri, registry.getUri()); } } } ================================================ FILE: apollo-biz/src/test/java/com/ctrip/framework/apollo/biz/repository/AccessKeyRepositoryTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.repository; import static org.assertj.core.api.Assertions.assertThat; import com.ctrip.framework.apollo.biz.AbstractIntegrationTest; import com.ctrip.framework.apollo.biz.entity.AccessKey; import java.time.Instant; import java.time.LocalDateTime; import java.time.ZoneId; import java.util.Date; import java.util.List; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.jdbc.Sql; import org.springframework.test.context.jdbc.Sql.ExecutionPhase; public class AccessKeyRepositoryTest extends AbstractIntegrationTest { @Autowired private AccessKeyRepository accessKeyRepository; @Test public void testSave() { String appId = "someAppId"; String secret = "someSecret"; AccessKey entity = new AccessKey(); entity.setAppId(appId); entity.setSecret(secret); AccessKey accessKey = accessKeyRepository.save(entity); assertThat(accessKey).isNotNull(); assertThat(accessKey.getAppId()).isEqualTo(appId); assertThat(accessKey.getSecret()).isEqualTo(secret); } @Test @Sql(scripts = "/sql/accesskey-test.sql", executionPhase = ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/sql/clean.sql", executionPhase = ExecutionPhase.AFTER_TEST_METHOD) public void testFindByAppId() { String appId = "someAppId"; List accessKeyList = accessKeyRepository.findByAppId(appId); assertThat(accessKeyList).hasSize(1); assertThat(accessKeyList.get(0).getAppId()).isEqualTo(appId); assertThat(accessKeyList.get(0).getSecret()).isEqualTo("someSecret"); } @Test @Sql(scripts = "/sql/accesskey-test.sql", executionPhase = ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/sql/clean.sql", executionPhase = ExecutionPhase.AFTER_TEST_METHOD) public void testFindFirst500ByDataChangeLastModifiedTimeGreaterThanOrderByDataChangeLastModifiedTime() { Instant instant = LocalDateTime.of(2019, 12, 19, 13, 44, 20).atZone(ZoneId.systemDefault()).toInstant(); Date date = Date.from(instant); List accessKeyList = accessKeyRepository .findFirst500ByDataChangeLastModifiedTimeGreaterThanOrderByDataChangeLastModifiedTimeAsc( date); assertThat(accessKeyList).hasSize(2); assertThat(accessKeyList.get(0).getAppId()).isEqualTo("100004458"); assertThat(accessKeyList.get(0).getSecret()).isEqualTo("4003c4d7783443dc9870932bebf3b7fe"); assertThat(accessKeyList.get(1).getAppId()).isEqualTo("100004458"); assertThat(accessKeyList.get(1).getSecret()).isEqualTo("c715cbc80fc44171b43732c3119c9456"); } @Test @Sql(scripts = "/sql/accesskey-test.sql", executionPhase = ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/sql/clean.sql", executionPhase = ExecutionPhase.AFTER_TEST_METHOD) public void testFindFirst500ByDataChangeLastModifiedTimeGreaterThanEqualAndDataChangeLastModifiedTimeLessThanOrderByDataChangeLastModifiedTime() { Instant instantStart = LocalDateTime.of(2019, 12, 19, 13, 44, 21).atZone(ZoneId.systemDefault()).toInstant(); Date dateStart = Date.from(instantStart); Instant instantEnd = LocalDateTime.of(2019, 12, 19, 13, 44, 22).atZone(ZoneId.systemDefault()).toInstant(); Date dateEnd = Date.from(instantEnd); List accessKeyList = accessKeyRepository .findFirst500ByDataChangeLastModifiedTimeGreaterThanEqualAndDataChangeLastModifiedTimeLessThanOrderByDataChangeLastModifiedTimeAsc( dateStart, dateEnd); assertThat(accessKeyList).hasSize(1); assertThat(accessKeyList.get(0).getAppId()).isEqualTo("100004458"); assertThat(accessKeyList.get(0).getSecret()).isEqualTo("4003c4d7783443dc9870932bebf3b7fe"); } } ================================================ FILE: apollo-biz/src/test/java/com/ctrip/framework/apollo/biz/repository/AppNamespaceRepositoryTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.repository; import com.ctrip.framework.apollo.biz.AbstractIntegrationTest; import com.ctrip.framework.apollo.common.entity.AppNamespace; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; public class AppNamespaceRepositoryTest extends AbstractIntegrationTest { @Autowired private AppNamespaceRepository repository; @Test public void testFindByNameAndIsPublicTrue() throws Exception { AppNamespace appNamespace = repository.findByNameAndIsPublicTrue("fx.apollo.config"); assertEquals("100003171", appNamespace.getAppId()); } @Test public void testFindByNameAndNoPublicNamespace() throws Exception { AppNamespace appNamespace = repository.findByNameAndIsPublicTrue("application"); assertNull(appNamespace); } } ================================================ FILE: apollo-biz/src/test/java/com/ctrip/framework/apollo/biz/repository/AppRepositoryTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.repository; import com.ctrip.framework.apollo.biz.AbstractIntegrationTest; import com.ctrip.framework.apollo.common.entity.App; import org.junit.Assert; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; public class AppRepositoryTest extends AbstractIntegrationTest { @Autowired private AppRepository appRepository; @Test public void testCreate() { String appId = "someAppId"; String appName = "someAppName"; String ownerName = "someOwnerName"; String ownerEmail = "someOwnerName@ctrip.com"; App app = new App(); app.setAppId(appId); app.setName(appName); app.setOwnerName(ownerName); app.setOwnerEmail(ownerEmail); Assert.assertEquals(0, appRepository.count()); appRepository.save(app); Assert.assertEquals(1, appRepository.count()); } @Test public void testRemove() { String appId = "someAppId"; String appName = "someAppName"; String ownerName = "someOwnerName"; String ownerEmail = "someOwnerName@ctrip.com"; App app = new App(); app.setAppId(appId); app.setName(appName); app.setOwnerName(ownerName); app.setOwnerEmail(ownerEmail); Assert.assertEquals(0, appRepository.count()); appRepository.save(app); Assert.assertEquals(1, appRepository.count()); appRepository.deleteById(app.getId()); Assert.assertEquals(0, appRepository.count()); } } ================================================ FILE: apollo-biz/src/test/java/com/ctrip/framework/apollo/biz/repository/InstanceConfigRepositoryTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.repository; import com.ctrip.framework.apollo.biz.AbstractIntegrationTest; import com.ctrip.framework.apollo.biz.entity.Instance; import com.ctrip.framework.apollo.biz.entity.InstanceConfig; import java.util.Date; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.test.annotation.Rollback; import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.MatcherAssert.assertThat; /** * Created by kezhenxu94 at 2019/1/18 15:33. * * @author kezhenxu94 (kezhenxu94 at 163 dot com) */ public class InstanceConfigRepositoryTest extends AbstractIntegrationTest { @Autowired private InstanceConfigRepository instanceConfigRepository; @Autowired private InstanceRepository instanceRepository; @Rollback @Test public void shouldPaginated() { for (int i = 0; i < 25; i++) { Instance instance = new Instance(); instance.setAppId("appId"); instanceRepository.save(instance); final InstanceConfig instanceConfig = new InstanceConfig(); instanceConfig.setConfigAppId("appId"); instanceConfig.setInstanceId(instance.getId()); instanceConfig.setConfigClusterName("cluster"); instanceConfig.setConfigNamespaceName("namespace"); instanceConfigRepository.save(instanceConfig); } Page ids = instanceConfigRepository.findInstanceIdsByNamespaceAndInstanceAppId("appId", "appId", "cluster", "namespace", new Date(0), PageRequest.of(0, 10)); assertThat(ids.getContent(), hasSize(10)); ids = instanceConfigRepository.findInstanceIdsByNamespaceAndInstanceAppId("appId", "appId", "cluster", "namespace", new Date(0), PageRequest.of(1, 10)); assertThat(ids.getContent(), hasSize(10)); ids = instanceConfigRepository.findInstanceIdsByNamespaceAndInstanceAppId("appId", "appId", "cluster", "namespace", new Date(0), PageRequest.of(2, 10)); assertThat(ids.getContent(), hasSize(5)); } } ================================================ FILE: apollo-biz/src/test/java/com/ctrip/framework/apollo/biz/repository/ReleaseHistoryRepositoryTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.repository; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import com.ctrip.framework.apollo.biz.AbstractIntegrationTest; import com.ctrip.framework.apollo.biz.entity.ReleaseHistory; import java.util.List; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.test.context.jdbc.Sql; import org.springframework.test.context.jdbc.Sql.ExecutionPhase; /** * @author kl (http://kailing.pub) * @since 2023/3/23 */ public class ReleaseHistoryRepositoryTest extends AbstractIntegrationTest { @Autowired private ReleaseHistoryRepository releaseHistoryRepository; @Test @Sql(scripts = "/sql/release-history-test.sql", executionPhase = ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/sql/clean.sql", executionPhase = ExecutionPhase.AFTER_TEST_METHOD) public void testFindReleaseHistoryRetentionMaxId() { Page releaseHistoryPage = releaseHistoryRepository .findByAppIdAndClusterNameAndNamespaceNameAndBranchNameOrderByIdDesc(APP_ID, CLUSTER_NAME, NAMESPACE_NAME, BRANCH_NAME, PageRequest.of(1, 1)); assertEquals(5, releaseHistoryPage.getContent().get(0).getId()); releaseHistoryPage = releaseHistoryRepository .findByAppIdAndClusterNameAndNamespaceNameAndBranchNameOrderByIdDesc(APP_ID, CLUSTER_NAME, NAMESPACE_NAME, BRANCH_NAME, PageRequest.of(2, 1)); assertEquals(4, releaseHistoryPage.getContent().get(0).getId()); releaseHistoryPage = releaseHistoryRepository .findByAppIdAndClusterNameAndNamespaceNameAndBranchNameOrderByIdDesc(APP_ID, CLUSTER_NAME, NAMESPACE_NAME, BRANCH_NAME, PageRequest.of(5, 1)); assertEquals(1, releaseHistoryPage.getContent().get(0).getId()); releaseHistoryRepository.deleteAll(); releaseHistoryPage = releaseHistoryRepository .findByAppIdAndClusterNameAndNamespaceNameAndBranchNameOrderByIdDesc(APP_ID, CLUSTER_NAME, NAMESPACE_NAME, BRANCH_NAME, PageRequest.of(1, 1)); assertTrue(releaseHistoryPage.isEmpty()); } @Test @Sql(scripts = "/sql/release-history-test.sql", executionPhase = ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/sql/clean.sql", executionPhase = ExecutionPhase.AFTER_TEST_METHOD) public void testFindFirst100ByAppIdAndClusterNameAndNamespaceNameAndBranchNameAndIdLessThanEqualOrderByIdAsc() { int releaseHistoryRetentionSize = 2; Page releaseHistoryPage = releaseHistoryRepository .findByAppIdAndClusterNameAndNamespaceNameAndBranchNameOrderByIdDesc(APP_ID, CLUSTER_NAME, NAMESPACE_NAME, BRANCH_NAME, PageRequest.of(releaseHistoryRetentionSize, 1)); long releaseMaxId = releaseHistoryPage.getContent().get(0).getId(); List releaseHistories = releaseHistoryRepository .findFirst100ByAppIdAndClusterNameAndNamespaceNameAndBranchNameAndIdLessThanEqualOrderByIdAsc( APP_ID, CLUSTER_NAME, NAMESPACE_NAME, BRANCH_NAME, releaseMaxId); assertEquals(4, releaseHistories.size()); releaseHistoryRetentionSize = 1; releaseHistoryPage = releaseHistoryRepository .findByAppIdAndClusterNameAndNamespaceNameAndBranchNameOrderByIdDesc(APP_ID, CLUSTER_NAME, NAMESPACE_NAME, BRANCH_NAME, PageRequest.of(releaseHistoryRetentionSize, 1)); releaseMaxId = releaseHistoryPage.getContent().get(0).getId(); releaseHistories = releaseHistoryRepository .findFirst100ByAppIdAndClusterNameAndNamespaceNameAndBranchNameAndIdLessThanEqualOrderByIdAsc( APP_ID, CLUSTER_NAME, NAMESPACE_NAME, BRANCH_NAME, releaseMaxId); assertEquals(5, releaseHistories.size()); } } ================================================ FILE: apollo-biz/src/test/java/com/ctrip/framework/apollo/biz/service/AccessKeyServiceTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.service; import static org.junit.Assert.assertNotNull; import com.ctrip.framework.apollo.biz.AbstractIntegrationTest; import com.ctrip.framework.apollo.biz.entity.AccessKey; import com.ctrip.framework.apollo.common.exception.BadRequestException; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; /** * @author nisiyong */ public class AccessKeyServiceTest extends AbstractIntegrationTest { @Autowired private AccessKeyService accessKeyService; @Test public void testCreate() { String appId = "someAppId"; String secret = "someSecret"; AccessKey entity = assembleAccessKey(appId, secret); AccessKey accessKey = accessKeyService.create(appId, entity); assertNotNull(accessKey); } @Test(expected = BadRequestException.class) public void testCreateWithException() { String appId = "someAppId"; String secret = "someSecret"; int maxCount = 5; for (int i = 0; i <= maxCount; i++) { AccessKey entity = assembleAccessKey(appId, secret); accessKeyService.create(appId, entity); } } private AccessKey assembleAccessKey(String appId, String secret) { AccessKey accessKey = new AccessKey(); accessKey.setAppId(appId); accessKey.setSecret(secret); return accessKey; } } ================================================ FILE: apollo-biz/src/test/java/com/ctrip/framework/apollo/biz/service/AdminServiceTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.service; import com.ctrip.framework.apollo.biz.AbstractIntegrationTest; import com.ctrip.framework.apollo.biz.entity.Audit; import com.ctrip.framework.apollo.biz.entity.Cluster; import com.ctrip.framework.apollo.biz.entity.Namespace; import com.ctrip.framework.apollo.biz.repository.AppRepository; import com.ctrip.framework.apollo.common.entity.App; import com.ctrip.framework.apollo.common.exception.ServiceException; import com.ctrip.framework.apollo.core.ConfigConsts; import java.util.Date; import java.util.List; import org.junit.Assert; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; public class AdminServiceTest extends AbstractIntegrationTest { @Autowired private AdminService adminService; @Autowired private AuditService auditService; @Autowired private AppRepository appRepository; @Autowired private ClusterService clusterService; @Autowired private NamespaceService namespaceService; @Autowired private AppNamespaceService appNamespaceService; @Test public void testCreateNewApp() { String appId = "someAppId"; App app = new App(); app.setAppId(appId); app.setName("someAppName"); String owner = "someOwnerName"; app.setOwnerName(owner); app.setOwnerEmail("someOwnerName@ctrip.com"); app.setDataChangeCreatedBy(owner); app.setDataChangeLastModifiedBy(owner); app.setDataChangeCreatedTime(new Date()); app = adminService.createNewApp(app); Assert.assertEquals(appId, app.getAppId()); List clusters = clusterService.findParentClusters(app.getAppId()); Assert.assertEquals(1, clusters.size()); Assert.assertEquals(ConfigConsts.CLUSTER_NAME_DEFAULT, clusters.get(0).getName()); List namespaces = namespaceService.findNamespaces(appId, clusters.get(0).getName()); Assert.assertEquals(1, namespaces.size()); Assert.assertEquals(ConfigConsts.NAMESPACE_APPLICATION, namespaces.get(0).getNamespaceName()); List audits = auditService.findByOwner(owner); Assert.assertEquals(4, audits.size()); } @Test(expected = ServiceException.class) public void testCreateDuplicateApp() { String appId = "someAppId"; App app = new App(); app.setAppId(appId); app.setName("someAppName"); String owner = "someOwnerName"; app.setOwnerName(owner); app.setOwnerEmail("someOwnerName@ctrip.com"); app.setDataChangeCreatedBy(owner); app.setDataChangeLastModifiedBy(owner); app.setDataChangeCreatedTime(new Date()); appRepository.save(app); adminService.createNewApp(app); } @Test public void testDeleteApp() { String appId = "someAppId"; App app = new App(); app.setAppId(appId); app.setName("someAppName"); String owner = "someOwnerName"; app.setOwnerName(owner); app.setOwnerEmail("someOwnerName@ctrip.com"); app.setDataChangeCreatedBy(owner); app.setDataChangeLastModifiedBy(owner); app.setDataChangeCreatedTime(new Date()); app = adminService.createNewApp(app); Assert.assertEquals(appId, app.getAppId()); Assert.assertEquals(1, appNamespaceService.findByAppId(appId).size()); Assert.assertEquals(1, clusterService.findClusters(appId).size()); Assert.assertEquals(1, namespaceService.findNamespaces(appId, ConfigConsts.CLUSTER_NAME_DEFAULT).size()); adminService.deleteApp(app, owner); Assert.assertEquals(0, appNamespaceService.findByAppId(appId).size()); Assert.assertEquals(0, clusterService.findClusters(appId).size()); Assert.assertEquals(0, namespaceService .findByAppIdAndNamespaceName(appId, ConfigConsts.CLUSTER_NAME_DEFAULT).size()); } } ================================================ FILE: apollo-biz/src/test/java/com/ctrip/framework/apollo/biz/service/AdminServiceTransactionTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.service; import com.ctrip.framework.apollo.biz.AbstractIntegrationTest; import com.ctrip.framework.apollo.biz.repository.AppNamespaceRepository; import com.ctrip.framework.apollo.biz.repository.AppRepository; import com.ctrip.framework.apollo.biz.repository.ClusterRepository; import com.ctrip.framework.apollo.biz.repository.NamespaceRepository; import com.ctrip.framework.apollo.common.entity.App; import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.annotation.Rollback; import org.springframework.test.context.transaction.AfterTransaction; import org.springframework.test.context.transaction.BeforeTransaction; import java.util.Date; public class AdminServiceTransactionTest extends AbstractIntegrationTest { @Autowired AdminService adminService; @Autowired private AppRepository appRepository; @Autowired private AppNamespaceRepository appNamespaceRepository; @Autowired private NamespaceRepository namespaceRepository; @Autowired private ClusterRepository clusterRepository; @BeforeTransaction public void verifyInitialDatabaseState() { for (App app : appRepository.findAll()) { System.out.println(app.getAppId()); } Assert.assertEquals(0, appRepository.count()); Assert.assertEquals(7, appNamespaceRepository.count()); Assert.assertEquals(0, namespaceRepository.count()); Assert.assertEquals(0, clusterRepository.count()); } @Before public void setUpTestDataWithinTransaction() { Assert.assertEquals(0, appRepository.count()); Assert.assertEquals(7, appNamespaceRepository.count()); Assert.assertEquals(0, namespaceRepository.count()); Assert.assertEquals(0, clusterRepository.count()); } @Test @Rollback public void modifyDatabaseWithinTransaction() { String appId = "someAppId"; App app = new App(); app.setAppId(appId); app.setName("someAppName"); String owner = "someOwnerName"; app.setOwnerName(owner); app.setOwnerEmail("someOwnerName@ctrip.com"); app.setDataChangeCreatedBy(owner); app.setDataChangeLastModifiedBy(owner); app.setDataChangeCreatedTime(new Date()); adminService.createNewApp(app); } @After public void tearDownWithinTransaction() { Assert.assertEquals(1, appRepository.count()); Assert.assertEquals(8, appNamespaceRepository.count()); Assert.assertEquals(1, namespaceRepository.count()); Assert.assertEquals(1, clusterRepository.count()); } @AfterTransaction public void verifyFinalDatabaseState() { Assert.assertEquals(0, appRepository.count()); Assert.assertEquals(7, appNamespaceRepository.count()); Assert.assertEquals(0, namespaceRepository.count()); Assert.assertEquals(0, clusterRepository.count()); } } ================================================ FILE: apollo-biz/src/test/java/com/ctrip/framework/apollo/biz/service/BizDBPropertySourceTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.service; import com.google.common.collect.Lists; import com.ctrip.framework.apollo.biz.AbstractUnitTest; import com.ctrip.framework.apollo.biz.MockBeanFactory; import com.ctrip.framework.apollo.biz.entity.ServerConfig; import com.ctrip.framework.apollo.biz.repository.ServerConfigRepository; import com.ctrip.framework.apollo.core.ConfigConsts; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.mockito.Mock; import org.springframework.core.env.Environment; import javax.sql.DataSource; import java.util.List; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; /** * @author Jason Song(song_s@ctrip.com) */ public class BizDBPropertySourceTest extends AbstractUnitTest { @Mock private ServerConfigRepository serverConfigRepository; @Mock private DataSource dataSource; @Mock private Environment environment; private BizDBPropertySource propertySource; private String clusterConfigKey = "clusterKey"; private String clusterConfigValue = "clusterValue"; private String dcConfigKey = "dcKey"; private String dcConfigValue = "dcValue"; private String defaultKey = "defaultKey"; private String defaultValue = "defaultValue"; @Before public void initTestData() { propertySource = spy(new BizDBPropertySource(serverConfigRepository, dataSource, environment)); List configs = Lists.newLinkedList(); // cluster config String cluster = "cluster"; configs.add(MockBeanFactory.mockServerConfig(clusterConfigKey, clusterConfigValue, cluster)); String dc = "dc"; configs.add(MockBeanFactory.mockServerConfig(clusterConfigKey, clusterConfigValue + "dc", dc)); configs.add(MockBeanFactory.mockServerConfig(clusterConfigKey, clusterConfigValue + ConfigConsts.CLUSTER_NAME_DEFAULT, ConfigConsts.CLUSTER_NAME_DEFAULT)); // dc config configs.add(MockBeanFactory.mockServerConfig(dcConfigKey, dcConfigValue, dc)); configs.add(MockBeanFactory.mockServerConfig(dcConfigKey, dcConfigValue + ConfigConsts.CLUSTER_NAME_DEFAULT, ConfigConsts.CLUSTER_NAME_DEFAULT)); // default config configs.add(MockBeanFactory.mockServerConfig(defaultKey, defaultValue, ConfigConsts.CLUSTER_NAME_DEFAULT)); System.setProperty(ConfigConsts.APOLLO_CLUSTER_KEY, cluster); when(propertySource.getCurrentDataCenter()).thenReturn(dc); when(serverConfigRepository.findAll()).thenReturn(configs); } @After public void clear() { System.clearProperty(ConfigConsts.APOLLO_CLUSTER_KEY); } @Test public void testGetClusterConfig() { propertySource.refresh(); assertEquals(propertySource.getProperty(clusterConfigKey), clusterConfigValue); } @Test public void testGetDcConfig() { propertySource.refresh(); assertEquals(propertySource.getProperty(dcConfigKey), dcConfigValue); } @Test public void testGetDefaultConfig() { propertySource.refresh(); assertEquals(propertySource.getProperty(defaultKey), defaultValue); } @Test public void testGetNull() { propertySource.refresh(); assertNull(propertySource.getProperty("noKey")); } } ================================================ FILE: apollo-biz/src/test/java/com/ctrip/framework/apollo/biz/service/ClusterServiceTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.service; import com.ctrip.framework.apollo.biz.AbstractIntegrationTest; import com.ctrip.framework.apollo.common.entity.App; import com.ctrip.framework.apollo.common.exception.ServiceException; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import java.util.Date; public class ClusterServiceTest extends AbstractIntegrationTest { @Autowired private AdminService adminService; @Autowired private ClusterService clusterService; @Test(expected = ServiceException.class) public void testCreateDuplicateCluster() { String appId = "someAppId"; App app = new App(); app.setAppId(appId); app.setName("someAppName"); String owner = "someOwnerName"; app.setOwnerName(owner); app.setOwnerEmail("someOwnerName@ctrip.com"); app.setDataChangeCreatedBy(owner); app.setDataChangeLastModifiedBy(owner); app.setDataChangeCreatedTime(new Date()); adminService.createNewApp(app); clusterService.createDefaultCluster(appId, owner); } } ================================================ FILE: apollo-biz/src/test/java/com/ctrip/framework/apollo/biz/service/InstanceServiceTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.service; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import com.ctrip.framework.apollo.biz.AbstractIntegrationTest; import com.ctrip.framework.apollo.biz.entity.Instance; import com.ctrip.framework.apollo.biz.entity.InstanceConfig; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.test.annotation.Rollback; import java.util.Calendar; import java.util.Date; import java.util.List; import java.util.Set; import java.util.stream.Collectors; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNull; /** * @author Jason Song(song_s@ctrip.com) */ public class InstanceServiceTest extends AbstractIntegrationTest { @Autowired private InstanceService instanceService; @Test @Rollback public void testCreateAndFindInstance() throws Exception { String someAppId = "someAppId"; String someClusterName = "someClusterName"; String someDataCenter = "someDataCenter"; String someIp = "someIp"; Instance instance = instanceService.findInstance(someAppId, someClusterName, someDataCenter, someIp); assertNull(instance); instanceService .createInstance(assembleInstance(someAppId, someClusterName, someDataCenter, someIp)); instance = instanceService.findInstance(someAppId, someClusterName, someDataCenter, someIp); assertNotEquals(0, instance.getId()); } @Test @Rollback public void testFindInstancesByIds() throws Exception { String someAppId = "someAppId"; String someClusterName = "someClusterName"; String someDataCenter = "someDataCenter"; String someIp = "someIp"; String anotherIp = "anotherIp"; Instance someInstance = instanceService .createInstance(assembleInstance(someAppId, someClusterName, someDataCenter, someIp)); Instance anotherInstance = instanceService .createInstance(assembleInstance(someAppId, someClusterName, someDataCenter, anotherIp)); List instances = instanceService .findInstancesByIds(Sets.newHashSet(someInstance.getId(), anotherInstance.getId())); Set ips = instances.stream().map(Instance::getIp).collect(Collectors.toSet()); assertEquals(2, instances.size()); assertEquals(Sets.newHashSet(someIp, anotherIp), ips); } @Test @Rollback public void testCreateAndFindInstanceConfig() throws Exception { long someInstanceId = 1; String someConfigAppId = "someConfigAppId"; String someConfigClusterName = "someConfigClusterName"; String someConfigNamespaceName = "someConfigNamespaceName"; String someReleaseKey = "someReleaseKey"; String anotherReleaseKey = "anotherReleaseKey"; InstanceConfig instanceConfig = instanceService.findInstanceConfig(someInstanceId, someConfigAppId, someConfigNamespaceName); assertNull(instanceConfig); instanceService.createInstanceConfig(assembleInstanceConfig(someInstanceId, someConfigAppId, someConfigClusterName, someConfigNamespaceName, someReleaseKey)); instanceConfig = instanceService.findInstanceConfig(someInstanceId, someConfigAppId, someConfigNamespaceName); assertNotEquals(0, instanceConfig.getId()); assertEquals(someReleaseKey, instanceConfig.getReleaseKey()); instanceConfig.setReleaseKey(anotherReleaseKey); instanceService.updateInstanceConfig(instanceConfig); InstanceConfig updated = instanceService.findInstanceConfig(someInstanceId, someConfigAppId, someConfigNamespaceName); assertEquals(instanceConfig.getId(), updated.getId()); assertEquals(anotherReleaseKey, updated.getReleaseKey()); } @Test @Rollback public void testFindActiveInstanceConfigs() throws Exception { long someInstanceId = 1; long anotherInstanceId = 2; String someConfigAppId = "someConfigAppId"; String someConfigClusterName = "someConfigClusterName"; String someConfigNamespaceName = "someConfigNamespaceName"; Date someValidDate = new Date(); Pageable pageable = PageRequest.of(0, 10); String someReleaseKey = "someReleaseKey"; Calendar calendar = Calendar.getInstance(); calendar.add(Calendar.DATE, -2); Date someInvalidDate = calendar.getTime(); prepareInstanceConfigForInstance(someInstanceId, someConfigAppId, someConfigClusterName, someConfigNamespaceName, someReleaseKey, someValidDate); prepareInstanceConfigForInstance(anotherInstanceId, someConfigAppId, someConfigClusterName, someConfigNamespaceName, someReleaseKey, someInvalidDate); Page validInstanceConfigs = instanceService.findActiveInstanceConfigsByReleaseKey(someReleaseKey, pageable); assertEquals(1, validInstanceConfigs.getContent().size()); assertEquals(someInstanceId, validInstanceConfigs.getContent().get(0).getInstanceId()); } @Test @Rollback public void testFindInstancesByNamespace() throws Exception { String someConfigAppId = "someConfigAppId"; String someConfigClusterName = "someConfigClusterName"; String someConfigNamespaceName = "someConfigNamespaceName"; String someReleaseKey = "someReleaseKey"; Date someValidDate = new Date(); String someAppId = "someAppId"; String someClusterName = "someClusterName"; String someDataCenter = "someDataCenter"; String someIp = "someIp"; String anotherIp = "anotherIp"; Instance someInstance = instanceService .createInstance(assembleInstance(someAppId, someClusterName, someDataCenter, someIp)); Instance anotherInstance = instanceService .createInstance(assembleInstance(someAppId, someClusterName, someDataCenter, anotherIp)); prepareInstanceConfigForInstance(someInstance.getId(), someConfigAppId, someConfigClusterName, someConfigNamespaceName, someReleaseKey, someValidDate); prepareInstanceConfigForInstance(anotherInstance.getId(), someConfigAppId, someConfigClusterName, someConfigNamespaceName, someReleaseKey, someValidDate); Page result = instanceService.findInstancesByNamespace(someConfigAppId, someConfigClusterName, someConfigNamespaceName, PageRequest.of(0, 10)); assertEquals(Lists.newArrayList(someInstance, anotherInstance), result.getContent()); } @Test @Rollback public void testFindInstancesByNamespaceAndInstanceAppId() throws Exception { String someConfigAppId = "someConfigAppId"; String someConfigClusterName = "someConfigClusterName"; String someConfigNamespaceName = "someConfigNamespaceName"; String someReleaseKey = "someReleaseKey"; Date someValidDate = new Date(); String someAppId = "someAppId"; String anotherAppId = "anotherAppId"; String someClusterName = "someClusterName"; String someDataCenter = "someDataCenter"; String someIp = "someIp"; Instance someInstance = instanceService .createInstance(assembleInstance(someAppId, someClusterName, someDataCenter, someIp)); Instance anotherInstance = instanceService .createInstance(assembleInstance(anotherAppId, someClusterName, someDataCenter, someIp)); prepareInstanceConfigForInstance(someInstance.getId(), someConfigAppId, someConfigClusterName, someConfigNamespaceName, someReleaseKey, someValidDate); prepareInstanceConfigForInstance(anotherInstance.getId(), someConfigAppId, someConfigClusterName, someConfigNamespaceName, someReleaseKey, someValidDate); Page result = instanceService.findInstancesByNamespaceAndInstanceAppId(someAppId, someConfigAppId, someConfigClusterName, someConfigNamespaceName, PageRequest.of(0, 10)); Page anotherResult = instanceService.findInstancesByNamespaceAndInstanceAppId(anotherAppId, someConfigAppId, someConfigClusterName, someConfigNamespaceName, PageRequest.of(0, 10)); assertEquals(Lists.newArrayList(someInstance), result.getContent()); assertEquals(Lists.newArrayList(anotherInstance), anotherResult.getContent()); } @Test @Rollback public void testFindInstanceConfigsByNamespaceWithReleaseKeysNotIn() throws Exception { long someInstanceId = 1; long anotherInstanceId = 2; long yetAnotherInstanceId = 3; String someConfigAppId = "someConfigAppId"; String someConfigClusterName = "someConfigClusterName"; String someConfigNamespaceName = "someConfigNamespaceName"; Date someValidDate = new Date(); String someReleaseKey = "someReleaseKey"; String anotherReleaseKey = "anotherReleaseKey"; String yetAnotherReleaseKey = "yetAnotherReleaseKey"; InstanceConfig someInstanceConfig = prepareInstanceConfigForInstance(someInstanceId, someConfigAppId, someConfigClusterName, someConfigNamespaceName, someReleaseKey, someValidDate); InstanceConfig anotherInstanceConfig = prepareInstanceConfigForInstance(anotherInstanceId, someConfigAppId, someConfigClusterName, someConfigNamespaceName, someReleaseKey, someValidDate); prepareInstanceConfigForInstance(yetAnotherInstanceId, someConfigAppId, someConfigClusterName, someConfigNamespaceName, anotherReleaseKey, someValidDate); List instanceConfigs = instanceService .findInstanceConfigsByNamespaceWithReleaseKeysNotIn(someConfigAppId, someConfigClusterName, someConfigNamespaceName, Sets.newHashSet(anotherReleaseKey, yetAnotherReleaseKey)); assertEquals(Lists.newArrayList(someInstanceConfig, anotherInstanceConfig), instanceConfigs); } private InstanceConfig prepareInstanceConfigForInstance(long instanceId, String configAppId, String configClusterName, String configNamespace, String releaseKey, Date lastModifiedTime) { InstanceConfig someConfig = assembleInstanceConfig(instanceId, configAppId, configClusterName, configNamespace, releaseKey); someConfig.setDataChangeCreatedTime(lastModifiedTime); someConfig.setDataChangeLastModifiedTime(lastModifiedTime); return instanceService.createInstanceConfig(someConfig); } private Instance assembleInstance(String appId, String clusterName, String dataCenter, String ip) { Instance instance = new Instance(); instance.setAppId(appId); instance.setIp(ip); instance.setClusterName(clusterName); instance.setDataCenter(dataCenter); return instance; } private InstanceConfig assembleInstanceConfig(long instanceId, String configAppId, String configClusterName, String configNamespaceName, String releaseKey) { InstanceConfig instanceConfig = new InstanceConfig(); instanceConfig.setInstanceId(instanceId); instanceConfig.setConfigAppId(configAppId); instanceConfig.setConfigClusterName(configClusterName); instanceConfig.setConfigNamespaceName(configNamespaceName); instanceConfig.setReleaseKey(releaseKey); return instanceConfig; } } ================================================ FILE: apollo-biz/src/test/java/com/ctrip/framework/apollo/biz/service/ItemServiceTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.service; import static org.mockito.Mockito.when; import com.ctrip.framework.apollo.biz.AbstractIntegrationTest; import com.ctrip.framework.apollo.biz.config.BizConfig; import com.ctrip.framework.apollo.biz.entity.Item; import com.ctrip.framework.apollo.biz.repository.ItemRepository; import com.ctrip.framework.apollo.common.dto.ItemInfoDTO; import com.ctrip.framework.apollo.common.exception.BadRequestException; import java.util.HashMap; import java.util.Map; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.mockito.Mock; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.test.context.jdbc.Sql; public class ItemServiceTest extends AbstractIntegrationTest { @Autowired private ItemService itemService; @Autowired private ItemRepository itemRepository; @Autowired private NamespaceService namespaceService; @Autowired private AuditService auditService; @Mock private BizConfig bizConfig; private ItemService itemService2; @Before public void setUp() throws Exception { itemService2 = new ItemService(itemRepository, namespaceService, auditService, bizConfig); } @Test @Sql(scripts = {"/sql/namespace-test.sql", "/sql/item-test.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/sql/clean.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testSaveItem() { Item item = createItem(1L, "k3", "v3", -1); try { itemService.save(item); Assert.fail(); } catch (Exception e) { Assert.assertTrue(e instanceof BadRequestException); } item.setType(0); Item dbItem = itemService.save(item); Assert.assertEquals(0, dbItem.getType()); } @Test @Sql(scripts = {"/sql/namespace-test.sql", "/sql/item-test.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/sql/clean.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testSaveItemWithNamespaceValueLengthLimitOverride() { long namespaceId = 1L; String itemValue = "test-demo"; Map namespaceValueLengthOverride = new HashMap<>(); namespaceValueLengthOverride.put(namespaceId, itemValue.length() - 1); when(bizConfig.namespaceValueLengthLimitOverride()).thenReturn(namespaceValueLengthOverride); when(bizConfig.itemKeyLengthLimit()).thenReturn(100); Item item = createItem(namespaceId, "k3", itemValue, 2); try { itemService2.save(item); Assert.fail(); } catch (Exception e) { Assert.assertTrue( e instanceof BadRequestException && e.getMessage().contains("value too long")); } } @Test @Sql(scripts = {"/sql/namespace-test.sql", "/sql/item-test.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/sql/clean.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testSaveItemWithAppIdValueLengthLimitOverride() { String appId = "testApp"; long namespaceId = 1L; String itemValue = "test-demo"; Map appIdValueLengthOverride = new HashMap<>(); appIdValueLengthOverride.put(appId, itemValue.length() - 1); when(bizConfig.appIdValueLengthLimitOverride()).thenReturn(appIdValueLengthOverride); when(bizConfig.itemKeyLengthLimit()).thenReturn(100); Item item = createItem(namespaceId, "k3", itemValue, 2); try { itemService2.save(item); Assert.fail(); } catch (Exception e) { Assert.assertTrue( e instanceof BadRequestException && e.getMessage().contains("value too long")); } } @Test @Sql(scripts = {"/sql/namespace-test.sql", "/sql/item-test.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/sql/clean.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testUpdateItem() { Item item = createItem(1, "k1", "v1-new", 2); item.setId(9901); item.setLineNum(1); Item dbItem = itemService.update(item); Assert.assertEquals(2, dbItem.getType()); Assert.assertEquals("v1-new", dbItem.getValue()); } @Test @Sql(scripts = {"/sql/namespace-test.sql", "/sql/item-test.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/sql/clean.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testSearchItem() { ItemInfoDTO itemInfoDTO = new ItemInfoDTO(); itemInfoDTO.setAppId("testApp"); itemInfoDTO.setClusterName("default"); itemInfoDTO.setNamespaceName("application"); itemInfoDTO.setKey("k1"); itemInfoDTO.setValue("v1"); String itemKey = "k1"; String itemValue = "v1"; Page ExpectedItemInfoDTOSByKeyAndValue = itemService.getItemInfoBySearch(itemKey, itemValue, PageRequest.of(0, 200)); Page ExpectedItemInfoDTOSByKey = itemService.getItemInfoBySearch(itemKey, "", PageRequest.of(0, 200)); Page ExpectedItemInfoDTOSByValue = itemService.getItemInfoBySearch("", itemValue, PageRequest.of(0, 200)); Assert.assertEquals(itemInfoDTO.toString(), ExpectedItemInfoDTOSByKeyAndValue.getContent().get(0).toString()); Assert.assertEquals(itemInfoDTO.toString(), ExpectedItemInfoDTOSByKey.getContent().get(0).toString()); Assert.assertEquals(itemInfoDTO.toString(), ExpectedItemInfoDTOSByValue.getContent().get(0).toString()); } private Item createItem(long namespaceId, String key, String value, int type) { Item item = new Item(); item.setNamespaceId(namespaceId); item.setKey(key); item.setValue(value); item.setType(type); item.setComment(""); item.setLineNum(3); return item; } } ================================================ FILE: apollo-biz/src/test/java/com/ctrip/framework/apollo/biz/service/ItemSetServiceTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.service; import static org.mockito.Mockito.when; import com.ctrip.framework.apollo.biz.AbstractIntegrationTest; import com.ctrip.framework.apollo.biz.config.BizConfig; import com.ctrip.framework.apollo.biz.entity.Item; import com.ctrip.framework.apollo.biz.entity.Namespace; import com.ctrip.framework.apollo.common.dto.ItemChangeSets; import com.ctrip.framework.apollo.common.dto.ItemDTO; import com.ctrip.framework.apollo.common.exception.BadRequestException; import org.junit.Assert; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.context.jdbc.Sql; public class ItemSetServiceTest extends AbstractIntegrationTest { @MockBean private BizConfig bizConfig; @Autowired private ItemService itemService; @Autowired private NamespaceService namespaceService; @Autowired private ItemSetService itemSetService; @Test @Sql(scripts = "/sql/itemset-test.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/sql/clean.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testUpdateSetWithoutItemNumLimit() { when(bizConfig.itemKeyLengthLimit()).thenReturn(128); when(bizConfig.itemValueLengthLimit()).thenReturn(20000); when(bizConfig.isItemNumLimitEnabled()).thenReturn(false); when(bizConfig.itemNumLimit()).thenReturn(5); Namespace namespace = namespaceService.findOne(1L); ItemChangeSets changeSets = new ItemChangeSets(); changeSets.addCreateItem(buildNormalItem(0L, namespace.getId(), "k6", "v6", "test item num limit", 6)); changeSets.addCreateItem(buildNormalItem(0L, namespace.getId(), "k7", "v7", "test item num limit", 7)); try { itemSetService.updateSet(namespace, changeSets); } catch (Exception e) { Assert.fail(); } int size = itemService.findNonEmptyItemCount(namespace.getId()); Assert.assertEquals(7, size); } @Test @Sql(scripts = "/sql/itemset-test.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/sql/clean.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testUpdateSetWithItemNumLimit() { when(bizConfig.itemKeyLengthLimit()).thenReturn(128); when(bizConfig.itemValueLengthLimit()).thenReturn(20000); when(bizConfig.isItemNumLimitEnabled()).thenReturn(true); when(bizConfig.itemNumLimit()).thenReturn(5); Namespace namespace = namespaceService.findOne(1L); Item item9901 = itemService.findOne(9901); Item item9902 = itemService.findOne(9902); ItemChangeSets changeSets = new ItemChangeSets(); changeSets.addUpdateItem(buildNormalItem(item9901.getId(), item9901.getNamespaceId(), item9901.getKey(), item9901.getValue() + " update", item9901.getComment(), item9901.getLineNum())); changeSets.addDeleteItem(buildNormalItem(item9902.getId(), item9902.getNamespaceId(), item9902.getKey(), item9902.getValue() + " update", item9902.getComment(), item9902.getLineNum())); changeSets.addCreateItem(buildNormalItem(0L, item9901.getNamespaceId(), "k6", "v6", "test item num limit", 6)); changeSets.addCreateItem(buildNormalItem(0L, item9901.getNamespaceId(), "k7", "v7", "test item num limit", 7)); try { itemSetService.updateSet(namespace, changeSets); Assert.fail(); } catch (Exception e) { Assert.assertTrue(e instanceof BadRequestException); } int size = itemService.findNonEmptyItemCount(namespace.getId()); Assert.assertEquals(5, size); } @Test @Sql(scripts = "/sql/itemset-test.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/sql/clean.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testUpdateSetWithItemNumLimit2() { when(bizConfig.itemKeyLengthLimit()).thenReturn(128); when(bizConfig.itemValueLengthLimit()).thenReturn(20000); when(bizConfig.isItemNumLimitEnabled()).thenReturn(true); when(bizConfig.itemNumLimit()).thenReturn(5); Namespace namespace = namespaceService.findOne(1L); Item item9901 = itemService.findOne(9901); Item item9902 = itemService.findOne(9902); ItemChangeSets changeSets = new ItemChangeSets(); changeSets.addUpdateItem(buildNormalItem(item9901.getId(), item9901.getNamespaceId(), item9901.getKey(), item9901.getValue() + " update", item9901.getComment(), item9901.getLineNum())); changeSets.addDeleteItem(buildNormalItem(item9902.getId(), item9902.getNamespaceId(), item9902.getKey(), item9902.getValue() + " update", item9902.getComment(), item9902.getLineNum())); changeSets.addCreateItem(buildNormalItem(0L, item9901.getNamespaceId(), "k6", "v6", "test item num limit", 6)); try { itemSetService.updateSet(namespace, changeSets); } catch (Exception e) { Assert.fail(); } int size = itemService.findNonEmptyItemCount(namespace.getId()); Assert.assertEquals(5, size); } private ItemDTO buildNormalItem(Long id, Long namespaceId, String key, String value, String comment, int lineNum) { ItemDTO item = new ItemDTO(key, value, comment, lineNum); item.setId(id); item.setNamespaceId(namespaceId); return item; } } ================================================ FILE: apollo-biz/src/test/java/com/ctrip/framework/apollo/biz/service/NamespaceBranchServiceTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.service; import com.ctrip.framework.apollo.biz.AbstractIntegrationTest; import com.ctrip.framework.apollo.biz.entity.GrayReleaseRule; import com.ctrip.framework.apollo.biz.entity.Namespace; import com.ctrip.framework.apollo.biz.entity.ReleaseHistory; import com.ctrip.framework.apollo.common.constants.NamespaceBranchStatus; import com.ctrip.framework.apollo.common.constants.ReleaseOperation; import com.ctrip.framework.apollo.common.utils.GrayReleaseRuleItemTransformer; import com.ctrip.framework.apollo.common.dto.GrayReleaseRuleItemDTO; import java.lang.reflect.Type; import java.util.Set; import java.util.Map; import org.junit.Assert; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.test.context.jdbc.Sql; import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; public class NamespaceBranchServiceTest extends AbstractIntegrationTest { @Autowired private NamespaceBranchService namespaceBranchService; @Autowired private ReleaseHistoryService releaseHistoryService; private String testApp = "test"; private String testCluster = "default"; private String testNamespace = "application"; private String testBranchName = "child-cluster"; private String operator = "apollo"; private Pageable pageable = PageRequest.of(0, 10); @Test @Sql(scripts = "/sql/namespace-branch-test.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/sql/clean.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testFindBranch() { Namespace branch = namespaceBranchService.findBranch(testApp, testCluster, testNamespace); Assert.assertNotNull(branch); Assert.assertEquals(testBranchName, branch.getClusterName()); } @Test @Sql(scripts = "/sql/namespace-branch-test.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/sql/clean.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testUpdateBranchGrayRulesWithUpdateOnce() { GrayReleaseRule rule = instanceGrayReleaseRule(); namespaceBranchService.updateBranchGrayRules(testApp, testCluster, testNamespace, testBranchName, rule); GrayReleaseRule activeRule = namespaceBranchService.findBranchGrayRules(testApp, testCluster, testNamespace, testBranchName); Assert.assertNotNull(activeRule); Assert.assertEquals(rule.getAppId(), activeRule.getAppId()); Assert.assertEquals(rule.getRules(), activeRule.getRules()); Assert.assertEquals(Long.valueOf(0), activeRule.getReleaseId()); Page releaseHistories = releaseHistoryService .findReleaseHistoriesByNamespace(testApp, testCluster, testNamespace, pageable); ReleaseHistory releaseHistory = releaseHistories.getContent().get(0); Assert.assertEquals(1, releaseHistories.getTotalElements()); Assert.assertEquals(ReleaseOperation.APPLY_GRAY_RULES, releaseHistory.getOperation()); Assert.assertEquals(0, releaseHistory.getReleaseId()); Assert.assertEquals(0, releaseHistory.getPreviousReleaseId()); Assert.assertTrue(containRules(releaseHistory.getOperationContext(), rule.getRules())); } @Test @Sql(scripts = "/sql/namespace-branch-test.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/sql/clean.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testUpdateBranchGrayRulesWithUpdateTwice() { GrayReleaseRule firstRule = instanceGrayReleaseRule(); namespaceBranchService.updateBranchGrayRules(testApp, testCluster, testNamespace, testBranchName, firstRule); GrayReleaseRule secondRule = instanceGrayReleaseRule(); secondRule.setRules( "[{\"clientAppId\":\"branch-test\",\"clientIpList\":[\"10.38.57.112\"],\"clientLabelList\":[\"branch-test\"]}]"); namespaceBranchService.updateBranchGrayRules(testApp, testCluster, testNamespace, testBranchName, secondRule); GrayReleaseRule activeRule = namespaceBranchService.findBranchGrayRules(testApp, testCluster, testNamespace, testBranchName); Assert.assertNotNull(secondRule); Assert.assertEquals(secondRule.getAppId(), activeRule.getAppId()); Assert.assertEquals(secondRule.getRules(), activeRule.getRules()); Assert.assertEquals(Long.valueOf(0), activeRule.getReleaseId()); Page releaseHistories = releaseHistoryService .findReleaseHistoriesByNamespace(testApp, testCluster, testNamespace, pageable); ReleaseHistory firstReleaseHistory = releaseHistories.getContent().get(1); ReleaseHistory secondReleaseHistory = releaseHistories.getContent().get(0); Assert.assertEquals(2, releaseHistories.getTotalElements()); Assert.assertEquals(ReleaseOperation.APPLY_GRAY_RULES, firstReleaseHistory.getOperation()); Assert.assertEquals(ReleaseOperation.APPLY_GRAY_RULES, secondReleaseHistory.getOperation()); Assert .assertTrue(containRules(firstReleaseHistory.getOperationContext(), firstRule.getRules())); Assert.assertFalse( containRules(firstReleaseHistory.getOperationContext(), secondRule.getRules())); Assert .assertTrue(containRules(secondReleaseHistory.getOperationContext(), firstRule.getRules())); Assert.assertTrue( containRules(secondReleaseHistory.getOperationContext(), secondRule.getRules())); } private boolean containRules(String context, String rules) { Type grayReleaseRuleItemsType = new TypeToken>>() {}.getType(); Map> contextRulesMap = new Gson().fromJson(context, grayReleaseRuleItemsType); Set ruleSet = GrayReleaseRuleItemTransformer.batchTransformFromJSON(rules); for (GrayReleaseRuleItemDTO rule : ruleSet) { boolean found = false; loop: for (Set contextRules : contextRulesMap.values()) { for (GrayReleaseRuleItemDTO contextRule : contextRules) { if (contextRule.toString().equals(rule.toString())) { found = true; break loop; } } } if (!found) { return false; } } return true; } @Test @Sql(scripts = "/sql/namespace-branch-test.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/sql/clean.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testUpdateRulesReleaseIdWithOldRuleNotExist() { long latestReleaseId = 100; namespaceBranchService.updateRulesReleaseId(testApp, testCluster, testNamespace, testBranchName, latestReleaseId, operator); GrayReleaseRule activeRule = namespaceBranchService.findBranchGrayRules(testApp, testCluster, testNamespace, testBranchName); Assert.assertNull(activeRule); } @Test @Sql(scripts = "/sql/namespace-branch-test.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/sql/clean.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testUpdateRulesReleaseIdWithOldRuleExist() { GrayReleaseRule rule = instanceGrayReleaseRule(); namespaceBranchService.updateBranchGrayRules(testApp, testCluster, testNamespace, testBranchName, rule); long latestReleaseId = 100; namespaceBranchService.updateRulesReleaseId(testApp, testCluster, testNamespace, testBranchName, latestReleaseId, operator); GrayReleaseRule activeRule = namespaceBranchService.findBranchGrayRules(testApp, testCluster, testNamespace, testBranchName); Assert.assertNotNull(activeRule); Assert.assertEquals(Long.valueOf(latestReleaseId), activeRule.getReleaseId()); Assert.assertEquals(rule.getRules(), activeRule.getRules()); Assert.assertEquals(NamespaceBranchStatus.ACTIVE, activeRule.getBranchStatus()); } @Test @Sql(scripts = "/sql/namespace-branch-test.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/sql/clean.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testDeleteBranch() { GrayReleaseRule rule = instanceGrayReleaseRule(); namespaceBranchService.updateBranchGrayRules(testApp, testCluster, testNamespace, testBranchName, rule); namespaceBranchService.deleteBranch(testApp, testCluster, testNamespace, testBranchName, NamespaceBranchStatus.DELETED, operator); Namespace branch = namespaceBranchService.findBranch(testApp, testCluster, testNamespace); Assert.assertNull(branch); GrayReleaseRule latestRule = namespaceBranchService.findBranchGrayRules(testApp, testCluster, testNamespace, testBranchName); Assert.assertNotNull(latestRule); Assert.assertEquals(NamespaceBranchStatus.DELETED, latestRule.getBranchStatus()); Assert.assertEquals("[]", latestRule.getRules()); Page releaseHistories = releaseHistoryService .findReleaseHistoriesByNamespace(testApp, testCluster, testNamespace, pageable); ReleaseHistory firstReleaseHistory = releaseHistories.getContent().get(1); ReleaseHistory secondReleaseHistory = releaseHistories.getContent().get(0); Assert.assertEquals(2, releaseHistories.getTotalElements()); Assert.assertEquals(ReleaseOperation.APPLY_GRAY_RULES, firstReleaseHistory.getOperation()); Assert.assertEquals(ReleaseOperation.ABANDON_GRAY_RELEASE, secondReleaseHistory.getOperation()); } private GrayReleaseRule instanceGrayReleaseRule() { GrayReleaseRule rule = new GrayReleaseRule(); rule.setAppId(testApp); rule.setClusterName(testCluster); rule.setNamespaceName(testNamespace); rule.setBranchName(testBranchName); rule.setBranchStatus(NamespaceBranchStatus.ACTIVE); rule.setRules( "[{\"clientAppId\":\"test\",\"clientIpList\":[\"1.0.0.4\"],\"clientLabelList\":[]}]"); return rule; } } ================================================ FILE: apollo-biz/src/test/java/com/ctrip/framework/apollo/biz/service/NamespacePublishInfoTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.service; import com.ctrip.framework.apollo.biz.AbstractUnitTest; import com.ctrip.framework.apollo.biz.entity.Cluster; import com.ctrip.framework.apollo.biz.entity.Item; import com.ctrip.framework.apollo.biz.entity.Namespace; import com.ctrip.framework.apollo.biz.entity.Release; import com.ctrip.framework.apollo.biz.repository.NamespaceRepository; import com.ctrip.framework.apollo.core.ConfigConsts; import org.junit.Assert; import org.junit.Test; import org.mockito.InjectMocks; import org.mockito.Mock; import java.util.Collections; import java.util.Map; import java.util.Random; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.Mockito.when; public class NamespacePublishInfoTest extends AbstractUnitTest { @Mock private ClusterService clusterService; @Mock private ReleaseService releaseService; @Mock private ItemService itemService; @Mock private NamespaceRepository namespaceRepository; @InjectMocks private NamespaceService namespaceService; private String testApp = "testApp"; @Test public void testNamespaceNotEverPublishedButHasItems() { Cluster cluster = createCluster(ConfigConsts.CLUSTER_NAME_DEFAULT); Namespace namespace = createNamespace(ConfigConsts.CLUSTER_NAME_DEFAULT, ConfigConsts.NAMESPACE_APPLICATION); Item item = createItem(namespace.getId(), "a", "b"); when(clusterService.findParentClusters(testApp)).thenReturn(Collections.singletonList(cluster)); when(namespaceRepository.findByAppIdAndClusterNameOrderByIdAsc(testApp, ConfigConsts.CLUSTER_NAME_DEFAULT)).thenReturn(Collections.singletonList(namespace)); when(itemService.findLastOne(anyLong())).thenReturn(item); Map result = namespaceService.namespacePublishInfo(testApp); Assert.assertEquals(1, result.size()); Assert.assertTrue(result.get(ConfigConsts.CLUSTER_NAME_DEFAULT)); } @Test public void testNamespaceEverPublishedAndNotModifiedAfter() { Cluster cluster = createCluster(ConfigConsts.CLUSTER_NAME_DEFAULT); Namespace namespace = createNamespace(ConfigConsts.CLUSTER_NAME_DEFAULT, ConfigConsts.NAMESPACE_APPLICATION); Item item = createItem(namespace.getId(), "a", "b"); Release release = createRelease("{\"a\":\"b\"}"); when(clusterService.findParentClusters(testApp)).thenReturn(Collections.singletonList(cluster)); when(namespaceRepository.findByAppIdAndClusterNameOrderByIdAsc(testApp, ConfigConsts.CLUSTER_NAME_DEFAULT)).thenReturn(Collections.singletonList(namespace)); when(releaseService.findLatestActiveRelease(namespace)).thenReturn(release); when(itemService.findItemsModifiedAfterDate(anyLong(), any())) .thenReturn(Collections.singletonList(item)); Map result = namespaceService.namespacePublishInfo(testApp); Assert.assertEquals(1, result.size()); Assert.assertFalse(result.get(ConfigConsts.CLUSTER_NAME_DEFAULT)); } @Test public void testNamespaceEverPublishedAndModifiedAfter() { Cluster cluster = createCluster(ConfigConsts.CLUSTER_NAME_DEFAULT); Namespace namespace = createNamespace(ConfigConsts.CLUSTER_NAME_DEFAULT, ConfigConsts.NAMESPACE_APPLICATION); Item item = createItem(namespace.getId(), "a", "b"); Release release = createRelease("{\"a\":\"c\"}"); when(clusterService.findParentClusters(testApp)).thenReturn(Collections.singletonList(cluster)); when(namespaceRepository.findByAppIdAndClusterNameOrderByIdAsc(testApp, ConfigConsts.CLUSTER_NAME_DEFAULT)).thenReturn(Collections.singletonList(namespace)); when(releaseService.findLatestActiveRelease(namespace)).thenReturn(release); when(itemService.findItemsModifiedAfterDate(anyLong(), any())) .thenReturn(Collections.singletonList(item)); Map result = namespaceService.namespacePublishInfo(testApp); Assert.assertEquals(1, result.size()); Assert.assertTrue(result.get(ConfigConsts.CLUSTER_NAME_DEFAULT)); } private Cluster createCluster(String clusterName) { Cluster cluster = new Cluster(); cluster.setAppId(testApp); cluster.setName(clusterName); cluster.setParentClusterId(0); return cluster; } private Namespace createNamespace(String clusterName, String namespaceName) { Namespace namespace = new Namespace(); namespace.setAppId(testApp); namespace.setClusterName(clusterName); namespace.setNamespaceName(namespaceName); namespace.setId(new Random().nextLong()); return namespace; } private Item createItem(long namespaceId, String key, String value) { Item item = new Item(); item.setNamespaceId(namespaceId); item.setKey(key); item.setValue(value); return item; } private Release createRelease(String configuration) { Release release = new Release(); release.setConfigurations(configuration); return release; } } ================================================ FILE: apollo-biz/src/test/java/com/ctrip/framework/apollo/biz/service/NamespaceServiceIntegrationTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.service; import com.ctrip.framework.apollo.biz.AbstractIntegrationTest; import com.ctrip.framework.apollo.biz.config.BizConfig; import com.ctrip.framework.apollo.biz.entity.Cluster; import com.ctrip.framework.apollo.biz.entity.Commit; import com.ctrip.framework.apollo.biz.entity.InstanceConfig; import com.ctrip.framework.apollo.biz.entity.Item; import com.ctrip.framework.apollo.biz.entity.Namespace; import com.ctrip.framework.apollo.biz.entity.Release; import com.ctrip.framework.apollo.biz.entity.ReleaseHistory; import com.ctrip.framework.apollo.biz.repository.InstanceConfigRepository; import com.ctrip.framework.apollo.biz.repository.NamespaceRepository; import com.ctrip.framework.apollo.common.entity.AppNamespace; import com.ctrip.framework.apollo.common.exception.ServiceException; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Arrays; import java.util.Date; import java.util.HashSet; import org.junit.Assert; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.test.context.jdbc.Sql; import java.util.List; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.when; public class NamespaceServiceIntegrationTest extends AbstractIntegrationTest { @Autowired private NamespaceService namespaceService; @Autowired private ItemService itemService; @Autowired private CommitService commitService; @Autowired private AppNamespaceService appNamespaceService; @Autowired private ClusterService clusterService; @Autowired private ReleaseService releaseService; @Autowired private ReleaseHistoryService releaseHistoryService; @Autowired private InstanceConfigRepository instanceConfigRepository; @Autowired private NamespaceRepository namespaceRepository; @MockBean private BizConfig bizConfig; private String testApp = "testApp"; private String testCluster = "default"; private String testChildCluster = "child-cluster"; private String testPrivateNamespace = "application"; private String testUser = "apollo"; private String commitTestApp = "commitTestApp"; @Test @Sql(scripts = "/sql/namespace-test.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/sql/clean.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testDeleteNamespace() { Namespace namespace = new Namespace(); namespace.setAppId(testApp); namespace.setClusterName(testCluster); namespace.setNamespaceName(testPrivateNamespace); namespace.setId(1); namespaceService.deleteNamespace(namespace, testUser); List items = itemService.findItemsWithoutOrdered(testApp, testCluster, testPrivateNamespace); List commits = commitService.find(testApp, testCluster, testPrivateNamespace, PageRequest.of(0, 10)); AppNamespace appNamespace = appNamespaceService.findOne(testApp, testPrivateNamespace); List childClusters = clusterService.findChildClusters(testApp, testCluster); InstanceConfig instanceConfig = instanceConfigRepository.findById(1L).orElse(null); List parentNamespaceReleases = releaseService.findActiveReleases(testApp, testCluster, testPrivateNamespace, PageRequest.of(0, 10)); List childNamespaceReleases = releaseService.findActiveReleases(testApp, testChildCluster, testPrivateNamespace, PageRequest.of(0, 10)); Page releaseHistories = releaseHistoryService.findReleaseHistoriesByNamespace( testApp, testCluster, testPrivateNamespace, PageRequest.of(0, 10)); assertEquals(0, items.size()); assertEquals(0, commits.size()); assertNotNull(appNamespace); assertEquals(0, childClusters.size()); assertEquals(0, parentNamespaceReleases.size()); assertEquals(0, childNamespaceReleases.size()); assertTrue(!releaseHistories.hasContent()); assertNull(instanceConfig); } @Test @Sql(scripts = "/sql/namespace-test.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/sql/clean.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testGetCommitsByModifiedTime() throws ParseException { String format = "yyyy-MM-dd HH:mm:ss"; SimpleDateFormat simpleDateFormat = new SimpleDateFormat(format); Date lastModifiedTime = simpleDateFormat.parse("2020-08-22 09:00:00"); List commitsByDate = commitService.find(commitTestApp, testCluster, testPrivateNamespace, lastModifiedTime, null); Date lastModifiedTimeGreater = simpleDateFormat.parse("2020-08-22 11:00:00"); List commitsByDateGreater = commitService.find(commitTestApp, testCluster, testPrivateNamespace, lastModifiedTimeGreater, null); Date lastModifiedTimePage = simpleDateFormat.parse("2020-08-22 09:30:00"); List commitsByDatePage = commitService.find(commitTestApp, testCluster, testPrivateNamespace, lastModifiedTimePage, PageRequest.of(0, 1)); assertEquals(1, commitsByDate.size()); assertEquals(0, commitsByDateGreater.size()); assertEquals(1, commitsByDatePage.size()); } @Test @Sql(scripts = "/sql/namespace-test.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/sql/clean.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testNamespaceNumLimit() { when(bizConfig.isNamespaceNumLimitEnabled()).thenReturn(true); when(bizConfig.namespaceNumLimit()).thenReturn(2); Namespace namespace = new Namespace(); namespace.setAppId(testApp); namespace.setClusterName(testCluster); namespace.setNamespaceName("demo-namespace"); namespaceService.save(namespace); try { Namespace namespace2 = new Namespace(); namespace2.setAppId(testApp); namespace2.setClusterName(testCluster); namespace2.setNamespaceName("demo-namespace2"); namespaceService.save(namespace2); Assert.fail(); } catch (Exception e) { Assert.assertTrue(e instanceof ServiceException); } int nowCount = namespaceRepository.countByAppIdAndClusterName(testApp, testCluster); Assert.assertEquals(2, nowCount); } @Test @Sql(scripts = "/sql/namespace-test.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/sql/clean.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testNamespaceNumLimitFalse() { when(bizConfig.namespaceNumLimit()).thenReturn(2); Namespace namespace = new Namespace(); namespace.setAppId(testApp); namespace.setClusterName(testCluster); namespace.setNamespaceName("demo-namespace"); namespaceService.save(namespace); Namespace namespace2 = new Namespace(); namespace2.setAppId(testApp); namespace2.setClusterName(testCluster); namespace2.setNamespaceName("demo-namespace2"); namespaceService.save(namespace2); int nowCount = namespaceRepository.countByAppIdAndClusterName(testApp, testCluster); Assert.assertEquals(3, nowCount); } @Test @Sql(scripts = "/sql/namespace-test.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/sql/clean.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testNamespaceNumLimitWhite() { when(bizConfig.isNamespaceNumLimitEnabled()).thenReturn(true); when(bizConfig.namespaceNumLimit()).thenReturn(2); when(bizConfig.namespaceNumLimitWhite()).thenReturn(new HashSet<>(Arrays.asList(testApp))); Namespace namespace = new Namespace(); namespace.setAppId(testApp); namespace.setClusterName(testCluster); namespace.setNamespaceName("demo-namespace"); namespaceService.save(namespace); Namespace namespace2 = new Namespace(); namespace2.setAppId(testApp); namespace2.setClusterName(testCluster); namespace2.setNamespaceName("demo-namespace2"); namespaceService.save(namespace2); int nowCount = namespaceRepository.countByAppIdAndClusterName(testApp, testCluster); Assert.assertEquals(3, nowCount); } } ================================================ FILE: apollo-biz/src/test/java/com/ctrip/framework/apollo/biz/service/NamespaceServiceTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.service; import com.ctrip.framework.apollo.biz.AbstractUnitTest; import com.ctrip.framework.apollo.biz.MockBeanFactory; import com.ctrip.framework.apollo.biz.entity.Namespace; import com.ctrip.framework.apollo.biz.repository.NamespaceRepository; import com.ctrip.framework.apollo.common.entity.AppNamespace; import com.ctrip.framework.apollo.common.exception.BadRequestException; import com.ctrip.framework.apollo.core.ConfigConsts; import org.junit.Test; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.Spy; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import java.util.Arrays; import java.util.List; import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.when; public class NamespaceServiceTest extends AbstractUnitTest { @Mock private AppNamespaceService appNamespaceService; @Mock private NamespaceRepository namespaceRepository; @Spy @InjectMocks private NamespaceService namespaceService; private String testPublicAppNamespace = "publicAppNamespace"; @Test(expected = BadRequestException.class) public void testFindPublicAppNamespaceWithWrongNamespace() { Pageable page = PageRequest.of(0, 10); when(appNamespaceService.findPublicNamespaceByName(testPublicAppNamespace)).thenReturn(null); namespaceService.findPublicAppNamespaceAllNamespaces(testPublicAppNamespace, page); } @Test public void testFindPublicAppNamespace() { AppNamespace publicAppNamespace = MockBeanFactory.mockAppNamespace(null, testPublicAppNamespace, true); when(appNamespaceService.findPublicNamespaceByName(testPublicAppNamespace)) .thenReturn(publicAppNamespace); Namespace firstParentNamespace = MockBeanFactory.mockNamespace("app", ConfigConsts.CLUSTER_NAME_DEFAULT, testPublicAppNamespace); Namespace secondParentNamespace = MockBeanFactory.mockNamespace("app1", ConfigConsts.CLUSTER_NAME_DEFAULT, testPublicAppNamespace); Pageable page = PageRequest.of(0, 10); when(namespaceRepository.findByNamespaceName(testPublicAppNamespace, page)) .thenReturn(Arrays.asList(firstParentNamespace, secondParentNamespace)); doReturn(false).when(namespaceService).isChildNamespace(firstParentNamespace); doReturn(false).when(namespaceService).isChildNamespace(secondParentNamespace); List namespaces = namespaceService.findPublicAppNamespaceAllNamespaces(testPublicAppNamespace, page); assertEquals(2, namespaces.size()); } } ================================================ FILE: apollo-biz/src/test/java/com/ctrip/framework/apollo/biz/service/ReleaseCreationTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.service; import com.google.gson.Gson; import com.ctrip.framework.apollo.biz.AbstractIntegrationTest; import com.ctrip.framework.apollo.biz.entity.GrayReleaseRule; import com.ctrip.framework.apollo.biz.entity.Namespace; import com.ctrip.framework.apollo.biz.entity.Release; import com.ctrip.framework.apollo.biz.entity.ReleaseHistory; import com.ctrip.framework.apollo.common.constants.GsonType; import com.ctrip.framework.apollo.common.constants.ReleaseOperation; import org.junit.Assert; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.test.context.jdbc.Sql; import java.util.Map; public class ReleaseCreationTest extends AbstractIntegrationTest { private static final Gson GSON = new Gson(); @Autowired private ReleaseService releaseService; @Autowired private NamespaceBranchService namespaceBranchService; @Autowired private ReleaseHistoryService releaseHistoryService; private String testApp = "test"; private String testNamespace = "application"; private String operator = "apollo"; private Pageable pageable = PageRequest.of(0, 10); @Test @Sql(scripts = "/sql/release-creation-test.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/sql/clean.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testPublishNormalNamespace() { long namespaceId = 100; String clusterName = "only-master"; Namespace namespace = instanceNamespace(namespaceId, clusterName); releaseService.publish(namespace, "", "", operator, false); Release latestRelease = releaseService.findLatestActiveRelease(namespace); Assert.assertNotNull(latestRelease); Map configuration = parseConfiguration(latestRelease.getConfigurations()); Assert.assertEquals(3, configuration.size()); Assert.assertEquals("v1", configuration.get("k1")); Assert.assertEquals("v2", configuration.get("k2")); Assert.assertEquals("v3", configuration.get("k3")); Page releaseHistories = releaseHistoryService .findReleaseHistoriesByNamespace(testApp, clusterName, testNamespace, pageable); ReleaseHistory releaseHistory = releaseHistories.getContent().get(0); Assert.assertEquals(1, releaseHistories.getTotalElements()); Assert.assertEquals(ReleaseOperation.NORMAL_RELEASE, releaseHistory.getOperation()); Assert.assertEquals(latestRelease.getId(), releaseHistory.getReleaseId()); Assert.assertEquals(0, releaseHistory.getPreviousReleaseId()); } /** * Master | Branch * ------------------------------ Master | Branch * Items k1=v1 | ---------------------------- * k2=v2 | k1=v1 | k1=v1 * k3=v3 publish master k2=v2 | k2=v2 * ------------------------------ ===========>> Result k3=v3 | k3=v3 * Release | * | * | */ @Test @Sql(scripts = "/sql/release-creation-test.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/sql/clean.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testPublishMasterNamespaceAndBranchHasNotItems() { long parentNamespaceId = 101; String parentClusterName = "default1"; long childNamespaceId = 102; String childClusterName = "child-cluster1"; Namespace parentNamespace = instanceNamespace(parentNamespaceId, parentClusterName); releaseService.publish(parentNamespace, "", "", operator, false); Release latestParentNamespaceRelease = releaseService.findLatestActiveRelease(parentNamespace); // assert parent namespace Assert.assertNotNull(latestParentNamespaceRelease); Map parentNamespaceConfiguration = parseConfiguration(latestParentNamespaceRelease.getConfigurations()); Assert.assertEquals(3, parentNamespaceConfiguration.size()); Assert.assertEquals("v1", parentNamespaceConfiguration.get("k1")); Assert.assertEquals("v2", parentNamespaceConfiguration.get("k2")); Assert.assertEquals("v3", parentNamespaceConfiguration.get("k3")); // assert child namespace Namespace childNamespace = instanceNamespace(childNamespaceId, childClusterName); Release latestChildNamespaceRelease = releaseService.findLatestActiveRelease(childNamespace); // assert parent namespace Assert.assertNotNull(latestChildNamespaceRelease); Map childNamespaceConfiguration = parseConfiguration(latestChildNamespaceRelease.getConfigurations()); Assert.assertEquals(3, childNamespaceConfiguration.size()); Assert.assertEquals("v1", childNamespaceConfiguration.get("k1")); Assert.assertEquals("v2", childNamespaceConfiguration.get("k2")); Assert.assertEquals("v3", childNamespaceConfiguration.get("k3")); GrayReleaseRule rule = namespaceBranchService.findBranchGrayRules(testApp, parentClusterName, testNamespace, childClusterName); Assert.assertNotNull(rule); Assert.assertEquals(1, rule.getBranchStatus()); Assert.assertEquals(Long.valueOf(latestChildNamespaceRelease.getId()), rule.getReleaseId()); // assert release history Page releaseHistories = releaseHistoryService .findReleaseHistoriesByNamespace(testApp, parentClusterName, testNamespace, pageable); ReleaseHistory masterReleaseHistory = releaseHistories.getContent().get(1); ReleaseHistory branchReleaseHistory = releaseHistories.getContent().get(0); Assert.assertEquals(2, releaseHistories.getTotalElements()); Assert.assertEquals(ReleaseOperation.NORMAL_RELEASE, masterReleaseHistory.getOperation()); Assert.assertEquals(latestParentNamespaceRelease.getId(), masterReleaseHistory.getReleaseId()); Assert.assertEquals(0, masterReleaseHistory.getPreviousReleaseId()); Assert.assertEquals(ReleaseOperation.MASTER_NORMAL_RELEASE_MERGE_TO_GRAY, branchReleaseHistory.getOperation()); Assert.assertEquals(latestChildNamespaceRelease.getId(), branchReleaseHistory.getReleaseId()); Assert.assertTrue(branchReleaseHistory.getOperationContext() .contains(String.format("\"baseReleaseId\":%d", latestParentNamespaceRelease.getId()))); Assert.assertTrue(branchReleaseHistory.getOperationContext().contains(rule.getRules())); } /** * Master | Branch * ------------------------------ Master | Branch * Items k1=v1 | k1=v1-2 ------------------------- * k2=v2 | k1=v1 | k1=v1 * k3=v3 publish master k2=v2 | k2=v2 * ------------------------------ ===========>> Result k3=v3 | k3=v3 * Release | * | * | */ @Test @Sql(scripts = "/sql/release-creation-test.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/sql/clean.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testPublishMasterNamespaceAndBranchHasItems() { long parentNamespaceId = 103; String parentClusterName = "default2"; long childNamespaceId = 104; String childClusterName = "child-cluster2"; Namespace parentNamespace = instanceNamespace(parentNamespaceId, parentClusterName); releaseService.publish(parentNamespace, "", "", operator, false); Release latestParentNamespaceRelease = releaseService.findLatestActiveRelease(parentNamespace); // assert parent namespace Assert.assertNotNull(latestParentNamespaceRelease); Map parentNamespaceConfiguration = parseConfiguration(latestParentNamespaceRelease.getConfigurations()); Assert.assertEquals(3, parentNamespaceConfiguration.size()); Assert.assertEquals("v1", parentNamespaceConfiguration.get("k1")); Assert.assertEquals("v2", parentNamespaceConfiguration.get("k2")); Assert.assertEquals("v3", parentNamespaceConfiguration.get("k3")); // assert child namespace Namespace childNamespace = instanceNamespace(childNamespaceId, childClusterName); Release latestChildNamespaceRelease = releaseService.findLatestActiveRelease(childNamespace); Assert.assertNotNull(latestChildNamespaceRelease); Map childNamespaceConfiguration = parseConfiguration(latestChildNamespaceRelease.getConfigurations()); Assert.assertEquals(3, childNamespaceConfiguration.size()); Assert.assertEquals("v1", childNamespaceConfiguration.get("k1")); Assert.assertEquals("v2", childNamespaceConfiguration.get("k2")); Assert.assertEquals("v3", childNamespaceConfiguration.get("k3")); GrayReleaseRule rule = namespaceBranchService.findBranchGrayRules(testApp, parentClusterName, testNamespace, childClusterName); Assert.assertNotNull(rule); Assert.assertEquals(1, rule.getBranchStatus()); Assert.assertEquals(Long.valueOf(latestChildNamespaceRelease.getId()), rule.getReleaseId()); // assert release history Page releaseHistories = releaseHistoryService .findReleaseHistoriesByNamespace(testApp, parentClusterName, testNamespace, pageable); ReleaseHistory masterReleaseHistory = releaseHistories.getContent().get(1); ReleaseHistory branchReleaseHistory = releaseHistories.getContent().get(0); Assert.assertEquals(2, releaseHistories.getTotalElements()); Assert.assertEquals(ReleaseOperation.NORMAL_RELEASE, masterReleaseHistory.getOperation()); Assert.assertEquals(latestParentNamespaceRelease.getId(), masterReleaseHistory.getReleaseId()); Assert.assertEquals(0, masterReleaseHistory.getPreviousReleaseId()); Assert.assertEquals(ReleaseOperation.MASTER_NORMAL_RELEASE_MERGE_TO_GRAY, branchReleaseHistory.getOperation()); Assert.assertEquals(latestChildNamespaceRelease.getId(), branchReleaseHistory.getReleaseId()); Assert.assertTrue(branchReleaseHistory.getOperationContext() .contains(String.format("\"baseReleaseId\":%d", latestParentNamespaceRelease.getId()))); Assert.assertTrue(branchReleaseHistory.getOperationContext().contains(rule.getRules())); } /** * Master | Branch * ------------------------------ Master | Branch * Items k1=v1 | k1=v1-2 ---------------------------- * k2=v2-2 | publish master k1=v1 | k1=v1-1 * ------------------------------ ===========>> Result k2=v2-2 | k2=v2-2 * Release k1=v1 | k1=v1-1 | * k2=v2 | k2=v3 * k3=v3 | k3=v3 */ @Test @Sql(scripts = "/sql/release-creation-test.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/sql/clean.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testModifyMasterNamespaceItemsAndBranchAlsoModify() { long parentNamespaceId = 105; String parentClusterName = "default3"; long childNamespaceId = 106; String childClusterName = "child-cluster3"; Namespace parentNamespace = instanceNamespace(parentNamespaceId, parentClusterName); releaseService.publish(parentNamespace, "", "", operator, false); Release latestParentNamespaceRelease = releaseService.findLatestActiveRelease(parentNamespace); // assert parent namespace Assert.assertNotNull(latestParentNamespaceRelease); Map parentNamespaceConfiguration = parseConfiguration(latestParentNamespaceRelease.getConfigurations()); Assert.assertEquals(2, parentNamespaceConfiguration.size()); Assert.assertEquals("v1", parentNamespaceConfiguration.get("k1")); Assert.assertEquals("v2-2", parentNamespaceConfiguration.get("k2")); // assert child namespace Namespace childNamespace = instanceNamespace(childNamespaceId, childClusterName); Release latestChildNamespaceRelease = releaseService.findLatestActiveRelease(childNamespace); Assert.assertNotNull(latestChildNamespaceRelease); Map childNamespaceConfiguration = parseConfiguration(latestChildNamespaceRelease.getConfigurations()); Assert.assertEquals(2, childNamespaceConfiguration.size()); Assert.assertEquals("v1-1", childNamespaceConfiguration.get("k1")); Assert.assertEquals("v2-2", childNamespaceConfiguration.get("k2")); GrayReleaseRule rule = namespaceBranchService.findBranchGrayRules(testApp, parentClusterName, testNamespace, childClusterName); Assert.assertNotNull(rule); Assert.assertEquals(1, rule.getBranchStatus()); Assert.assertEquals(Long.valueOf(latestChildNamespaceRelease.getId()), rule.getReleaseId()); // assert release history Page releaseHistories = releaseHistoryService .findReleaseHistoriesByNamespace(testApp, parentClusterName, testNamespace, pageable); ReleaseHistory masterReleaseHistory = releaseHistories.getContent().get(1); ReleaseHistory branchReleaseHistory = releaseHistories.getContent().get(0); Assert.assertEquals(2, releaseHistories.getTotalElements()); Assert.assertEquals(ReleaseOperation.NORMAL_RELEASE, masterReleaseHistory.getOperation()); Assert.assertEquals(latestParentNamespaceRelease.getId(), masterReleaseHistory.getReleaseId()); Assert.assertEquals(1, masterReleaseHistory.getPreviousReleaseId()); Assert.assertEquals(ReleaseOperation.MASTER_NORMAL_RELEASE_MERGE_TO_GRAY, branchReleaseHistory.getOperation()); Assert.assertEquals(latestChildNamespaceRelease.getId(), branchReleaseHistory.getReleaseId()); Assert.assertEquals(2, branchReleaseHistory.getPreviousReleaseId()); Assert.assertTrue(branchReleaseHistory.getOperationContext() .contains(String.format("\"baseReleaseId\":%d", latestParentNamespaceRelease.getId()))); Assert.assertTrue(branchReleaseHistory.getOperationContext().contains(rule.getRules())); } /** * Master | Branch * ------------------------------ Master | Branch * Items k1=v1 | k1=v1-2 ---------------------------- * k2=v2-2 | k4=v4 publish branch k1=v1 | k1=v1-2 * ------------------------------ ===========>> Result k2=v2 | k2=v2 * Release k1=v1 | k3=v3 | k3=v3 * k2=v2 | | k4=v4 * k3=v3 | */ @Test @Sql(scripts = "/sql/release-creation-test.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/sql/clean.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testPublishBranchAtFirstTime() { long parentNamespaceId = 107; String parentClusterName = "default4"; long childNamespaceId = 108; String childClusterName = "child-cluster4"; // assert child namespace Namespace childNamespace = instanceNamespace(childNamespaceId, childClusterName); releaseService.publish(childNamespace, "", "", operator, false); Release latestChildNamespaceRelease = releaseService.findLatestActiveRelease(childNamespace); Assert.assertNotNull(latestChildNamespaceRelease); Map childNamespaceConfiguration = parseConfiguration(latestChildNamespaceRelease.getConfigurations()); Assert.assertEquals(4, childNamespaceConfiguration.size()); Assert.assertEquals("v1-2", childNamespaceConfiguration.get("k1")); Assert.assertEquals("v2", childNamespaceConfiguration.get("k2")); Assert.assertEquals("v3", childNamespaceConfiguration.get("k3")); Assert.assertEquals("v4", childNamespaceConfiguration.get("k4")); Namespace parentNamespace = instanceNamespace(parentNamespaceId, parentClusterName); Release latestParentNamespaceRelease = releaseService.findLatestActiveRelease(parentNamespace); // assert parent namespace Assert.assertNotNull(latestParentNamespaceRelease); Map parentNamespaceConfiguration = parseConfiguration(latestParentNamespaceRelease.getConfigurations()); Assert.assertEquals(3, parentNamespaceConfiguration.size()); Assert.assertEquals("v1", parentNamespaceConfiguration.get("k1")); Assert.assertEquals("v2", parentNamespaceConfiguration.get("k2")); Assert.assertEquals("v3", parentNamespaceConfiguration.get("k3")); GrayReleaseRule rule = namespaceBranchService.findBranchGrayRules(testApp, parentClusterName, testNamespace, childClusterName); Assert.assertNotNull(rule); Assert.assertEquals(1, rule.getBranchStatus()); Assert.assertEquals(Long.valueOf(latestChildNamespaceRelease.getId()), rule.getReleaseId()); // assert release history Page releaseHistories = releaseHistoryService .findReleaseHistoriesByNamespace(testApp, parentClusterName, testNamespace, pageable); ReleaseHistory branchReleaseHistory = releaseHistories.getContent().get(0); Assert.assertEquals(1, releaseHistories.getTotalElements()); Assert.assertEquals(ReleaseOperation.GRAY_RELEASE, branchReleaseHistory.getOperation()); Assert.assertEquals(latestChildNamespaceRelease.getId(), branchReleaseHistory.getReleaseId()); Assert.assertEquals(0, branchReleaseHistory.getPreviousReleaseId()); Assert.assertTrue(branchReleaseHistory.getOperationContext().contains("\"baseReleaseId\":3")); Assert.assertTrue(branchReleaseHistory.getOperationContext().contains(rule.getRules())); } /** * Master | Branch * ------------------------------ Master | Branch * Items k1=v1 | k1=v1-2 ---------------------------- * k2=v2-2 | k4=v4 k1=v1 | k1=v1-2 * k6=v6 publish branch k2=v2 | k2=v2 * ------------------------------ ===========>> Result k3=v3 | k3=v3 * Release k1=v1 | k1=v1-1 | k4=v4 * k2=v2 | k2=v2 | k6=v6 * k3=v3 | k3=v3 * | k4=v4 * | k5=v5 */ @Test @Sql(scripts = "/sql/release-creation-test.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/sql/clean.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testPublishBranch() { long parentNamespaceId = 109; String parentClusterName = "default5"; long childNamespaceId = 1010; String childClusterName = "child-cluster5"; // assert child namespace Namespace childNamespace = instanceNamespace(childNamespaceId, childClusterName); releaseService.publish(childNamespace, "", "", operator, false); Release latestChildNamespaceRelease = releaseService.findLatestActiveRelease(childNamespace); Assert.assertNotNull(latestChildNamespaceRelease); Map childNamespaceConfiguration = parseConfiguration(latestChildNamespaceRelease.getConfigurations()); Assert.assertEquals(5, childNamespaceConfiguration.size()); Assert.assertEquals("v1-2", childNamespaceConfiguration.get("k1")); Assert.assertEquals("v2", childNamespaceConfiguration.get("k2")); Assert.assertEquals("v3", childNamespaceConfiguration.get("k3")); Assert.assertEquals("v4", childNamespaceConfiguration.get("k4")); Assert.assertEquals("v6", childNamespaceConfiguration.get("k6")); Namespace parentNamespace = instanceNamespace(parentNamespaceId, parentClusterName); Release latestParentNamespaceRelease = releaseService.findLatestActiveRelease(parentNamespace); // assert parent namespace Assert.assertNotNull(latestParentNamespaceRelease); Map parentNamespaceConfiguration = parseConfiguration(latestParentNamespaceRelease.getConfigurations()); Assert.assertEquals(3, parentNamespaceConfiguration.size()); Assert.assertEquals("v1", parentNamespaceConfiguration.get("k1")); Assert.assertEquals("v2", parentNamespaceConfiguration.get("k2")); Assert.assertEquals("v3", parentNamespaceConfiguration.get("k3")); GrayReleaseRule rule = namespaceBranchService.findBranchGrayRules(testApp, parentClusterName, testNamespace, childClusterName); Assert.assertNotNull(rule); Assert.assertEquals(1, rule.getBranchStatus()); Assert.assertEquals(Long.valueOf(latestChildNamespaceRelease.getId()), rule.getReleaseId()); // assert release history Page releaseHistories = releaseHistoryService .findReleaseHistoriesByNamespace(testApp, parentClusterName, testNamespace, pageable); ReleaseHistory branchReleaseHistory = releaseHistories.getContent().get(0); Assert.assertEquals(1, releaseHistories.getTotalElements()); Assert.assertEquals(ReleaseOperation.GRAY_RELEASE, branchReleaseHistory.getOperation()); Assert.assertEquals(latestChildNamespaceRelease.getId(), branchReleaseHistory.getReleaseId()); Assert.assertEquals(5, branchReleaseHistory.getPreviousReleaseId()); Assert.assertTrue(branchReleaseHistory.getOperationContext().contains("\"baseReleaseId\":4")); Assert.assertTrue(branchReleaseHistory.getOperationContext().contains(rule.getRules())); } /** * Master | Branch * ------------------------------ Master | Branch * Rollback Release k1=v1 | k1=v1-2 ---------------------------- * k2=v2 | k2=v2 k1=v1-1 | k1=v1-2 * | k3=v3 k2=v2-1 | k2=v2-1 * rollback k3=v3 | k3=v3 * ------------------------------ ===========>> New Release * New Release k1=v1-1 | * k2=v2-1 | * k3=v3 | * * */ @Test @Sql(scripts = "/sql/release-creation-test.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/sql/clean.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testRollback() { String parentClusterName = "default6"; String childClusterName = "child-cluster6"; String operator = "apollo"; Release parentNamespaceLatestRelease = releaseService.findLatestActiveRelease(testApp, parentClusterName, testNamespace); releaseService.rollback(parentNamespaceLatestRelease.getId(), operator); Release parentNamespaceNewLatestRelease = releaseService.findLatestActiveRelease(testApp, parentClusterName, testNamespace); Map parentNamespaceConfiguration = parseConfiguration(parentNamespaceNewLatestRelease.getConfigurations()); Assert.assertEquals(3, parentNamespaceConfiguration.size()); Assert.assertEquals("v1-1", parentNamespaceConfiguration.get("k1")); Assert.assertEquals("v2-1", parentNamespaceConfiguration.get("k2")); Assert.assertEquals("v3", parentNamespaceConfiguration.get("k3")); Release childNamespaceNewLatestRelease = releaseService.findLatestActiveRelease(testApp, childClusterName, testNamespace); Map childNamespaceConfiguration = parseConfiguration(childNamespaceNewLatestRelease.getConfigurations()); Assert.assertEquals(3, childNamespaceConfiguration.size()); Assert.assertEquals("v1-2", childNamespaceConfiguration.get("k1")); Assert.assertEquals("v2-1", childNamespaceConfiguration.get("k2")); Assert.assertEquals("v3", childNamespaceConfiguration.get("k3")); // assert release history Page releaseHistories = releaseHistoryService .findReleaseHistoriesByNamespace(testApp, parentClusterName, testNamespace, pageable); ReleaseHistory masterReleaseHistory = releaseHistories.getContent().get(1); ReleaseHistory branchReleaseHistory = releaseHistories.getContent().get(0); Assert.assertEquals(2, releaseHistories.getTotalElements()); Assert.assertEquals(ReleaseOperation.ROLLBACK, masterReleaseHistory.getOperation()); Assert.assertEquals(6, masterReleaseHistory.getReleaseId()); Assert.assertEquals(7, masterReleaseHistory.getPreviousReleaseId()); Assert.assertEquals(ReleaseOperation.MATER_ROLLBACK_MERGE_TO_GRAY, branchReleaseHistory.getOperation()); Assert.assertEquals(childNamespaceNewLatestRelease.getId(), branchReleaseHistory.getReleaseId()); Assert.assertEquals(8, branchReleaseHistory.getPreviousReleaseId()); Assert.assertTrue(branchReleaseHistory.getOperationContext() .contains(String.format("\"baseReleaseId\":%d", parentNamespaceNewLatestRelease.getId()))); } private Namespace instanceNamespace(long id, String clusterName) { Namespace namespace = new Namespace(); namespace.setAppId(testApp); namespace.setNamespaceName(testNamespace); namespace.setId(id); namespace.setClusterName(clusterName); return namespace; } private Map parseConfiguration(String configuration) { return GSON.fromJson(configuration, GsonType.CONFIG); } } ================================================ FILE: apollo-biz/src/test/java/com/ctrip/framework/apollo/biz/service/ReleaseHistoryServiceTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.service; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; import com.ctrip.framework.apollo.biz.BizTestConfiguration; import com.ctrip.framework.apollo.biz.config.BizConfig; import com.ctrip.framework.apollo.biz.entity.Release; import com.ctrip.framework.apollo.biz.entity.ReleaseHistory; import com.ctrip.framework.apollo.biz.repository.ReleaseHistoryRepository; import com.ctrip.framework.apollo.biz.repository.ReleaseRepository; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Maps; import java.lang.reflect.Method; import java.sql.SQLException; import org.hibernate.exception.JDBCConnectionException; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.springframework.aop.framework.AopProxyUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.annotation.DirtiesContext.ClassMode; import org.springframework.test.context.jdbc.Sql; import org.springframework.test.context.jdbc.Sql.ExecutionPhase; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.util.ReflectionUtils; /** * @author kl (http://kailing.pub) * @since 2023/3/24 */ @RunWith(SpringJUnit4ClassRunner.class) @SpringBootTest(classes = BizTestConfiguration.class, webEnvironment = WebEnvironment.RANDOM_PORT) @DirtiesContext(classMode = ClassMode.AFTER_CLASS) public class ReleaseHistoryServiceTest { @Mock private BizConfig bizConfig; @Mock private ReleaseRepository mockReleaseRepository; private ReleaseHistory mockReleaseHistory; private static final String APP_ID = "kl-app"; private static final String CLUSTER_NAME = "default"; private static final String NAMESPACE_NAME = "application"; private static final String BRANCH_NAME = "default"; @Autowired private ReleaseHistoryService releaseHistoryService; @Autowired private ReleaseHistoryRepository releaseHistoryRepository; @Autowired private ReleaseRepository releaseRepository; @Before public void setUp() throws Exception { ReflectionTestUtils.setField(releaseHistoryService, "bizConfig", bizConfig); mockReleaseHistory = spy(ReleaseHistory.class); mockReleaseHistory.setBranchName(BRANCH_NAME); mockReleaseHistory.setNamespaceName(NAMESPACE_NAME); mockReleaseHistory.setClusterName(CLUSTER_NAME); mockReleaseHistory.setAppId(APP_ID); } @Test @Sql(scripts = "/sql/release-history-test.sql", executionPhase = ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/sql/clean.sql", executionPhase = ExecutionPhase.AFTER_TEST_METHOD) public void testCleanReleaseHistory() { ReleaseHistoryService service = (ReleaseHistoryService) AopProxyUtils.getSingletonTarget(releaseHistoryService); assert service != null; Method method = ReflectionUtils.findMethod(service.getClass(), "cleanReleaseHistory", ReleaseHistory.class); assert method != null; ReflectionUtils.makeAccessible(method); when(bizConfig.releaseHistoryRetentionSize()).thenReturn(-1); when(bizConfig.releaseHistoryRetentionSizeOverride()).thenReturn(Maps.newHashMap()); ReflectionUtils.invokeMethod(method, service, mockReleaseHistory); Assert.assertEquals(6, releaseHistoryRepository.count()); Assert.assertEquals(6, releaseRepository.count()); when(bizConfig.releaseHistoryRetentionSize()).thenReturn(2); when(bizConfig.releaseHistoryRetentionSizeOverride()).thenReturn(Maps.newHashMap()); ReflectionUtils.invokeMethod(method, service, mockReleaseHistory); Assert.assertEquals(2, releaseHistoryRepository.count()); Assert.assertEquals(2, releaseRepository.count()); when(bizConfig.releaseHistoryRetentionSize()).thenReturn(2); when(bizConfig.releaseHistoryRetentionSizeOverride()) .thenReturn(ImmutableMap.of("kl-app+default+application+default", 1)); ReflectionUtils.invokeMethod(method, service, mockReleaseHistory); Assert.assertEquals(1, releaseHistoryRepository.count()); Assert.assertEquals(1, releaseRepository.count()); Iterable historyList = releaseHistoryRepository.findAll(); historyList.forEach(history -> Assert.assertEquals(6, history.getId())); Iterable releaseList = releaseRepository.findAll(); releaseList.forEach(release -> Assert.assertEquals(6, release.getId())); } @Test @Sql(scripts = "/sql/release-history-test.sql", executionPhase = ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/sql/clean.sql", executionPhase = ExecutionPhase.AFTER_TEST_METHOD) public void testCleanReleaseHistoryTransactionalRollBack() { ReleaseHistoryService service = (ReleaseHistoryService) AopProxyUtils.getSingletonTarget(releaseHistoryService); assert service != null; Method method = ReflectionUtils.findMethod(service.getClass(), "cleanReleaseHistory", ReleaseHistory.class); assert method != null; ReflectionUtils.makeAccessible(method); when(bizConfig.releaseHistoryRetentionSize()).thenReturn(1); when(bizConfig.releaseHistoryRetentionSizeOverride()).thenReturn(Maps.newHashMap()); ReflectionTestUtils.setField(releaseHistoryService, "releaseRepository", mockReleaseRepository); doThrow(new JDBCConnectionException("error", new SQLException("sql"))) .when(mockReleaseRepository).deleteAllById(any()); Assert.assertThrows(JDBCConnectionException.class, () -> ReflectionUtils.invokeMethod(method, service, mockReleaseHistory)); Assert.assertEquals(6, releaseHistoryRepository.count()); ReflectionTestUtils.setField(releaseHistoryService, "releaseRepository", releaseRepository); Assert.assertEquals(6, releaseRepository.count()); ReflectionUtils.invokeMethod(method, service, mockReleaseHistory); Assert.assertEquals(1, releaseHistoryRepository.count()); Assert.assertEquals(1, releaseRepository.count()); } } ================================================ FILE: apollo-biz/src/test/java/com/ctrip/framework/apollo/biz/service/ReleaseServiceTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.service; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import com.ctrip.framework.apollo.biz.AbstractUnitTest; import com.ctrip.framework.apollo.biz.MockBeanFactory; import com.ctrip.framework.apollo.biz.entity.Release; import com.ctrip.framework.apollo.biz.repository.ReleaseRepository; import com.ctrip.framework.apollo.common.exception.BadRequestException; import java.util.ArrayList; import java.util.Optional; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.mockito.InjectMocks; import org.mockito.Mock; import org.springframework.data.domain.PageRequest; import java.util.Arrays; import java.util.List; import java.util.Set; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; public class ReleaseServiceTest extends AbstractUnitTest { @Mock private ReleaseRepository releaseRepository; @Mock private NamespaceService namespaceService; @Mock private ReleaseHistoryService releaseHistoryService; @Mock private ItemSetService itemSetService; @InjectMocks private ReleaseService releaseService; private String appId = "appId-test"; private String clusterName = "cluster-test"; private String namespaceName = "namespace-test"; private String user = "user-test"; private long releaseId = 1; private Release firstRelease; private Release secondRelease; private PageRequest pageRequest; @Before public void init() { firstRelease = new Release(); firstRelease.setId(releaseId); firstRelease.setAppId(appId); firstRelease.setClusterName(clusterName); firstRelease.setNamespaceName(namespaceName); firstRelease.setAbandoned(false); secondRelease = new Release(); secondRelease.setAppId(appId); secondRelease.setClusterName(clusterName); secondRelease.setNamespaceName(namespaceName); secondRelease.setAbandoned(false); pageRequest = PageRequest.of(0, 2); } @Test(expected = BadRequestException.class) public void testNamespaceNotExist() { when(releaseRepository.findById(releaseId)).thenReturn(Optional.of(firstRelease)); releaseService.rollback(releaseId, user); } @Test(expected = BadRequestException.class) public void testHasNoRelease() { when(releaseRepository.findById(releaseId)).thenReturn(Optional.of(firstRelease)); when(releaseRepository.findByAppIdAndClusterNameAndNamespaceNameAndIsAbandonedFalseOrderByIdDesc(appId, clusterName, namespaceName, pageRequest)) .thenReturn(null); releaseService.rollback(releaseId, user); } @Test public void testRollback() { when(releaseRepository.findById(releaseId)).thenReturn(Optional.of(firstRelease)); when(releaseRepository.findByAppIdAndClusterNameAndNamespaceNameAndIsAbandonedFalseOrderByIdDesc(appId, clusterName, namespaceName, pageRequest)) .thenReturn( Arrays.asList(firstRelease, secondRelease)); releaseService.rollback(releaseId, user); verify(releaseRepository).save(firstRelease); Assert.assertEquals(true, firstRelease.isAbandoned()); Assert.assertEquals(user, firstRelease.getDataChangeLastModifiedBy()); } @Test public void testRollbackTo() { List releaseList = new ArrayList<>(); for (int i = 0; i < 3; i++) { Release release = new Release(); release.setId(3 - i); release.setAppId(appId); release.setClusterName(clusterName); release.setNamespaceName(namespaceName); release.setAbandoned(false); releaseList.add(release); } long releaseId1 = 1; long releaseId3 = 3; when(releaseRepository.findById(releaseId1)).thenReturn(Optional.of(releaseList.get(2))); when(releaseRepository.findById(releaseId3)).thenReturn(Optional.of(releaseList.get(0))); when(releaseRepository .findByAppIdAndClusterNameAndNamespaceNameAndIsAbandonedFalseAndIdBetweenOrderByIdDesc( appId, clusterName, namespaceName, releaseId1, releaseId3)) .thenReturn(releaseList); releaseService.rollbackTo(releaseId3, releaseId1, user); verify(releaseRepository).saveAll(releaseList); Assert.assertTrue(releaseList.get(0).isAbandoned()); Assert.assertTrue(releaseList.get(1).isAbandoned()); Assert.assertFalse(releaseList.get(2).isAbandoned()); Assert.assertEquals(user, releaseList.get(0).getDataChangeLastModifiedBy()); Assert.assertEquals(user, releaseList.get(1).getDataChangeLastModifiedBy()); } @Test public void testFindRelease() throws Exception { String someAppId = "1"; String someClusterName = "someClusterName"; String someNamespaceName = "someNamespaceName"; long someReleaseId = 1; String someReleaseKey = "someKey"; String someValidConfiguration = "{\"apollo.bar\": \"foo\"}"; Release someRelease = MockBeanFactory.mockRelease(someReleaseId, someReleaseKey, someAppId, someClusterName, someNamespaceName, someValidConfiguration); when(releaseRepository .findFirstByAppIdAndClusterNameAndNamespaceNameAndIsAbandonedFalseOrderByIdDesc(someAppId, someClusterName, someNamespaceName)) .thenReturn(someRelease); Release result = releaseService.findLatestActiveRelease(someAppId, someClusterName, someNamespaceName); verify(releaseRepository, times(1)) .findFirstByAppIdAndClusterNameAndNamespaceNameAndIsAbandonedFalseOrderByIdDesc(someAppId, someClusterName, someNamespaceName); assertEquals(someAppId, result.getAppId()); assertEquals(someClusterName, result.getClusterName()); assertEquals(someReleaseId, result.getId()); assertEquals(someReleaseKey, result.getReleaseKey()); assertEquals(someValidConfiguration, result.getConfigurations()); } @Test public void testLoadConfigWithConfigNotFound() throws Exception { String someAppId = "1"; String someClusterName = "someClusterName"; String someNamespaceName = "someNamespaceName"; when(releaseRepository .findFirstByAppIdAndClusterNameAndNamespaceNameAndIsAbandonedFalseOrderByIdDesc(someAppId, someClusterName, someNamespaceName)) .thenReturn(null); Release result = releaseService.findLatestActiveRelease(someAppId, someClusterName, someNamespaceName); assertNull(result); verify(releaseRepository, times(1)) .findFirstByAppIdAndClusterNameAndNamespaceNameAndIsAbandonedFalseOrderByIdDesc(someAppId, someClusterName, someNamespaceName); } @Test public void testFindByReleaseIds() throws Exception { Release someRelease = mock(Release.class); Release anotherRelease = mock(Release.class); long someReleaseId = 1; long anotherReleaseId = 2; List someReleases = Lists.newArrayList(someRelease, anotherRelease); Set someReleaseIds = Sets.newHashSet(someReleaseId, anotherReleaseId); when(releaseRepository.findAllById(someReleaseIds)).thenReturn(someReleases); List result = releaseService.findByReleaseIds(someReleaseIds); assertEquals(someReleases, result); } @Test public void testFindByReleaseKeys() throws Exception { Release someRelease = mock(Release.class); Release anotherRelease = mock(Release.class); String someReleaseKey = "key1"; String anotherReleaseKey = "key2"; List someReleases = Lists.newArrayList(someRelease, anotherRelease); Set someReleaseKeys = Sets.newHashSet(someReleaseKey, anotherReleaseKey); when(releaseRepository.findByReleaseKeyIn(someReleaseKeys)).thenReturn(someReleases); List result = releaseService.findByReleaseKeys(someReleaseKeys); assertEquals(someReleases, result); } } ================================================ FILE: apollo-biz/src/test/java/com/ctrip/framework/apollo/biz/service/ServerConfigServiceTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.service; import com.ctrip.framework.apollo.biz.AbstractIntegrationTest; import com.ctrip.framework.apollo.biz.entity.ServerConfig; import java.util.List; import static org.assertj.core.api.Assertions.*; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; /** * @author kl (http://kailing.pub) * @since 2022/12/14 */ public class ServerConfigServiceTest extends AbstractIntegrationTest { @Autowired private ServerConfigService serverConfigService; @Test public void findAll() { List serverConfigs = serverConfigService.findAll(); assertThat(serverConfigs).isNotNull(); assertThat(serverConfigs.size()).isGreaterThanOrEqualTo(0); } @Test public void createOrUpdateConfig() { ServerConfig serverConfig = new ServerConfig(); serverConfig.setKey("name"); serverConfig.setValue("kl"); serverConfigService.createOrUpdateConfig(serverConfig); List serverConfigs = serverConfigService.findAll(); assertThat(serverConfigs).isNotNull(); assertThat(serverConfigs.get(0).getValue()).isEqualTo("kl"); assertThat(serverConfigs.get(0).getCluster()).isEqualTo("default"); assertThat(serverConfigs.get(0).getKey()).isEqualTo("name"); serverConfig.setValue("kl2"); serverConfigService.createOrUpdateConfig(serverConfig); serverConfigs = serverConfigService.findAll(); assertThat(serverConfigs).isNotNull(); assertThat(serverConfigs.size()).isEqualTo(1); assertThat(serverConfigs.get(0).getValue()).isEqualTo("kl2"); assertThat(serverConfigs.get(0).getKey()).isEqualTo("name"); serverConfig = new ServerConfig(); serverConfig.setKey("name2"); serverConfig.setValue("kl2"); serverConfigService.createOrUpdateConfig(serverConfig); serverConfigs = serverConfigService.findAll(); assertThat(serverConfigs).isNotNull(); assertThat(serverConfigs.size()).isEqualTo(2); } } ================================================ FILE: apollo-biz/src/test/java/com/ctrip/framework/apollo/biz/utils/ConfigChangeContentBuilderTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.utils; import static org.junit.Assert.assertNotNull; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import org.junit.Before; import org.junit.Test; import com.ctrip.framework.apollo.biz.MockBeanFactory; import com.ctrip.framework.apollo.biz.entity.Item; /** * @author jian.tan */ public class ConfigChangeContentBuilderTest { private ConfigChangeContentBuilder configChangeContentBuilder; private String configString; private Item createdItem; private Item updatedItem; private Item updatedItemFalseCheck; private Item createdItemFalseCheck; @Before public void initConfig() { configChangeContentBuilder = new ConfigChangeContentBuilder(); createdItem = MockBeanFactory.mockItem(1, 1, "timeout", "100", 1); updatedItem = MockBeanFactory.mockItem(1, 1, "timeout", "1001", 1); updatedItemFalseCheck = MockBeanFactory.mockItem(1, 1, "timeout", "100", 1); createdItemFalseCheck = MockBeanFactory.mockItem(1, 1, "", "100", 1); configChangeContentBuilder.createItem(createdItem); configChangeContentBuilder.createItem(createdItemFalseCheck); configChangeContentBuilder.updateItem(createdItem, updatedItem); configChangeContentBuilder.updateItem(createdItem, updatedItemFalseCheck); configChangeContentBuilder.deleteItem(updatedItem); configChangeContentBuilder.deleteItem(createdItemFalseCheck); configString = configChangeContentBuilder.build(); } @Test public void testHasContent() { assertTrue(configChangeContentBuilder.hasContent()); configChangeContentBuilder.getCreateItems().clear(); assertTrue(configChangeContentBuilder.hasContent()); configChangeContentBuilder.getUpdateItems().clear(); assertTrue(configChangeContentBuilder.hasContent()); } @Test public void testHasContentFalseCheck() { configChangeContentBuilder.getCreateItems().clear(); configChangeContentBuilder.getUpdateItems().clear(); configChangeContentBuilder.getDeleteItems().clear(); assertFalse(configChangeContentBuilder.hasContent()); } @Test public void testConvertJsonString() { ConfigChangeContentBuilder contentBuilder = ConfigChangeContentBuilder.convertJsonString(configString); assertNotNull(contentBuilder.getCreateItems()); assertNotNull(contentBuilder.getUpdateItems().get(0).oldItem); assertNotNull(contentBuilder.getUpdateItems().get(0).newItem); assertNotNull(contentBuilder.getDeleteItems()); } } ================================================ FILE: apollo-biz/src/test/java/com/ctrip/framework/apollo/biz/utils/ReleaseKeyGeneratorTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.biz.utils; import com.google.common.collect.Sets; import com.ctrip.framework.apollo.biz.MockBeanFactory; import com.ctrip.framework.apollo.biz.entity.Namespace; import java.util.List; import org.junit.Test; import java.util.Set; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import static org.junit.Assert.assertEquals; /** * @author Jason Song(song_s@ctrip.com) */ public class ReleaseKeyGeneratorTest { @Test public void testGenerateReleaseKey() throws Exception { String someAppId = "someAppId"; String someCluster = "someCluster"; String someNamespace = "someNamespace"; String anotherAppId = "anotherAppId"; Namespace namespace = MockBeanFactory.mockNamespace(someAppId, someCluster, someNamespace); Namespace anotherNamespace = MockBeanFactory.mockNamespace(anotherAppId, someCluster, someNamespace); int generateTimes = 50000; Set releaseKeys = Sets.newConcurrentHashSet(); ExecutorService executorService = Executors.newFixedThreadPool(2); CountDownLatch latch = new CountDownLatch(1); executorService.submit(generateReleaseKeysTask(namespace, releaseKeys, generateTimes, latch)); executorService .submit(generateReleaseKeysTask(anotherNamespace, releaseKeys, generateTimes, latch)); latch.countDown(); executorService.shutdown(); executorService.awaitTermination(10, TimeUnit.SECONDS); // make sure keys are unique assertEquals(generateTimes * 2, releaseKeys.size()); } @Test public void testMessageToList() { String message = "appId+cluster+namespace"; List keys = ReleaseMessageKeyGenerator.messageToList(message); assert keys != null; assertEquals(3, keys.size()); assertEquals("appId", keys.get(0)); assertEquals("cluster", keys.get(1)); assertEquals("namespace", keys.get(2)); message = "appId+cluster"; keys = ReleaseMessageKeyGenerator.messageToList(message); assert keys != null; assertEquals(0, keys.size()); } private Runnable generateReleaseKeysTask(Namespace namespace, Set releaseKeys, int generateTimes, CountDownLatch latch) { return () -> { try { latch.await(); } catch (InterruptedException e) { // ignore } for (int i = 0; i < generateTimes; i++) { releaseKeys.add(ReleaseKeyGenerator.generateReleaseKey(namespace)); } }; } } ================================================ FILE: apollo-biz/src/test/resources/application.properties ================================================ # # Copyright 2024 Apollo Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # spring.cloud.consul.enabled=false spring.cloud.zookeeper.enabled=false spring.cloud.discovery.enabled=false spring.datasource.url = jdbc:h2:mem:~/apolloconfigdb;mode=mysql;DB_CLOSE_ON_EXIT=FALSE;DB_CLOSE_DELAY=-1;BUILTIN_ALIAS_OVERRIDE=TRUE;DATABASE_TO_UPPER=FALSE spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl spring.jpa.hibernate.globally_quoted_identifiers=false spring.jpa.properties.hibernate.globally_quoted_identifiers=false spring.jpa.properties.hibernate.show_sql=false spring.jpa.properties.hibernate.metadata_builder_contributor=com.ctrip.framework.apollo.common.jpa.SqlFunctionsMetadataBuilderContributor spring.jpa.defer-datasource-initialization=true spring.h2.console.enabled = true spring.h2.console.settings.web-allow-others=true ================================================ FILE: apollo-biz/src/test/resources/data.sql ================================================ -- -- Copyright 2024 Apollo Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. -- You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. -- INSERT INTO "AppNamespace" ("AppId", "Name", "IsPublic") VALUES ('100003171', 'application', false); INSERT INTO "AppNamespace" ("AppId", "Name", "IsPublic") VALUES ('100003171', 'fx.apollo.config', true); INSERT INTO "AppNamespace" ("AppId", "Name", "IsPublic") VALUES ('100003172', 'application', false); INSERT INTO "AppNamespace" ("AppId", "Name", "IsPublic") VALUES ('100003172', 'fx.apollo.admin', true); INSERT INTO "AppNamespace" ("AppId", "Name", "IsPublic") VALUES ('100003173', 'application', false); INSERT INTO "AppNamespace" ("AppId", "Name", "IsPublic") VALUES ('100003173', 'fx.apollo.portal', true); INSERT INTO "AppNamespace" ("AppId", "Name", "IsPublic") VALUES ('fxhermesproducer', 'fx.hermes.producer', true); ================================================ FILE: apollo-biz/src/test/resources/import.sql ================================================ -- -- Copyright 2024 Apollo Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. -- You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. -- ALTER TABLE "App" ALTER COLUMN DataChange_CreatedBy VARCHAR(255) NULL; ALTER TABLE "App" ALTER COLUMN DataChange_CreatedTime TIMESTAMP NULL; ALTER TABLE "App" ALTER COLUMN OrgName VARCHAR(255) NULL; ALTER TABLE "App" ALTER COLUMN OrgId VARCHAR(255) NULL; ALTER TABLE "AppNamespace" ALTER COLUMN DataChange_CreatedBy VARCHAR(255) NULL; ALTER TABLE "AppNamespace" ALTER COLUMN DataChange_CreatedTime TIMESTAMP NULL; ALTER TABLE "AppNamespace" ALTER COLUMN Format VARCHAR(255) NULL; ALTER TABLE "AccessKey" ALTER COLUMN DataChange_CreatedBy VARCHAR(255) NULL; ALTER TABLE "AccessKey" ALTER COLUMN DataChange_CreatedTime TIMESTAMP NULL; ALTER TABLE "Instance" ALTER COLUMN ClusterName VARCHAR(255) NULL; ALTER TABLE "Instance" ALTER COLUMN DataCenter VARCHAR(255) NULL; ALTER TABLE "Instance" ALTER COLUMN Ip VARCHAR(255) NULL; ALTER TABLE "InstanceConfig" ALTER COLUMN ReleaseDeliveryTime VARCHAR(255) NULL; ALTER TABLE "InstanceConfig" ALTER COLUMN ReleaseKey VARCHAR(255) NULL; ALTER TABLE "ServerConfig" ALTER COLUMN DataChange_CreatedBy VARCHAR(255) NULL; ALTER TABLE "ServerConfig" ALTER COLUMN DataChange_CreatedTime TIMESTAMP NULL; ALTER TABLE "ServerConfig" ALTER COLUMN Comment VARCHAR(255) NULL; ALTER TABLE "Audit" ALTER COLUMN DataChange_CreatedBy VARCHAR(255) NULL; ALTER TABLE "Audit" ALTER COLUMN DataChange_CreatedTime TIMESTAMP NULL; ALTER TABLE "Cluster" ALTER COLUMN DataChange_CreatedBy VARCHAR(255) NULL; ALTER TABLE "Cluster" ALTER COLUMN DataChange_CreatedTime TIMESTAMP NULL; ALTER TABLE "Cluster" ALTER COLUMN ParentClusterId BIGINT DEFAULT 0; ALTER TABLE "Namespace" ALTER COLUMN DataChange_CreatedBy VARCHAR(255) NULL; ALTER TABLE "Namespace" ALTER COLUMN DataChange_CreatedTime TIMESTAMP NULL; ALTER TABLE "Item" ALTER COLUMN DataChange_CreatedBy VARCHAR(255) NULL; ALTER TABLE "Item" ALTER COLUMN DataChange_CreatedTime TIMESTAMP NULL; ALTER TABLE "GrayReleaseRule" ALTER COLUMN DataChange_CreatedBy VARCHAR(255) NULL; ALTER TABLE "GrayReleaseRule" ALTER COLUMN DataChange_CreatedTime TIMESTAMP NULL; ALTER TABLE "Release" ALTER COLUMN DataChange_CreatedBy VARCHAR(255) NULL; ALTER TABLE "Release" ALTER COLUMN DataChange_CreatedTime TIMESTAMP NULL; ALTER TABLE "Release" ALTER COLUMN Comment VARCHAR(255) NULL; ALTER TABLE "Release" ALTER COLUMN Name VARCHAR(255) NULL; ALTER TABLE "Release" ALTER COLUMN ReleaseKey VARCHAR(255) NULL; ALTER TABLE "ReleaseHistory" ALTER COLUMN DataChange_CreatedBy VARCHAR(255) NULL; ALTER TABLE "ReleaseHistory" ALTER COLUMN DataChange_CreatedTime TIMESTAMP NULL; ALTER TABLE "Commit" ALTER COLUMN DataChange_CreatedBy VARCHAR(255) NULL; ALTER TABLE "Commit" ALTER COLUMN DataChange_CreatedTime TIMESTAMP NULL; ALTER TABLE "NamespaceLock" ALTER COLUMN DataChange_CreatedBy VARCHAR(255) NULL; ALTER TABLE "NamespaceLock" ALTER COLUMN DataChange_CreatedTime TIMESTAMP NULL; CREATE ALIAS IF NOT EXISTS UNIX_TIMESTAMP FOR "com.ctrip.framework.apollo.common.jpa.H2Function.unixTimestamp"; ================================================ FILE: apollo-biz/src/test/resources/json/converter/element.1.json ================================================ {"a":"1"} ================================================ FILE: apollo-biz/src/test/resources/json/converter/element.2.json ================================================ {"a":"1","disableCheck":"true"} ================================================ FILE: apollo-biz/src/test/resources/logback-test.xml ================================================ utf-8 [%p] %c - %m%n ================================================ FILE: apollo-biz/src/test/resources/sql/accesskey-test.sql ================================================ -- -- Copyright 2024 Apollo Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. -- You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. -- INSERT INTO "AccessKey" (`Id`, `AppId`, `Secret`, `Mode`, `IsEnabled`, `IsDeleted`, `DataChange_CreatedBy`, `DataChange_CreatedTime`, `DataChange_LastModifiedBy`, `DataChange_LastTime`) VALUES (1, 'someAppId', 'someSecret', 0, 0, 0, 'apollo', '2019-12-19 10:28:40', 'apollo', '2019-12-19 10:28:40'), (2, '100004458', 'c715cbc80fc44171b43732c3119c9456', 0, 0, 0, 'apollo', '2019-12-19 10:39:54', 'apollo', '2019-12-19 14:46:35'), (3, '100004458', '25a0e68d2a3941edb1ed3ab6dd0646cd', 0, 0, 1, 'apollo', '2019-12-19 13:44:13', 'apollo', '2019-12-19 13:44:19'), (4, '100004458', '4003c4d7783443dc9870932bebf3b7fe', 0, 0, 0, 'apollo', '2019-12-19 13:43:52', 'apollo', '2019-12-19 13:44:21'); ================================================ FILE: apollo-biz/src/test/resources/sql/clean.sql ================================================ -- -- Copyright 2024 Apollo Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. -- You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. -- DELETE FROM "AccessKey"; DELETE FROM "App"; DELETE FROM "AppNamespace"; DELETE FROM "Cluster"; DELETE FROM "Namespace"; DELETE FROM "GrayReleaseRule" ; DELETE FROM "Release"; DELETE FROM "Item"; DELETE FROM "ReleaseMessage"; DELETE FROM "ReleaseHistory"; DELETE FROM "NamespaceLock"; DELETE FROM "Commit"; ================================================ FILE: apollo-biz/src/test/resources/sql/item-test.sql ================================================ -- -- Copyright 2024 Apollo Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. -- You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. -- INSERT INTO "Item" (`Id`, `NamespaceId`, "Key", "Type", "Value", `Comment`, `LineNum`) VALUES (9901, 1, 'k1', 0, 'v1', '', 1), (9902, 2, 'k2', 2, 'v2', '', 2); ================================================ FILE: apollo-biz/src/test/resources/sql/itemset-test.sql ================================================ -- -- Copyright 2024 Apollo Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. -- You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. -- INSERT INTO "Namespace" (`Id`, `AppId`, `ClusterName`, `NamespaceName`, `IsDeleted`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`)VALUES(1,'testApp', 'default', 'application', 0, 'apollo', 'apollo'); INSERT INTO "Item" (`Id`, `NamespaceId`, "Key", "Type", "Value", `Comment`, `LineNum`) VALUES (9901, 1, 'k1', 0, 'v1', '', 1), (9902, 1, 'k2', 2, 'v2', '', 2), (9903, 1, 'k3', 0, 'v3', '', 3), (9904, 1, 'k4', 0, 'v4', '', 4), (9905, 1, 'k5', 0, 'v5', '', 5); ================================================ FILE: apollo-biz/src/test/resources/sql/namespace-branch-test.sql ================================================ -- -- Copyright 2024 Apollo Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. -- You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. -- INSERT INTO "App" ( `AppId`, `Name`, `OrgId`, `OrgName`, `OwnerName`, `OwnerEmail`, `IsDeleted`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`)VALUES('test', 'test0620-06', 'default', 'default', 'default', 'default', 0, 'default', 'default'); INSERT INTO "Cluster" (`Id`, `Name`, `AppId`, `ParentClusterId`, `IsDeleted`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (1, 'default', 'test', 0, 0, 'default', 'default'); INSERT INTO "Cluster" (`Name`, `AppId`, `ParentClusterId`, `IsDeleted`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`)VALUES('child-cluster', 'test', 1, 0, 'default', 'default'); INSERT INTO "Namespace" (`AppId`, `ClusterName`, `NamespaceName`, `IsDeleted`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`)VALUES('test', 'default', 'application', 0, 'apollo', 'apollo'); INSERT INTO "Namespace" (`AppId`, `ClusterName`, `NamespaceName`, `IsDeleted`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`)VALUES('test', 'child-cluster', 'application', 0, 'apollo', 'apollo'); ================================================ FILE: apollo-biz/src/test/resources/sql/namespace-test.sql ================================================ -- -- Copyright 2024 Apollo Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. -- You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. -- INSERT INTO "App" ( `AppId`, `Name`, `OrgId`, `OrgName`, `OwnerName`, `OwnerEmail`, `IsDeleted`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`)VALUES('testApp', 'test', 'default', 'default', 'default', 'default', 0, 'default', 'default'); INSERT INTO "Cluster" (`Id`, `Name`, `AppId`, `ParentClusterId`, `IsDeleted`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (1, 'default', 'testApp', 0, 0, 'default', 'default'); INSERT INTO "Cluster" (`Name`, `AppId`, `ParentClusterId`, `IsDeleted`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`)VALUES('child-cluster', 'testApp', 1, 0, 'default', 'default'); INSERT INTO "AppNamespace" (`Name`, `AppId`, `Format`, `IsPublic`) VALUES ( 'application', 'testApp', 'properties', 0); INSERT INTO "Namespace" (`Id`, `AppId`, `ClusterName`, `NamespaceName`, `IsDeleted`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`)VALUES(1,'testApp', 'default', 'application', 0, 'apollo', 'apollo'); INSERT INTO "Namespace" (`AppId`, `ClusterName`, `NamespaceName`, `IsDeleted`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`)VALUES('testApp', 'child-cluster', 'application', 0, 'apollo', 'apollo'); INSERT INTO "Commit" (`ChangeSets`, `AppId`, `ClusterName`, `NamespaceName`)VALUES('{}', 'testApp', 'default', 'application'); INSERT INTO "Commit" (`ChangeSets`, `AppId`, `ClusterName`, `NamespaceName`, `DataChange_LastTime`)VALUES('{}', 'commitTestApp', 'default', 'application', '2020-08-22 10:00:00'); INSERT INTO "Item" (`NamespaceId`, "Key", "Value", `Comment`, `LineNum`)VALUES(1, 'k1', 'v1', '', 1); INSERT INTO "NamespaceLock" (`NamespaceId`)VALUES(1); INSERT INTO "Release" (`AppId`, `ClusterName`, `NamespaceName`, `Configurations`, `IsAbandoned`)VALUES('branch-test', 'default', 'application', '{}', 0); INSERT INTO "Release" (`AppId`, `ClusterName`, `NamespaceName`, `Configurations`, `IsAbandoned`)VALUES('branch-test', 'child-cluster', 'application', '{}', 0); INSERT INTO "ReleaseHistory" (`AppId`, `ClusterName`, `NamespaceName`, `BranchName`, `ReleaseId`, `PreviousReleaseId`, `Operation`, `OperationContext`)VALUES('branch-test', 'default', 'application', 'default', 0, 0, 7, '{}'); INSERT INTO "InstanceConfig" (`Id`, `InstanceId`, `ConfigAppId`, `ConfigClusterName`, `ConfigNamespaceName`, `ReleaseKey`, `ReleaseDeliveryTime`, `DataChange_CreatedTime`, `DataChange_LastTime`) VALUES (1, 90, 'testApp', 'default', 'application', '20160829134524-dee271ddf9fced58', '2016-08-29 13:45:24', '2016-08-30 17:03:32', '2016-10-19 11:13:47'); ================================================ FILE: apollo-biz/src/test/resources/sql/release-creation-test.sql ================================================ -- -- Copyright 2024 Apollo Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. -- You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. -- INSERT INTO "App" ( `AppId`, `Name`, `OrgId`, `OrgName`, `OwnerName`, `OwnerEmail`, `IsDeleted`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`)VALUES('test', 'test0620-06', 'default', 'default', 'default', 'default', 0, 'default', 'default'); /* normal namespace*/ INSERT INTO "Cluster" ( `Name`, `AppId`, `ParentClusterId`, `IsDeleted`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES ( 'only-master', 'test', 0, 0, 'default', 'default'); INSERT INTO "Namespace" (Id, `AppId`, `ClusterName`, `NamespaceName`, `IsDeleted`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`)VALUES(100, 'test', 'only-master', 'application', 0, 'apollo', 'apollo'); INSERT INTO "Item" (`NamespaceId`, "Key", "Type", "Value", `Comment`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`)VALUES(100, 'k1', '0', 'v1', '', 'apollo', 'apollo'); INSERT INTO "Item" (`NamespaceId`, "Key", "Type", "Value", `Comment`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`)VALUES(100, 'k2', '0', 'v2', '', 'apollo', 'apollo'); INSERT INTO "Item" (`NamespaceId`, "Key", "Type", "Value", `Comment`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`)VALUES(100, 'k3', '0', 'v3', '', 'apollo', 'apollo'); /* namespace has branch. master has items but branch has not item*/ INSERT INTO "Cluster" (Id, `Name`, `AppId`, `ParentClusterId`, `IsDeleted`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (101, 'default1', 'test', 0, 0, 'default', 'default'); INSERT INTO "Cluster" (Id, `Name`, `AppId`, `ParentClusterId`, `IsDeleted`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`)VALUES(102, 'child-cluster1', 'test', 101, 0, 'default', 'default'); INSERT INTO "Namespace" (Id, `AppId`, `ClusterName`, `NamespaceName`, `IsDeleted`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`)VALUES(101, 'test', 'default1', 'application', 0, 'apollo', 'apollo'); INSERT INTO "Namespace" (Id, `AppId`, `ClusterName`, `NamespaceName`, `IsDeleted`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`)VALUES(102, 'test', 'child-cluster1', 'application', 0, 'apollo', 'apollo'); INSERT INTO "Item" (`NamespaceId`, "Key", "Type", "Value", `Comment`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`)VALUES(101, 'k1', '0', 'v1', '', 'apollo', 'apollo'); INSERT INTO "Item" (`NamespaceId`, "Key", "Type", "Value", `Comment`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`)VALUES(101, 'k2', '0', 'v2', '', 'apollo', 'apollo'); INSERT INTO "Item" (`NamespaceId`, "Key", "Type", "Value", `Comment`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`)VALUES(101, 'k3', '0', 'v3', '', 'apollo', 'apollo'); INSERT INTO "GrayReleaseRule" (`AppId`, `ClusterName`, `NamespaceName`, `BranchName`, `Rules`, `ReleaseId`, `BranchStatus`)VALUES ('test', 'default1', 'application', 'child-cluster1', '[]', 1155, 1); /* namespace has branch. master has items and branch has item*/ INSERT INTO "Cluster" (Id, `Name`, `AppId`, `ParentClusterId`, `IsDeleted`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (103, 'default2', 'test', 0, 0, 'default', 'default'); INSERT INTO "Cluster" (Id, `Name`, `AppId`, `ParentClusterId`, `IsDeleted`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`)VALUES(104, 'child-cluster2', 'test', 103, 0, 'default', 'default'); INSERT INTO "Namespace" (Id, `AppId`, `ClusterName`, `NamespaceName`, `IsDeleted`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`)VALUES(103, 'test', 'default2', 'application', 0, 'apollo', 'apollo'); INSERT INTO "Namespace" (Id, `AppId`, `ClusterName`, `NamespaceName`, `IsDeleted`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`)VALUES(104, 'test', 'child-cluster2', 'application', 0, 'apollo', 'apollo'); INSERT INTO "Item" (`NamespaceId`, "Key", "Type", "Value", `Comment`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`)VALUES(103, 'k1', '0', 'v1', '', 'apollo', 'apollo'); INSERT INTO "Item" (`NamespaceId`, "Key", "Type", "Value", `Comment`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`)VALUES(103, 'k2', '0', 'v2', '', 'apollo', 'apollo'); INSERT INTO "Item" (`NamespaceId`, "Key", "Type", "Value", `Comment`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`)VALUES(103, 'k3', '0', 'v3', '', 'apollo', 'apollo'); INSERT INTO "Item" (`NamespaceId`, "Key", "Type", "Value", `Comment`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`)VALUES(104, 'k1', '0', 'v1-1', '', 'apollo', 'apollo'); INSERT INTO "GrayReleaseRule" (`AppId`, `ClusterName`, `NamespaceName`, `BranchName`, `Rules`, `ReleaseId`, `BranchStatus`)VALUES ('test', 'default2', 'application', 'child-cluster2', '[]', 1155, 1); /* namespace has branch. master has items and branch has cover item */ INSERT INTO "Cluster" (Id, `Name`, `AppId`, `ParentClusterId`, `IsDeleted`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (105, 'default3', 'test', 0, 0, 'default', 'default'); INSERT INTO "Cluster" (Id, `Name`, `AppId`, `ParentClusterId`, `IsDeleted`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`)VALUES(106, 'child-cluster3', 'test', 105, 0, 'default', 'default'); INSERT INTO "Namespace" (Id, `AppId`, `ClusterName`, `NamespaceName`, `IsDeleted`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`)VALUES(105, 'test', 'default3', 'application', 0, 'apollo', 'apollo'); INSERT INTO "Namespace" (Id, `AppId`, `ClusterName`, `NamespaceName`, `IsDeleted`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`)VALUES(106, 'test', 'child-cluster3', 'application', 0, 'apollo', 'apollo'); INSERT INTO "Item" (`NamespaceId`, "Key", "Type", "Value", `Comment`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`)VALUES(105, 'k1', '0', 'v1', '', 'apollo', 'apollo'); INSERT INTO "Item" (`NamespaceId`, "Key", "Type", "Value", `Comment`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`)VALUES(105, 'k2', '0', 'v2-2', '', 'apollo', 'apollo'); INSERT INTO "Item" (`NamespaceId`, "Key", "Type", "Value", `Comment`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`)VALUES(106, 'k1', '0', 'v1-2', '', 'apollo', 'apollo'); INSERT INTO "Release" (`Id`, `ReleaseKey`, `Name`, `Comment`, `AppId`, `ClusterName`, `NamespaceName`, `Configurations`, `IsAbandoned`)VALUES(1, '20160823102253-fc0071ddf9fd3260', '20160823101703-release', '', 'test', 'default3', 'application', '{"k1":"v1","k2":"v2","k3":"v3"}', 0); INSERT INTO "Release" (`Id`, `ReleaseKey`, `Name`, `Comment`, `AppId`, `ClusterName`, `NamespaceName`, `Configurations`, `IsAbandoned`)VALUES(2, '20160823102253-fc0071ddf9fd3260', '20160823101703-release', '', 'test', 'child-cluster3', 'application', '{"k1":"v1-1","k2":"v2","k3":"v3"}', 0); INSERT INTO "GrayReleaseRule" (`AppId`, `ClusterName`, `NamespaceName`, `BranchName`, `Rules`, `ReleaseId`, `BranchStatus`)VALUES ('test', 'default3', 'application', 'child-cluster3', '[]', 1155, 1); /*publish branch at first time */ INSERT INTO "Cluster" (Id, `Name`, `AppId`, `ParentClusterId`, `IsDeleted`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (107, 'default4', 'test', 0, 0, 'default', 'default'); INSERT INTO "Cluster" (Id, `Name`, `AppId`, `ParentClusterId`, `IsDeleted`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`)VALUES(108, 'child-cluster4', 'test', 107, 0, 'default', 'default'); INSERT INTO "Namespace" (Id, `AppId`, `ClusterName`, `NamespaceName`, `IsDeleted`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`)VALUES(107, 'test', 'default4', 'application', 0, 'apollo', 'apollo'); INSERT INTO "Namespace" (Id, `AppId`, `ClusterName`, `NamespaceName`, `IsDeleted`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`)VALUES(108, 'test', 'child-cluster4', 'application', 0, 'apollo', 'apollo'); INSERT INTO "Item" (`NamespaceId`, "Key", "Type", "Value", `Comment`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`)VALUES(107, 'k1', '0', 'v1', '', 'apollo', 'apollo'); INSERT INTO "Item" (`NamespaceId`, "Key", "Type", "Value", `Comment`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`)VALUES(107, 'k2', '0', 'v2-2', '', 'apollo', 'apollo'); INSERT INTO "Item" (`NamespaceId`, "Key", "Type", "Value", `Comment`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`)VALUES(108, 'k1', '0', 'v1-2', '', 'apollo', 'apollo'); INSERT INTO "Item" (`NamespaceId`, "Key", "Type", "Value", `Comment`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`)VALUES(108, 'k4', '0', 'v4', '', 'apollo', 'apollo'); INSERT INTO "Release" (`Id`, `ReleaseKey`, `Name`, `Comment`, `AppId`, `ClusterName`, `NamespaceName`, `Configurations`, `IsAbandoned`)VALUES(3, '20160823102253-fc0071ddf9fd3260', '20160823101703-release', '', 'test', 'default4', 'application', '{"k1":"v1","k2":"v2","k3":"v3"}', 0); INSERT INTO "GrayReleaseRule" (`AppId`, `ClusterName`, `NamespaceName`, `BranchName`, `Rules`, `ReleaseId`, `BranchStatus`)VALUES ('test', 'default4', 'application', 'child-cluster4', '[]', 1155, 1); /*publish branch*/ INSERT INTO "Cluster" (Id, `Name`, `AppId`, `ParentClusterId`, `IsDeleted`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (109, 'default5', 'test', 0, 0, 'default', 'default'); INSERT INTO "Cluster" (Id, `Name`, `AppId`, `ParentClusterId`, `IsDeleted`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`)VALUES(1010, 'child-cluster5', 'test', 109, 0, 'default', 'default'); INSERT INTO "Namespace" (Id, `AppId`, `ClusterName`, `NamespaceName`, `IsDeleted`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`)VALUES(109, 'test', 'default5', 'application', 0, 'apollo', 'apollo'); INSERT INTO "Namespace" (Id, `AppId`, `ClusterName`, `NamespaceName`, `IsDeleted`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`)VALUES(1010, 'test', 'child-cluster5', 'application', 0, 'apollo', 'apollo'); INSERT INTO "Item" (`NamespaceId`, "Key", "Type", "Value", `Comment`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`)VALUES(109, 'k1', '0', 'v1', '', 'apollo', 'apollo'); INSERT INTO "Item" (`NamespaceId`, "Key", "Type", "Value", `Comment`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`)VALUES(109, 'k2', '0', 'v2-2', '', 'apollo', 'apollo'); INSERT INTO "Item" (`NamespaceId`, "Key", "Type", "Value", `Comment`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`)VALUES(1010, 'k1', '0', 'v1-2', '', 'apollo', 'apollo'); INSERT INTO "Item" (`NamespaceId`, "Key", "Type", "Value", `Comment`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`)VALUES(1010, 'k4', '0', 'v4', '', 'apollo', 'apollo'); INSERT INTO "Item" (`NamespaceId`, "Key", "Type", "Value", `Comment`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`)VALUES(1010, 'k6', '0', 'v6', '', 'apollo', 'apollo'); INSERT INTO "Release" (`Id`, `ReleaseKey`, `Name`, `Comment`, `AppId`, `ClusterName`, `NamespaceName`, `Configurations`, `IsAbandoned`)VALUES(4, '20160823102253-fc0071ddf9fd3260', '20160823101703-release', '', 'test', 'default5', 'application', '{"k1":"v1","k2":"v2","k3":"v3"}', 0); INSERT INTO "Release" (`Id`, `ReleaseKey`, `Name`, `Comment`, `AppId`, `ClusterName`, `NamespaceName`, `Configurations`, `IsAbandoned`)VALUES(5, '20160823102253-fc0071ddf9fd3260', '20160823101703-release', '', 'test', 'child-cluster5', 'application', '{"k1":"v1-1","k2":"v2","k3":"v3","k4":"v4","k5":"v5"}', 0); INSERT INTO "GrayReleaseRule" (`AppId`, `ClusterName`, `NamespaceName`, `BranchName`, `Rules`, `ReleaseId`, `BranchStatus`)VALUES ('test', 'default5', 'application', 'child-cluster5', '[]', 1155, 1); /* rollback */ INSERT INTO "Cluster" (Id, `Name`, `AppId`, `ParentClusterId`, `IsDeleted`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (1011, 'default6', 'test', 0, 0, 'default', 'default'); INSERT INTO "Cluster" (Id, `Name`, `AppId`, `ParentClusterId`, `IsDeleted`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`)VALUES(1012, 'child-cluster6', 'test', 1011, 0, 'default', 'default'); INSERT INTO "Namespace" (Id, `AppId`, `ClusterName`, `NamespaceName`, `IsDeleted`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`)VALUES(1011, 'test', 'default6', 'application', 0, 'apollo', 'apollo'); INSERT INTO "Namespace" (Id, `AppId`, `ClusterName`, `NamespaceName`, `IsDeleted`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`)VALUES(1012, 'test', 'child-cluster6', 'application', 0, 'apollo', 'apollo'); INSERT INTO "Release" (`Id`, `ReleaseKey`, `Name`, `Comment`, `AppId`, `ClusterName`, `NamespaceName`, `Configurations`, `IsAbandoned`)VALUES(6, '20160823102253-fc0071ddf9fd3260', '20160823101703-release', '', 'test', 'default6', 'application', '{"k1":"v1-1","k2":"v2-1","k3":"v3"}', 0); INSERT INTO "Release" (`Id`, `ReleaseKey`, `Name`, `Comment`, `AppId`, `ClusterName`, `NamespaceName`, `Configurations`, `IsAbandoned`)VALUES(7, '20160823102253-fc0071ddf9fd3260', '20160823101703-release', '', 'test', 'default6', 'application', '{"k1":"v1","k2":"v2"}', 0); INSERT INTO "Release" (`Id`, `ReleaseKey`, `Name`, `Comment`, `AppId`, `ClusterName`, `NamespaceName`, `Configurations`, `IsAbandoned`)VALUES(8, '20160823102253-fc0071ddf9fd3260', '20160823101703-release', '', 'test', 'child-cluster6', 'application', '{"k1":"v1-2","k2":"v2","k3":"v3"}', 0); ================================================ FILE: apollo-biz/src/test/resources/sql/release-history-test.sql ================================================ -- -- Copyright 2024 Apollo Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. -- You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. -- INSERT INTO ReleaseHistory (Id, AppId, ClusterName, NamespaceName, BranchName, ReleaseId, PreviousReleaseId, Operation, OperationContext, DataChange_CreatedBy, DataChange_LastModifiedBy) VALUES (1, 'kl-app', 'default', 'application', 'default', 1, 0, 0, '{"isEmergencyPublish":false}', 'apollo', 'apollo'), (2, 'kl-app', 'default', 'application', 'default', 2, 1, 0, '{"isEmergencyPublish":false}', 'apollo', 'apollo'), (3, 'kl-app', 'default', 'application', 'default', 3, 2, 0, '{"isEmergencyPublish":false}', 'apollo', 'apollo'), (4, 'kl-app', 'default', 'application', 'default', 4, 3, 0, '{"isEmergencyPublish":false}', 'apollo', 'apollo'), (5, 'kl-app', 'default', 'application', 'default', 5, 4, 0, '{"isEmergencyPublish":false}', 'apollo', 'apollo'), (6, 'kl-app', 'default', 'application', 'default', 6, 5, 0, '{"isEmergencyPublish":false}', 'apollo', 'apollo'); INSERT INTO "Release" (Id, ReleaseKey, Name, Comment, AppId, ClusterName, NamespaceName, Configurations) VALUES (1, 'TEST-RELEASE-KEY1', 'test','First Release','kl-app', 'default', 'application', '{"k1":"override-someDC-v1"}'), (2, 'TEST-RELEASE-KEY2', 'test','First Release','kl-app', 'default', 'application', '{"k1":"override-someDC-v1"}'), (3, 'TEST-RELEASE-KEY3', 'test','First Release','kl-app', 'default', 'application', '{"k1":"override-someDC-v1"}'), (4, 'TEST-RELEASE-KEY4', 'test','First Release','kl-app', 'default', 'application', '{"k1":"override-someDC-v1"}'), (5, 'TEST-RELEASE-KEY5', 'test','First Release','kl-app', 'default', 'application', '{"k1":"override-someDC-v1"}'), (6, 'TEST-RELEASE-KEY6', 'test','First Release','kl-app', 'default', 'application', '{"k1":"override-someDC-v1"}'); ================================================ FILE: apollo-build-sql-converter/pom.xml ================================================ com.ctrip.framework.apollo apollo ${revision} ../pom.xml 4.0.0 apollo-build-sql-converter Apollo Build Sql Converter org.freemarker freemarker com.h2database h2 test org.springframework.boot spring-boot-starter-jdbc test sql-converter false org.apache.maven.plugins maven-compiler-plugin org.codehaus.mojo exec-maven-plugin 3.0.0 sql-converter compile java com.ctrip.framework.apollo.build.sql.converter.ApolloSqlConverter ================================================ FILE: apollo-build-sql-converter/src/main/java/com/ctrip/framework/apollo/build/sql/converter/ApolloH2ConverterUtil.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.build.sql.converter; import java.io.BufferedWriter; import java.io.IOException; import java.io.UncheckedIOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; import java.util.List; import java.util.StringJoiner; import java.util.regex.Matcher; import java.util.regex.Pattern; public class ApolloH2ConverterUtil { public static void convert(SqlTemplate sqlTemplate, String targetSql, SqlTemplateContext context) { ApolloSqlConverterUtil.ensureDirectories(targetSql); String rawText = ApolloSqlConverterUtil.process(sqlTemplate, context); List sqlStatements = ApolloSqlConverterUtil.toStatements(rawText); try (BufferedWriter bufferedWriter = Files.newBufferedWriter(Paths.get(targetSql), StandardCharsets.UTF_8, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) { for (SqlStatement sqlStatement : sqlStatements) { String convertedText; try { convertedText = convertAssemblyH2Line(sqlStatement); } catch (Throwable e) { throw new RuntimeException("convert error: " + sqlStatement.getRawText(), e); } bufferedWriter.write(convertedText); bufferedWriter.write('\n'); } } catch (IOException e) { throw new UncheckedIOException(e); } } private static final Pattern OPERATION_TABLE_PATTERN = Pattern.compile( "(?DROP|CREATE|ALTER)\\s+TABLE\\s+(`)?(?[a-zA-Z0-9\\-_]+)(`)?\\s*", Pattern.CASE_INSENSITIVE); private static final Pattern CREATE_INDEX_ON_PATTERN = Pattern.compile( "CREATE\\s+INDEX\\s+(`)?(?[a-zA-Z0-9\\-_]+)(`)?\\s+ON\\s+(`)?(?[a-zA-Z0-9\\-_]+)(`)?", Pattern.CASE_INSENSITIVE); private static String convertAssemblyH2Line(SqlStatement sqlStatement) { String convertedText = sqlStatement.getRawText(); // TABLE `` Matcher opTableMatcher = OPERATION_TABLE_PATTERN.matcher(convertedText); if (opTableMatcher.find()) { String operation = opTableMatcher.group("operation"); if ("DROP".equalsIgnoreCase(operation)) { return ""; } else if ("CREATE".equalsIgnoreCase(operation)) { return convertCreateTable(convertedText, sqlStatement, opTableMatcher); } else if ("ALTER".equalsIgnoreCase(operation)) { return convertAlterTable(convertedText, sqlStatement, opTableMatcher); } } // CREATE INDEX `` ON `` Matcher createIndexOnMatcher = CREATE_INDEX_ON_PATTERN.matcher(convertedText); if (createIndexOnMatcher.find()) { String createIndexOnTableName = createIndexOnMatcher.group("tableName"); // index with table return convertIndexOnTable(convertedText, createIndexOnTableName, sqlStatement); } // others return convertedText; } private static String convertCreateTable(String convertedText, SqlStatement sqlStatement, Matcher opTableMatcher) { String tableName = opTableMatcher.group("tableName"); // table config convertedText = convertTableConfig(convertedText, sqlStatement); // index with table convertedText = convertIndexWithTable(convertedText, tableName, sqlStatement); // column convertedText = convertColumn(convertedText, sqlStatement); return convertedText; } private static final Pattern ENGINE_PATTERN = Pattern.compile("ENGINE\\s*=\\s*InnoDB", Pattern.CASE_INSENSITIVE); private static final Pattern DEFAULT_CHARSET_PATTERN = Pattern.compile("DEFAULT\\s+CHARSET\\s*=\\s*utf8mb4", Pattern.CASE_INSENSITIVE); private static final Pattern ROW_FORMAT_PATTERN = Pattern.compile("ROW_FORMAT\\s*=\\s*DYNAMIC", Pattern.CASE_INSENSITIVE); private static String convertTableConfig(String convertedText, SqlStatement sqlStatement) { Matcher engineMatcher = ENGINE_PATTERN.matcher(convertedText); if (engineMatcher.find()) { convertedText = engineMatcher.replaceAll(""); } Matcher defaultCharsetMatcher = DEFAULT_CHARSET_PATTERN.matcher(convertedText); if (defaultCharsetMatcher.find()) { convertedText = defaultCharsetMatcher.replaceAll(""); } Matcher rowFormatMatcher = ROW_FORMAT_PATTERN.matcher(convertedText); if (rowFormatMatcher.find()) { convertedText = rowFormatMatcher.replaceAll(""); } return convertedText; } private static final Pattern INDEX_NAME_PATTERN = Pattern.compile( // KEY `AppId_ClusterName_GroupName` "(KEY\\s*`|KEY\\s+)(?[a-zA-Z0-9\\-_]+)(`)?\\s*" // (`AppId`,`ClusterName`(191),`NamespaceName`(191)) + "\\((?" + "(`)?[a-zA-Z0-9\\-_]+(`)?\\s*(\\([0-9]+\\))?" + "(," + "(`)?[a-zA-Z0-9\\-_]+(`)?\\s*(\\([0-9]+\\))?" + ")*" + ")\\)", Pattern.CASE_INSENSITIVE); private static String convertIndexWithTable(String convertedText, String tableName, SqlStatement sqlStatement) { String[] lines = convertedText.split("\n"); StringJoiner joiner = new StringJoiner("\n"); for (String line : lines) { String convertedLine = line; if (convertedLine.contains("KEY") || convertedLine.contains("key")) { // replace index name // KEY `AppId_ClusterName_GroupName` (`AppId`,`ClusterName`(191),`NamespaceName`(191)) // -> // KEY `tableName_AppId_ClusterName_GroupName` // (`AppId`,`ClusterName`(191),`NamespaceName`(191)) Matcher indexNameMatcher = INDEX_NAME_PATTERN.matcher(convertedLine); if (indexNameMatcher.find()) { convertedLine = indexNameMatcher.replaceAll("KEY `" + tableName + "_${indexName}` (${indexColumns})"); } convertedLine = removePrefixIndex(convertedLine); } joiner.add(convertedLine); } return joiner.toString(); } private static String convertColumn(String convertedText, SqlStatement sqlStatement) { // convert bit(1) to boolean // `IsDeleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '1: deleted, 0: normal' // -> // `IsDeleted` boolean NOT NULL DEFAULT FALSE if (convertedText.contains("bit(1)")) { convertedText = convertedText.replace("bit(1)", "boolean"); } if (convertedText.contains("b'0'")) { convertedText = convertedText.replace("b'0'", "FALSE"); } if (convertedText.contains("b'1'")) { convertedText = convertedText.replace("b'1'", "TRUE"); } return convertedText; } private static String convertAlterTable(String convertedText, SqlStatement sqlStatement, Matcher opTableMatcher) { String tableName = opTableMatcher.group("tableName"); // remove first table name convertedText = opTableMatcher.replaceAll(""); convertedText = convertAlterTableMulti(convertedText, sqlStatement, tableName); return convertedText; } private static final Pattern ADD_COLUMN_PATTERN = Pattern.compile( "\\s*ADD\\s+COLUMN\\s+(`)?(?[a-zA-Z0-9\\-_]+)(`)?(?.*)[,;]", Pattern.CASE_INSENSITIVE); private static final Pattern MODIFY_COLUMN_PATTERN = Pattern.compile( "\\s*MODIFY\\s+COLUMN\\s+(`)?(?[a-zA-Z0-9\\-_]+)(`)?(?.*)[,;]", Pattern.CASE_INSENSITIVE); private static final Pattern CHANGE_PATTERN = Pattern.compile( "\\s*CHANGE\\s+(`)?(?[a-zA-Z0-9\\-_]+)(`)?\\s+(`)?(?[a-zA-Z0-9\\-_]+)(`)?(?.*)[,;]", Pattern.CASE_INSENSITIVE); private static final Pattern DROP_COLUMN_PATTERN = Pattern.compile("\\s*DROP\\s+(COLUMN\\s+)?(`)?(?[a-zA-Z0-9\\-_]+)(`)?\\s*[,;]", Pattern.CASE_INSENSITIVE); private static final Pattern ADD_KEY_PATTERN = Pattern.compile( "\\s*ADD\\s+(?(UNIQUE\\s+)?KEY)\\s+(`)?(?[a-zA-Z0-9\\-_]+)(`)?(?.*)[,;]", Pattern.CASE_INSENSITIVE); private static final Pattern ADD_INDEX_PATTERN = Pattern.compile( "\\s*ADD\\s+(?(UNIQUE\\s+)?INDEX)\\s+(`)?(?[a-zA-Z0-9\\-_]+)(`)?(?.*)[,;]", Pattern.CASE_INSENSITIVE); private static final Pattern DROP_INDEX_PATTERN = Pattern.compile("\\s*DROP\\s+INDEX\\s+(`)?(?[a-zA-Z0-9\\-_]+)(`)?\\s*[,;]", Pattern.CASE_INSENSITIVE); private static String convertAlterTableMulti(String convertedText, SqlStatement sqlStatement, String tableName) { Matcher addColumnMatcher = ADD_COLUMN_PATTERN.matcher(convertedText); if (addColumnMatcher.find()) { convertedText = addColumnMatcher.replaceAll( "\nALTER TABLE `" + tableName + "` ADD COLUMN `${columnName}`${subStatement};"); } Matcher modifyColumnMatcher = MODIFY_COLUMN_PATTERN.matcher(convertedText); if (modifyColumnMatcher.find()) { convertedText = modifyColumnMatcher.replaceAll( "\nALTER TABLE `" + tableName + "` MODIFY COLUMN `${columnName}`${subStatement};"); } Matcher changeMatcher = CHANGE_PATTERN.matcher(convertedText); if (changeMatcher.find()) { convertedText = changeMatcher.replaceAll("\nALTER TABLE `" + tableName + "` CHANGE `${oldColumnName}` `${newColumnName}` ${subStatement};"); } Matcher dropColumnMatcher = DROP_COLUMN_PATTERN.matcher(convertedText); if (dropColumnMatcher.find()) { convertedText = dropColumnMatcher.replaceAll("\nALTER TABLE `" + tableName + "` DROP `${columnName}`;"); } Matcher addKeyMatcher = ADD_KEY_PATTERN.matcher(convertedText); if (addKeyMatcher.find()) { convertedText = addKeyMatcher.replaceAll("\nALTER TABLE `" + tableName + "` ADD ${indexType} `" + tableName + "_${indexName}` ${subStatement};"); convertedText = removePrefixIndex(convertedText); } Matcher addIndexMatcher = ADD_INDEX_PATTERN.matcher(convertedText); if (addIndexMatcher.find()) { convertedText = addIndexMatcher.replaceAll("\nALTER TABLE `" + tableName + "` ADD ${indexType} `" + tableName + "_${indexName}` ${subStatement};"); convertedText = removePrefixIndex(convertedText); } Matcher dropIndexMatcher = DROP_INDEX_PATTERN.matcher(convertedText); if (dropIndexMatcher.find()) { convertedText = dropIndexMatcher.replaceAll( "\nALTER TABLE `" + tableName + "` DROP INDEX `" + tableName + "_${indexName}`;"); } return convertedText; } private static final Pattern CREATE_INDEX_PATTERN = Pattern.compile( "CREATE\\s+(?(UNIQUE\\s+)?INDEX)\\s+(`)?(?[a-zA-Z0-9\\-_]+)(`)?", Pattern.CASE_INSENSITIVE); private static String convertIndexOnTable(String convertedText, String tableName, SqlStatement sqlStatement) { Matcher createIndexMatcher = CREATE_INDEX_PATTERN.matcher(convertedText); if (createIndexMatcher.find()) { convertedText = createIndexMatcher.replaceAll("CREATE ${indexType} `" + tableName + "_${indexName}`"); convertedText = removePrefixIndex(convertedText); } return convertedText; } private static final Pattern PREFIX_INDEX_PATTERN = Pattern.compile("(?\\(" // other columns + "((`)?[a-zA-Z0-9\\-_]+(`)?\\s*(\\([0-9]+\\))?,)*)" // ``(191) + "(`)?(?[a-zA-Z0-9\\-_]+)(`)?\\s*\\([0-9]+\\)" // other columns + "(?(,(`)?[a-zA-Z0-9\\-_]+(`)?\\s*(\\([0-9]+\\))?)*" + "\\))"); private static String removePrefixIndex(String convertedText) { // convert prefix index // (`AppId`,`ClusterName`(191),`NamespaceName`(191)) // -> // (`AppId`,`ClusterName`,`NamespaceName`) for (Matcher prefixIndexMatcher = PREFIX_INDEX_PATTERN.matcher(convertedText); prefixIndexMatcher .find(); prefixIndexMatcher = PREFIX_INDEX_PATTERN.matcher(convertedText)) { convertedText = prefixIndexMatcher.replaceAll("${prefix}`${columnName}`${suffix}"); } return convertedText; } } ================================================ FILE: apollo-build-sql-converter/src/main/java/com/ctrip/framework/apollo/build/sql/converter/ApolloMysqlDefaultConverterUtil.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.build.sql.converter; import java.io.BufferedWriter; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; import java.util.List; public class ApolloMysqlDefaultConverterUtil { public static void convert(SqlTemplate sqlTemplate, String targetSql, SqlTemplateContext context) { String databaseName; String srcSql = sqlTemplate.getSrcPath(); if (srcSql.contains("apolloconfigdb")) { databaseName = "ApolloConfigDB"; } else if (srcSql.contains("apolloportaldb")) { databaseName = "ApolloPortalDB"; } else { throw new IllegalArgumentException("unknown database name: " + srcSql); } ApolloSqlConverterUtil.ensureDirectories(targetSql); String rawText = ApolloSqlConverterUtil.process(sqlTemplate, context); List sqlStatements = ApolloSqlConverterUtil.toStatements(rawText); try (BufferedWriter bufferedWriter = Files.newBufferedWriter(Paths.get(targetSql), StandardCharsets.UTF_8, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) { for (SqlStatement sqlStatement : sqlStatements) { String convertedText = convertMainMysqlLine(sqlStatement, databaseName); bufferedWriter.write(convertedText); bufferedWriter.write('\n'); } } catch (IOException e) { throw new RuntimeException(e); } } private static String convertMainMysqlLine(SqlStatement sqlStatement, String databaseName) { String convertedText = sqlStatement.getRawText(); convertedText = convertedText.replace("ApolloAssemblyDB", databaseName); return convertedText; } } ================================================ FILE: apollo-build-sql-converter/src/main/java/com/ctrip/framework/apollo/build/sql/converter/ApolloSqlConverter.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.build.sql.converter; import freemarker.template.Configuration; import java.io.File; import java.io.IOException; import java.io.UncheckedIOException; import java.nio.charset.StandardCharsets; import java.util.List; public class ApolloSqlConverter { public static void main(String[] args) { String repositoryDir = ApolloSqlConverterUtil.getRepositoryDir(); String srcDir = repositoryDir + "/scripts/sql/src"; String targetParentDir = repositoryDir + "/scripts"; SqlTemplateGist gists = ApolloSqlConverterUtil.getGists(repositoryDir); convert(repositoryDir, srcDir, targetParentDir, gists); } public static List convert(String repositoryDir, String srcDir, String targetParentDir, SqlTemplateGist gists) { Configuration configuration = createConfiguration(srcDir); // 'scripts/sql/src/apolloconfigdb.sql' // 'scripts/sql/src/apolloportaldb.sql' // 'scripts/sql/src/delta/**/*.sql' List srcSqlList = ApolloSqlConverterUtil.getSqlList(srcDir); List templateList = ApolloSqlConverterUtil.toTemplates(srcSqlList, srcDir, configuration); // 'scripts/sql/src' -> 'scripts/sql/profiles/mysql-default' convertMysqlDefaultList(templateList, srcDir, targetParentDir, gists); // 'scripts/sql/src' -> 'scripts/sql/profiles/mysql-database-not-specified' convertMysqlDatabaseNotSpecifiedList(templateList, srcDir, targetParentDir, gists); // 'scripts/sql/src' -> 'scripts/sql/profiles/h2-default' convertH2DefaultList(templateList, srcDir, targetParentDir, gists); return srcSqlList; } private static Configuration createConfiguration(String srcDir) { Configuration configuration = new Configuration(Configuration.VERSION_2_3_32); try { configuration.setDirectoryForTemplateLoading(new File(srcDir)); } catch (IOException e) { throw new UncheckedIOException(e); } configuration.setDefaultEncoding(StandardCharsets.UTF_8.name()); return configuration; } private static void convertMysqlDefaultList(List templateList, String srcDir, String targetParentDir, SqlTemplateGist gists) { String targetDir = targetParentDir + "/sql/profiles/mysql-default"; SqlTemplateGist mainMysqlGists = SqlTemplateGist.builder() .autoGeneratedDeclaration(gists.getAutoGeneratedDeclaration()).h2Function("") .setupDatabase(gists.getSetupDatabase()).useDatabase(gists.getUseDatabase()).build(); SqlTemplateContext context = SqlTemplateContext.builder().gists(mainMysqlGists).build(); for (SqlTemplate sqlTemplate : templateList) { String targetSql = ApolloSqlConverterUtil.replacePath(sqlTemplate.getSrcPath(), srcDir, targetDir); ApolloMysqlDefaultConverterUtil.convert(sqlTemplate, targetSql, context); } } private static void convertMysqlDatabaseNotSpecifiedList(List templateList, String srcDir, String targetParentDir, SqlTemplateGist gists) { String targetDir = targetParentDir + "/sql/profiles/mysql-database-not-specified"; SqlTemplateGist mainMysqlGists = SqlTemplateGist.builder().autoGeneratedDeclaration(gists.getAutoGeneratedDeclaration()) .h2Function("").setupDatabase("").useDatabase("").build(); SqlTemplateContext context = SqlTemplateContext.builder().gists(mainMysqlGists).build(); for (SqlTemplate sqlTemplate : templateList) { String targetSql = ApolloSqlConverterUtil.replacePath(sqlTemplate.getSrcPath(), srcDir, targetDir); ApolloMysqlDefaultConverterUtil.convert(sqlTemplate, targetSql, context); } } private static void convertH2DefaultList(List templateList, String srcDir, String targetParentDir, SqlTemplateGist gists) { String targetDir = targetParentDir + "/sql/profiles/h2-default"; SqlTemplateGist mainMysqlGists = SqlTemplateGist.builder().autoGeneratedDeclaration(gists.getAutoGeneratedDeclaration()) .h2Function(gists.getH2Function()).setupDatabase("").useDatabase("").build(); SqlTemplateContext context = SqlTemplateContext.builder().gists(mainMysqlGists).build(); for (SqlTemplate sqlTemplate : templateList) { String targetSql = ApolloSqlConverterUtil.replacePath(sqlTemplate.getSrcPath(), srcDir, targetDir); ApolloH2ConverterUtil.convert(sqlTemplate, targetSql, context); } } } ================================================ FILE: apollo-build-sql-converter/src/main/java/com/ctrip/framework/apollo/build/sql/converter/ApolloSqlConverterUtil.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.build.sql.converter; import freemarker.template.Configuration; import freemarker.template.Template; import java.io.BufferedReader; import java.io.IOException; import java.io.StringReader; import java.io.StringWriter; import java.io.UncheckedIOException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.nio.charset.StandardCharsets; import java.nio.file.DirectoryStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.security.CodeSource; import java.security.ProtectionDomain; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Set; import java.util.StringJoiner; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import java.util.regex.Pattern; public class ApolloSqlConverterUtil { private static final Pattern BASE_PATTERN = Pattern.compile("(apolloconfigdb|apolloportaldb)-v[0-9]{3,}-v[0-9]{3,}-base.sql"); private static final Pattern BEFORE_PATTERN = Pattern.compile("(apolloconfigdb|apolloportaldb)-v[0-9]{3,}-v[0-9]{3,}-before.sql"); private static final Pattern DELTA_PATTERN = Pattern.compile("(apolloconfigdb|apolloportaldb)-v[0-9]{3,}-v[0-9]{3,}.sql"); private static final Pattern AFTER_PATTERN = Pattern.compile("(apolloconfigdb|apolloportaldb)-v[0-9]{3,}-v[0-9]{3,}-after.sql"); public static String getRepositoryDir() { ProtectionDomain protectionDomain = ApolloSqlConverter.class.getProtectionDomain(); CodeSource codeSource = protectionDomain.getCodeSource(); URL location = codeSource.getLocation(); URI uri; try { uri = location.toURI(); } catch (URISyntaxException e) { throw new IllegalArgumentException(e.getLocalizedMessage(), e); } Path path = Paths.get(uri); String unixClassPath = path.toString().replace("\\", "/"); if (!unixClassPath.endsWith("/apollo-build-sql-converter/target/classes")) { throw new IllegalStateException("illegal class path: " + unixClassPath); } return ApolloSqlConverterUtil.replacePath(unixClassPath, "/apollo-build-sql-converter/target/classes", ""); } public static void ensureDirectories(String targetFilePath) { Path path = Paths.get(targetFilePath); Path dirPath = path.getParent(); try { Files.createDirectories(dirPath); } catch (IOException e) { throw new UncheckedIOException(e); } } public static String process(SqlTemplate sqlTemplate, SqlTemplateContext context) { Template freemarkerTemplate = sqlTemplate.getTemplate(); StringWriter writer = new StringWriter(); try { freemarkerTemplate.process(context, writer); } catch (Exception e) { throw new RuntimeException(e); } return writer.toString(); } public static String replacePrefix(String origin, String prefix, String target) { if (!origin.startsWith(prefix)) { throw new IllegalArgumentException("illegal file path: " + origin); } return origin.replace(prefix, target); } public static String replacePath(String origin, String src, String target) { if (!origin.contains(src)) { throw new IllegalArgumentException("illegal file path: " + origin); } return origin.replace(src, target); } public static SqlTemplateGist getGists(String repositoryDir) { String gistDir = repositoryDir + "/scripts/sql/src/gist"; String autoGeneratedDeclaration = getGist(gistDir + "/autoGeneratedDeclaration.sql"); String h2Function = getGist(gistDir + "/h2Function.sql"); String setupDatabase = getGist(gistDir + "/setupDatabase.sql"); String useDatabase = getGist(gistDir + "/useDatabase.sql"); return SqlTemplateGist.builder().autoGeneratedDeclaration(autoGeneratedDeclaration) .h2Function(h2Function).setupDatabase(setupDatabase).useDatabase(useDatabase).build(); } private static String getGist(String gistPath) { StringJoiner joiner = new StringJoiner("\n", "\n", ""); boolean accept = false; try (BufferedReader bufferedReader = Files.newBufferedReader(Paths.get(gistPath), StandardCharsets.UTF_8)) { for (String line = bufferedReader.readLine(); line != null; line = bufferedReader.readLine()) { if (line.contains("@@gist-start@@")) { accept = true; continue; } if (line.contains("@@gist-end@@")) { break; } if (accept) { joiner.add(line); } } } catch (IOException e) { throw new UncheckedIOException("failed to open gistPath " + e.getLocalizedMessage(), e); } return joiner.toString(); } public static List getSqlList(String dir, Set ignoreDirs) { List sqlList = new ArrayList<>(); if (Files.exists(Paths.get(dir + "/apolloconfigdb.sql"))) { sqlList.add(dir + "/apolloconfigdb.sql"); } if (Files.exists(Paths.get(dir + "/apolloportaldb.sql"))) { sqlList.add(dir + "/apolloportaldb.sql"); } List deltaSqlList = getDeltaSqlList(dir, ignoreDirs); sqlList.addAll(deltaSqlList); return sqlList; } public static List getSqlList(String dir) { return getSqlList(dir, Collections.emptySet()); } public static List list(Path dir) { List subPathList = new ArrayList<>(); try (DirectoryStream ds = Files.newDirectoryStream(dir)) { for (Path path : ds) { subPathList.add(path); } } catch (IOException e) { throw new UncheckedIOException("failed to open dir " + e.getLocalizedMessage(), e); } return subPathList; } public static List listSorted(Path dir, Comparator comparator) { List subPathList = list(dir); List sortedSubPathList = new ArrayList<>(subPathList); sortedSubPathList.sort(comparator); return sortedSubPathList; } public static List listSorted(Path dir) { return listSorted(dir, Comparator.comparing(Path::toString)); } public static void deleteDir(Path dir) { try { ApolloSqlConverterUtil.deleteDirInternal(dir); } catch (IOException e) { throw new UncheckedIOException("failed to delete dir " + e.getLocalizedMessage(), e); } } private static void deleteDirInternal(Path dir) throws IOException { if (!Files.exists(dir)) { return; } List files = ApolloSqlConverterUtil.list(dir); for (Path file : files) { if (!Files.exists(file)) { continue; } if (Files.isDirectory(file)) { ApolloSqlConverterUtil.deleteDirInternal(file); Files.delete(file); } else { Files.delete(file); } } } public static Comparator deltaSqlComparator() { return Comparator.comparing(path -> { String unixPath = path.replace("\\", "/"); int lastIndex = unixPath.lastIndexOf("/"); String fileName; if (lastIndex > 0) { fileName = unixPath.substring(lastIndex + 1); } else { fileName = unixPath; } if (!fileName.endsWith(".sql")) { // not sql file return path; } // sort: base < before < delta < after if (BASE_PATTERN.matcher(fileName).matches()) { return "00" + path; } else if (BEFORE_PATTERN.matcher(fileName).matches()) { return "30" + path; } else if (DELTA_PATTERN.matcher(fileName).matches()) { return "50" + path; } else if (AFTER_PATTERN.matcher(fileName).matches()) { return "90" + path; } else { throw new IllegalArgumentException("illegal file name: " + fileName); } }); } private static List getDeltaSqlList(String dir, Set ignoreDirs) { Path dirPath = Paths.get(dir + "/delta"); if (!Files.exists(dirPath)) { return Collections.emptyList(); } List deltaDirList = listSorted(dirPath); List allDeltaSqlList = new ArrayList<>(); for (Path deltaDir : deltaDirList) { if (!Files.isDirectory(deltaDir)) { continue; } if (ignoreDirs.contains(deltaDir.toString().replace("\\", "/"))) { continue; } List deltaFiles = listSorted(deltaDir, Comparator.comparing(Path::toString, deltaSqlComparator())); for (Path path : deltaFiles) { String fileName = path.toString(); if (fileName.endsWith(".sql")) { allDeltaSqlList.add(fileName.replace("\\", "/")); } } } return allDeltaSqlList; } public static List toTemplates(List srcSqlList, String srcDir, Configuration configuration) { List templateList = new ArrayList<>(srcSqlList.size()); for (String srcSql : srcSqlList) { String templateName = ApolloSqlConverterUtil.replacePrefix(srcSql, srcDir + "/", ""); Template template; try { template = configuration.getTemplate(templateName); } catch (IOException e) { throw new UncheckedIOException(e); } templateList.add(new SqlTemplate(srcSql, template)); } return templateList; } public static List toStatements(String rawText) { List sqlStatementList = new ArrayList<>(); try (BufferedReader bufferedReader = new BufferedReader(new StringReader(rawText))) { AtomicReference rawTextJoinerRef = new AtomicReference<>(new StringJoiner("\n")); StringBuilder singleLineTextBuilder = new StringBuilder(); List textLines = new ArrayList<>(); AtomicBoolean comment = new AtomicBoolean(false); for (String line = bufferedReader.readLine(); line != null; line = bufferedReader.readLine()) { if (line.startsWith("--")) { commentLine(line, rawTextJoinerRef, singleLineTextBuilder, textLines, comment, sqlStatementList); } else { noCommentLine(line, rawTextJoinerRef, singleLineTextBuilder, textLines, comment, sqlStatementList); } } if (!textLines.isEmpty()) { StringJoiner sqlStatementRawTextJoiner = rawTextJoinerRef.get(); SqlStatement sqlStatement = createStatement(sqlStatementRawTextJoiner, singleLineTextBuilder, textLines); sqlStatementList.add(sqlStatement); } } catch (IOException e) { throw new RuntimeException(e); } return sqlStatementList; } private static void commentLine(String line, AtomicReference rawTextJoinerRef, StringBuilder singleLineTextBuilder, List textLines, AtomicBoolean comment, List sqlStatementList) { if (!comment.get()) { comment.set(true); if (!textLines.isEmpty()) { StringJoiner sqlStatementRawTextJoiner = rawTextJoinerRef.get(); SqlStatement sqlStatement = createStatement(sqlStatementRawTextJoiner, singleLineTextBuilder, textLines); resetStatementBuffer(rawTextJoinerRef, singleLineTextBuilder, textLines); sqlStatementList.add(sqlStatement); } } StringJoiner sqlStatementRawTextJoiner = rawTextJoinerRef.get(); // raw text sqlStatementRawTextJoiner.add(line); // single line text singleLineTextBuilder.append(line); // text lines textLines.add(line); } private static void noCommentLine(String line, AtomicReference rawTextJoinerRef, StringBuilder singleLineTextBuilder, List textLines, AtomicBoolean comment, List sqlStatementList) { if (comment.get()) { comment.set(false); if (!textLines.isEmpty()) { StringJoiner sqlStatementRawTextJoiner = rawTextJoinerRef.get(); SqlStatement sqlStatement = createStatement(sqlStatementRawTextJoiner, singleLineTextBuilder, textLines); resetStatementBuffer(rawTextJoinerRef, singleLineTextBuilder, textLines); sqlStatementList.add(sqlStatement); } } StringJoiner sqlStatementRawTextJoiner = rawTextJoinerRef.get(); // ; is the end of a statement int indexOfSemicolon = line.indexOf(';'); if (indexOfSemicolon == -1) { // raw text sqlStatementRawTextJoiner.add(line); // single line text singleLineTextBuilder.append(line); // text lines textLines.add(line); } else { String lineBeforeSemicolon = line.substring(0, indexOfSemicolon + 1); // raw text sqlStatementRawTextJoiner.add(lineBeforeSemicolon); // single line text singleLineTextBuilder.append(lineBeforeSemicolon); // text lines textLines.add(lineBeforeSemicolon); SqlStatement sqlStatement = createStatement(sqlStatementRawTextJoiner, singleLineTextBuilder, textLines); resetStatementBuffer(rawTextJoinerRef, singleLineTextBuilder, textLines); sqlStatementList.add(sqlStatement); } } private static SqlStatement createStatement(StringJoiner sqlStatementRawTextJoiner, StringBuilder singleLineTextBuilder, List textLines) { return SqlStatement.builder().rawText(sqlStatementRawTextJoiner.toString()) .singleLineText(singleLineTextBuilder.toString()).textLines(new ArrayList<>(textLines)) .build(); } private static void resetStatementBuffer(AtomicReference rawTextJoinerRef, StringBuilder singleLineTextBuilder, List textLines) { // raw text rawTextJoinerRef.set(new StringJoiner("\n")); // single line text singleLineTextBuilder.setLength(0); // text lines textLines.clear(); } } ================================================ FILE: apollo-build-sql-converter/src/main/java/com/ctrip/framework/apollo/build/sql/converter/SqlStatement.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.build.sql.converter; import java.util.Collections; import java.util.List; import java.util.StringJoiner; public class SqlStatement { private final String rawText; private final String singleLineText; private final List textLines; SqlStatement(Builder builder) { this.rawText = builder.rawText; this.singleLineText = builder.singleLineText; this.textLines = builder.textLines; } public static Builder builder() { return new Builder(); } public Builder toBuilder() { Builder builder = new Builder(); builder.rawText = this.rawText; builder.singleLineText = this.singleLineText; builder.textLines = this.textLines; return builder; } public String getRawText() { return this.rawText; } public String getSingleLineText() { return this.singleLineText; } public List getTextLines() { return this.textLines; } @Override public String toString() { return new StringJoiner(", ", SqlStatement.class.getSimpleName() + "[", "]") // fields .add("rawText='" + this.rawText + "'").toString(); } public static final class Builder { private String rawText; private String singleLineText; private List textLines; Builder() {} public Builder rawText(String rawText) { this.rawText = rawText; return this; } public Builder singleLineText(String singleLineText) { this.singleLineText = singleLineText; return this; } public Builder textLines(List textLines) { this.textLines = textLines == null ? null : // nonnull (textLines.isEmpty() ? Collections.emptyList() : Collections.unmodifiableList(textLines)); return this; } public SqlStatement build() { return new SqlStatement(this); } } } ================================================ FILE: apollo-build-sql-converter/src/main/java/com/ctrip/framework/apollo/build/sql/converter/SqlTemplate.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.build.sql.converter; import freemarker.template.Template; public class SqlTemplate { private final String srcPath; private final Template template; public SqlTemplate(String srcPath, Template template) { this.srcPath = srcPath; this.template = template; } public String getSrcPath() { return this.srcPath; } public Template getTemplate() { return this.template; } } ================================================ FILE: apollo-build-sql-converter/src/main/java/com/ctrip/framework/apollo/build/sql/converter/SqlTemplateContext.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.build.sql.converter; import java.util.StringJoiner; public class SqlTemplateContext { /** * sql gist */ private final SqlTemplateGist gists; SqlTemplateContext(Builder builder) { this.gists = builder.gists; } public static Builder builder() { return new Builder(); } public Builder toBuilder() { Builder builder = new Builder(); builder.gists = this.gists; return builder; } public SqlTemplateGist getGists() { return this.gists; } @Override public String toString() { return new StringJoiner(", ", SqlTemplateContext.class.getSimpleName() + "[", "]") // fields .add("gists=" + this.gists).toString(); } public static final class Builder { private SqlTemplateGist gists; Builder() {} public Builder gists(SqlTemplateGist gists) { this.gists = gists; return this; } public SqlTemplateContext build() { return new SqlTemplateContext(this); } } } ================================================ FILE: apollo-build-sql-converter/src/main/java/com/ctrip/framework/apollo/build/sql/converter/SqlTemplateGist.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.build.sql.converter; import java.util.StringJoiner; public class SqlTemplateGist { private final String autoGeneratedDeclaration; private final String h2Function; private final String setupDatabase; private final String useDatabase; SqlTemplateGist(Builder builder) { this.autoGeneratedDeclaration = builder.autoGeneratedDeclaration; this.h2Function = builder.h2Function; this.setupDatabase = builder.setupDatabase; this.useDatabase = builder.useDatabase; } public static Builder builder() { return new Builder(); } public Builder toBuilder() { Builder builder = new Builder(); builder.autoGeneratedDeclaration = this.autoGeneratedDeclaration; builder.h2Function = this.h2Function; builder.setupDatabase = this.setupDatabase; builder.useDatabase = this.useDatabase; return builder; } public String getAutoGeneratedDeclaration() { return this.autoGeneratedDeclaration; } public String getH2Function() { return this.h2Function; } public String getSetupDatabase() { return this.setupDatabase; } public String getUseDatabase() { return this.useDatabase; } @Override public String toString() { return new StringJoiner(", ", SqlTemplateGist.class.getSimpleName() + "[", "]") // fields .add("autoGeneratedDeclaration='" + this.autoGeneratedDeclaration + "'") .add("h2Function='" + this.h2Function + "'") .add("setupDatabase='" + this.setupDatabase + "'") .add("useDatabase='" + this.useDatabase + "'").toString(); } public static final class Builder { private String autoGeneratedDeclaration; private String h2Function; private String setupDatabase; private String useDatabase; Builder() {} public Builder autoGeneratedDeclaration(String autoGeneratedDeclaration) { this.autoGeneratedDeclaration = autoGeneratedDeclaration; return this; } public Builder h2Function(String h2Function) { this.h2Function = h2Function; return this; } public Builder setupDatabase(String setupDatabase) { this.setupDatabase = setupDatabase; return this; } public Builder useDatabase(String useDatabase) { this.useDatabase = useDatabase; return this; } public SqlTemplateGist build() { return new SqlTemplateGist(this); } } } ================================================ FILE: apollo-build-sql-converter/src/test/java/com/ctrip/framework/apollo/build/sql/converter/ApolloSqlConverterAutoGeneratedTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.build.sql.converter; import java.io.BufferedReader; import java.io.IOException; import java.io.UncheckedIOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Paths; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; class ApolloSqlConverterAutoGeneratedTest { private static final String GENERATE_TIPS = "mvn compile -pl apollo-build-sql-converter -Psql-converter"; /** * Check sql files in 'scripts/sql/profiles' are auto generated. */ @Test void checkAutoGenerated() { String repositoryDir = ApolloSqlConverterUtil.getRepositoryDir(); String srcDir = repositoryDir + "/scripts/sql/src"; String checkerParentDir = repositoryDir + "/apollo-build-sql-converter/target/scripts/sql/checker-auto-generated"; String repositoryParentDir = repositoryDir + "/scripts"; // generate checker sql files ApolloSqlConverterUtil.deleteDir(Paths.get(checkerParentDir)); SqlTemplateGist gists = ApolloSqlConverterUtil.getGists(repositoryDir); List srcSqlList = ApolloSqlConverter.convert(repositoryDir, srcDir, checkerParentDir, gists); // compare checker sql files with repository sql files this.checkSqlList(srcSqlList, srcDir, checkerParentDir, repositoryParentDir); } private void checkSqlList(List srcSqlList, String srcDir, String checkerParentDir, String repositoryParentDir) { // 'scripts/sql/profiles/mysql-default' this.checkMysqlDefaultList(srcSqlList, srcDir, checkerParentDir, repositoryParentDir); // 'scripts/sql/profiles/mysql-database-not-specified' this.checkMysqlDatabaseNotSpecifiedList(srcSqlList, srcDir, checkerParentDir, repositoryParentDir); // '/scripts/sql/profiles/h2-default' this.checkH2DefaultList(srcSqlList, srcDir, checkerParentDir, repositoryParentDir); } private void checkMysqlDefaultList(List srcSqlList, String srcDir, String checkerParentDir, String repositoryParentDir) { String checkerTargetDir = checkerParentDir + "/sql/profiles/mysql-default"; String repositoryTargetDir = repositoryParentDir + "/sql/profiles/mysql-default"; List checkerSqlList = ApolloSqlConverterUtil.getSqlList(checkerTargetDir); Set ignoreDirs = this.getIgnoreDirs(repositoryTargetDir); List repositorySqlList = ApolloSqlConverterUtil.getSqlList(repositoryTargetDir, ignoreDirs); List redundantSqlList = this.findRedundantSqlList(checkerTargetDir, checkerSqlList, repositoryTargetDir, repositorySqlList); Assertions.assertEquals(0, redundantSqlList.size(), "redundant sql files, please add sql files in 'scripts/sql/src' and then run '" + GENERATE_TIPS + "' to generated. Do not edit 'scripts/sql/profiles' manually !!!\npath: " + redundantSqlList); List missingSqlList = this.findMissingSqlList(checkerTargetDir, checkerSqlList, repositoryTargetDir, repositorySqlList); Assertions.assertEquals(0, missingSqlList.size(), "missing sql files, please run '" + GENERATE_TIPS + "' to regenerated\npath: " + missingSqlList); for (String srcSql : srcSqlList) { String checkerTargetSql = ApolloSqlConverterUtil.replacePath(srcSql, srcDir, checkerTargetDir); String repositoryTargetSql = ApolloSqlConverterUtil.replacePath(srcSql, srcDir, repositoryTargetDir); this.doCheck(checkerTargetSql, repositoryTargetSql); } } private Set getIgnoreDirs(String repositoryTargetDir) { Set ignoreDirs = new LinkedHashSet<>(); ignoreDirs.add(repositoryTargetDir + "/delta/v040-v050"); ignoreDirs.add(repositoryTargetDir + "/delta/v060-v062"); ignoreDirs.add(repositoryTargetDir + "/delta/v080-v090"); ignoreDirs.add(repositoryTargetDir + "/delta/v151-v160"); ignoreDirs.add(repositoryTargetDir + "/delta/v170-v180"); ignoreDirs.add(repositoryTargetDir + "/delta/v180-v190"); ignoreDirs.add(repositoryTargetDir + "/delta/v190-v200"); ignoreDirs.add(repositoryTargetDir + "/delta/v200-v210"); ignoreDirs.add(repositoryTargetDir + "/delta/v210-v220"); return ignoreDirs; } private List findRedundantSqlList(String checkerTargetDir, List checkerSqlList, String repositoryTargetDir, List repositorySqlList) { // repository - checker Map missingSqlMap = this.findMissing(repositoryTargetDir, repositorySqlList, checkerTargetDir, checkerSqlList); return new ArrayList<>(missingSqlMap.keySet()); } private List findMissingSqlList(String checkerTargetDir, List checkerSqlList, String repositoryTargetDir, List repositorySqlList) { // checker - repository Map missingSqlMap = this.findMissing(checkerTargetDir, checkerSqlList, repositoryTargetDir, repositorySqlList); return new ArrayList<>(missingSqlMap.values()); } private Map findMissing(String sourceDir, List sourceSqlList, String targetDir, List targetSqlList) { Map missingSqlList = new LinkedHashMap<>(); Set targetSqlSet = new LinkedHashSet<>(targetSqlList); for (String sourceSql : sourceSqlList) { String targetSql = ApolloSqlConverterUtil.replacePath(sourceSql, sourceDir, targetDir); if (!targetSqlSet.contains(targetSql)) { missingSqlList.put(sourceSql, targetSql); } } return missingSqlList; } private void doCheck(String checkerTargetSql, String repositoryTargetSql) { List checkerLines = new ArrayList<>(); try (BufferedReader bufferedReader = Files.newBufferedReader(Paths.get(checkerTargetSql), StandardCharsets.UTF_8)) { for (String line = bufferedReader.readLine(); line != null; line = bufferedReader.readLine()) { checkerLines.add(line); } } catch (IOException e) { throw new UncheckedIOException(e); } List repositoryLines = new ArrayList<>(); try (BufferedReader bufferedReader = Files.newBufferedReader(Paths.get(repositoryTargetSql), StandardCharsets.UTF_8)) { for (String line = bufferedReader.readLine(); line != null; line = bufferedReader.readLine()) { repositoryLines.add(line); } } catch (IOException e) { throw new UncheckedIOException(e); } for (int i = 0; i < checkerLines.size(); i++) { String checkerLine = checkerLines.get(i); int lineNumber = i + 1; if (i >= repositoryLines.size()) { Assertions.fail("invalid sql file content, please run '" + GENERATE_TIPS + "' to regenerated\npath: " + repositoryTargetSql + "(line: " + lineNumber + ")"); } String repositoryLine = repositoryLines.get(i); Assertions.assertEquals(checkerLine, repositoryLine, "invalid sql file content, please run '" + GENERATE_TIPS + "' to regenerated\npath: " + repositoryTargetSql + "(line: " + lineNumber + ")"); } Assertions.assertEquals(checkerLines.size(), repositoryLines.size(), "invalid sql file content, please run '" + GENERATE_TIPS + "' to regenerated\npath: " + repositoryTargetSql + "(line: " + checkerLines.size() + ")"); } private void checkMysqlDatabaseNotSpecifiedList(List srcSqlList, String srcDir, String checkerParentDir, String repositoryParentDir) { String checkerTargetDir = checkerParentDir + "/sql/profiles/mysql-database-not-specified"; String repositoryTargetDir = repositoryParentDir + "/sql/profiles/mysql-database-not-specified"; for (String srcSql : srcSqlList) { String checkerTargetSql = ApolloSqlConverterUtil.replacePath(srcSql, srcDir, checkerTargetDir); String repositoryTargetSql = ApolloSqlConverterUtil.replacePath(srcSql, srcDir, repositoryTargetDir); this.doCheck(checkerTargetSql, repositoryTargetSql); } } private void checkH2DefaultList(List srcSqlList, String srcDir, String checkerParentDir, String repositoryParentDir) { String checkerTargetDir = checkerParentDir + "/sql/profiles/h2-default"; String repositoryTargetDir = repositoryParentDir + "/sql/profiles/h2-default"; for (String srcSql : srcSqlList) { String checkerTargetSql = ApolloSqlConverterUtil.replacePath(srcSql, srcDir, checkerTargetDir); String repositoryTargetSql = ApolloSqlConverterUtil.replacePath(srcSql, srcDir, repositoryTargetDir); this.doCheck(checkerTargetSql, repositoryTargetSql); } } } ================================================ FILE: apollo-build-sql-converter/src/test/java/com/ctrip/framework/apollo/build/sql/converter/ApolloSqlConverterH2Test.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.build.sql.converter; import java.nio.charset.StandardCharsets; import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.springframework.core.io.PathResource; import org.springframework.jdbc.datasource.SimpleDriverDataSource; import org.springframework.jdbc.datasource.init.DatabasePopulatorUtils; import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator; class ApolloSqlConverterH2Test { @Test void checkH2() { String repositoryDir = ApolloSqlConverterUtil.getRepositoryDir(); String srcDir = repositoryDir + "/scripts/sql/src"; String checkerParentDir = repositoryDir + "/apollo-build-sql-converter/target/scripts/sql/checker-h2"; String testSrcDir = repositoryDir + "/apollo-build-sql-converter/src/test/resources/META-INF/sql/h2-test"; String testCheckerParentDir = repositoryDir + "/apollo-build-sql-converter/target/scripts/sql/checker-h2-test"; // generate checker sql files ApolloSqlConverterUtil.deleteDir(Paths.get(checkerParentDir)); SqlTemplateGist gists = ApolloSqlConverterUtil.getGists(repositoryDir); SqlTemplateGist h2TestGist = gists.toBuilder().h2Function("\n" + "\n" + "-- H2 Function\n" + "-- ------------------------------------------------------------\n" + "CREATE ALIAS IF NOT EXISTS UNIX_TIMESTAMP FOR \"com.ctrip.framework.apollo.build.sql.converter.TestH2Function.unixTimestamp\";\n") .build(); List srcSqlList = ApolloSqlConverter.convert(repositoryDir, srcDir, checkerParentDir, h2TestGist); List checkerSqlList = new ArrayList<>(srcSqlList.size()); for (String srcSql : srcSqlList) { String checkerSql = ApolloSqlConverterUtil.replacePath(srcSql, srcDir, checkerParentDir + "/sql/profiles/h2-default"); checkerSqlList.add(checkerSql); } // generate test checker sql files ApolloSqlConverterUtil.deleteDir(Paths.get(testCheckerParentDir)); List testSrcSqlList = ApolloSqlConverter.convert(repositoryDir, testSrcDir, testCheckerParentDir, h2TestGist); List testCheckerSqlList = new ArrayList<>(testSrcSqlList.size()); for (String testSrcSql : testSrcSqlList) { String testCheckerSql = ApolloSqlConverterUtil.replacePath(testSrcSql, testSrcDir, testCheckerParentDir + "/sql/profiles/h2-default"); testCheckerSqlList.add(testCheckerSql); } this.checkSort(testSrcSqlList); String h2Path = "test-h2"; this.checkConfigDatabase(h2Path, checkerSqlList, testCheckerSqlList); this.checkPortalDatabase(h2Path, checkerSqlList, testCheckerSqlList); } private void checkSort(List testSrcSqlList) { int baseIndex = this.getIndex(testSrcSqlList, "apolloconfigdb-v000-v010-base.sql"); Assertions.assertTrue(baseIndex >= 0); int beforeIndex = this.getIndex(testSrcSqlList, "apolloconfigdb-v000-v010-before.sql"); Assertions.assertTrue(beforeIndex >= 0); int deltaIndex = this.getIndex(testSrcSqlList, "apolloconfigdb-v000-v010.sql"); Assertions.assertTrue(deltaIndex >= 0); int afterIndex = this.getIndex(testSrcSqlList, "apolloconfigdb-v000-v010-after.sql"); Assertions.assertTrue(afterIndex >= 0); // base < before < delta < after Assertions.assertTrue(baseIndex < beforeIndex); Assertions.assertTrue(beforeIndex < deltaIndex); Assertions.assertTrue(deltaIndex < afterIndex); } private int getIndex(List srcSqlList, String fileName) { for (int i = 0; i < srcSqlList.size(); i++) { String sqlFile = srcSqlList.get(i); if (sqlFile.endsWith(fileName)) { return i; } } return -1; } private void checkConfigDatabase(String h2Path, List checkerSqlList, List testCheckerSqlList) { SimpleDriverDataSource configDataSource = new SimpleDriverDataSource(); configDataSource.setUrl("jdbc:h2:mem:~/" + h2Path + "/apollo-config-db;mode=mysql;DB_CLOSE_ON_EXIT=FALSE;DB_CLOSE_DELAY=-1;BUILTIN_ALIAS_OVERRIDE=TRUE;DATABASE_TO_UPPER=FALSE"); configDataSource.setDriverClass(org.h2.Driver.class); ResourceDatabasePopulator populator = new ResourceDatabasePopulator(); populator.setContinueOnError(false); populator.setSeparator(";"); populator.setSqlScriptEncoding(StandardCharsets.UTF_8.name()); for (String sqlFile : testCheckerSqlList) { if (sqlFile.contains("apolloconfigdb-")) { populator.addScript(new PathResource(Paths.get(sqlFile))); } } for (String sqlFile : checkerSqlList) { if (sqlFile.contains("apolloconfigdb-")) { populator.addScript(new PathResource(Paths.get(sqlFile))); } } DatabasePopulatorUtils.execute(populator, configDataSource); } private void checkPortalDatabase(String h2Path, List checkerSqlList, List testCheckerSqlList) { SimpleDriverDataSource portalDataSource = new SimpleDriverDataSource(); portalDataSource.setUrl("jdbc:h2:mem:~/" + h2Path + "/apollo-portal-db;mode=mysql;DB_CLOSE_ON_EXIT=FALSE;DB_CLOSE_DELAY=-1;BUILTIN_ALIAS_OVERRIDE=TRUE;DATABASE_TO_UPPER=FALSE"); portalDataSource.setDriverClass(org.h2.Driver.class); ResourceDatabasePopulator populator = new ResourceDatabasePopulator(); populator.setContinueOnError(false); populator.setSeparator(";"); populator.setSqlScriptEncoding(StandardCharsets.UTF_8.name()); for (String sqlFile : testCheckerSqlList) { if (sqlFile.contains("apolloportaldb-")) { populator.addScript(new PathResource(Paths.get(sqlFile))); } } for (String sqlFile : checkerSqlList) { if (sqlFile.contains("apolloportaldb-")) { populator.addScript(new PathResource(Paths.get(sqlFile))); } } DatabasePopulatorUtils.execute(populator, portalDataSource); } } ================================================ FILE: apollo-build-sql-converter/src/test/java/com/ctrip/framework/apollo/build/sql/converter/TestH2Function.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.build.sql.converter; public class TestH2Function { public static long unixTimestamp(java.sql.Timestamp timestamp) { return timestamp.getTime() / 1000L; } } ================================================ FILE: apollo-build-sql-converter/src/test/resources/META-INF/sql/h2-test/delta/v000-v010/apolloconfigdb-v000-v010-after.sql ================================================ -- -- Copyright 2024 Apollo Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. -- You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. -- /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; /*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; /*!40101 SET NAMES utf8 */; /*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; /*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; /*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; -- ${gists.autoGeneratedDeclaration} -- ${gists.h2Function} -- ${gists.setupDatabase} -- ${gists.autoGeneratedDeclaration} ================================================ FILE: apollo-build-sql-converter/src/test/resources/META-INF/sql/h2-test/delta/v000-v010/apolloconfigdb-v000-v010-base.sql ================================================ -- -- Copyright 2024 Apollo Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. -- You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. -- /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; /*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; /*!40101 SET NAMES utf8 */; /*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; /*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; /*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; -- ${gists.autoGeneratedDeclaration} -- ${gists.h2Function} -- ${gists.setupDatabase} -- Dump of table app -- ------------------------------------------------------------ DROP TABLE IF EXISTS `App`; CREATE TABLE `App` ( `Id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键', `AppId` varchar(500) NOT NULL DEFAULT 'default' COMMENT 'AppID', `Name` varchar(500) NOT NULL DEFAULT 'default' COMMENT '应用名', `OrgId` varchar(32) NOT NULL DEFAULT 'default' COMMENT '部门Id', `OrgName` varchar(64) NOT NULL DEFAULT 'default' COMMENT '部门名字', `OwnerName` varchar(500) NOT NULL DEFAULT 'default' COMMENT 'ownerName', `OwnerEmail` varchar(500) NOT NULL DEFAULT 'default' COMMENT 'ownerEmail', `IsDeleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '1: deleted, 0: normal', `DataChange_CreatedBy` varchar(32) NOT NULL DEFAULT 'default' COMMENT '创建人邮箱前缀', `DataChange_CreatedTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `DataChange_LastModifiedBy` varchar(32) DEFAULT '' COMMENT '最后修改人邮箱前缀', `DataChange_LastTime` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改时间', PRIMARY KEY (`Id`), KEY `AppId` (`AppId`(191)), KEY `DataChange_LastTime` (`DataChange_LastTime`), KEY `Name` (`Name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='应用表'; -- Dump of table appnamespace -- ------------------------------------------------------------ DROP TABLE IF EXISTS `AppNamespace`; CREATE TABLE `AppNamespace` ( `Id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主键', `Name` varchar(32) NOT NULL DEFAULT '' COMMENT 'namespace名字,注意,需要全局唯一', `AppId` varchar(32) NOT NULL DEFAULT '' COMMENT 'app id', `Format` varchar(32) NOT NULL DEFAULT 'properties' COMMENT 'namespace的format类型', `IsPublic` bit(1) NOT NULL DEFAULT b'0' COMMENT 'namespace是否为公共', `Comment` varchar(64) NOT NULL DEFAULT '' COMMENT '注释', `IsDeleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '1: deleted, 0: normal', `DataChange_CreatedBy` varchar(32) NOT NULL DEFAULT '' COMMENT '创建人邮箱前缀', `DataChange_CreatedTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `DataChange_LastModifiedBy` varchar(32) DEFAULT '' COMMENT '最后修改人邮箱前缀', `DataChange_LastTime` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改时间', PRIMARY KEY (`Id`), KEY `Name_AppId` (`Name`,`AppId`), KEY `DataChange_LastTime` (`DataChange_LastTime`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='应用namespace定义'; -- Dump of table audit -- ------------------------------------------------------------ DROP TABLE IF EXISTS `Audit`; CREATE TABLE `Audit` ( `Id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键', `EntityName` varchar(50) NOT NULL DEFAULT 'default' COMMENT '表名', `EntityId` int(10) unsigned DEFAULT NULL COMMENT '记录ID', `OpName` varchar(50) NOT NULL DEFAULT 'default' COMMENT '操作类型', `Comment` varchar(500) DEFAULT NULL COMMENT '备注', `IsDeleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '1: deleted, 0: normal', `DataChange_CreatedBy` varchar(32) NOT NULL DEFAULT 'default' COMMENT '创建人邮箱前缀', `DataChange_CreatedTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `DataChange_LastModifiedBy` varchar(32) DEFAULT '' COMMENT '最后修改人邮箱前缀', `DataChange_LastTime` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改时间', PRIMARY KEY (`Id`), KEY `DataChange_LastTime` (`DataChange_LastTime`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='日志审计表'; -- Dump of table cluster -- ------------------------------------------------------------ DROP TABLE IF EXISTS `Cluster`; CREATE TABLE `Cluster` ( `Id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主键', `Name` varchar(32) NOT NULL DEFAULT '' COMMENT '集群名字', `AppId` varchar(32) NOT NULL DEFAULT '' COMMENT 'App id', `ParentClusterId` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '父cluster', `IsDeleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '1: deleted, 0: normal', `DataChange_CreatedBy` varchar(32) NOT NULL DEFAULT '' COMMENT '创建人邮箱前缀', `DataChange_CreatedTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `DataChange_LastModifiedBy` varchar(32) DEFAULT '' COMMENT '最后修改人邮箱前缀', `DataChange_LastTime` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改时间', PRIMARY KEY (`Id`), KEY `IX_AppId_Name` (`AppId`,`Name`), KEY `DataChange_LastTime` (`DataChange_LastTime`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='集群'; -- Dump of table commit -- ------------------------------------------------------------ DROP TABLE IF EXISTS `Commit`; CREATE TABLE `Commit` ( `Id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键', `ChangeSets` longtext NOT NULL COMMENT '修改变更集', `AppId` varchar(500) NOT NULL DEFAULT 'default' COMMENT 'AppID', `ClusterName` varchar(500) NOT NULL DEFAULT 'default' COMMENT 'ClusterName', `NamespaceName` varchar(500) NOT NULL DEFAULT 'default' COMMENT 'namespaceName', `Comment` varchar(500) DEFAULT NULL COMMENT '备注', `IsDeleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '1: deleted, 0: normal', `DataChange_CreatedBy` varchar(32) NOT NULL DEFAULT 'default' COMMENT '创建人邮箱前缀', `DataChange_CreatedTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `DataChange_LastModifiedBy` varchar(32) DEFAULT '' COMMENT '最后修改人邮箱前缀', `DataChange_LastTime` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改时间', PRIMARY KEY (`Id`), KEY `DataChange_LastTime` (`DataChange_LastTime`), KEY `AppId` (`AppId`(191)), KEY `ClusterName` (`ClusterName`(191)), KEY `NamespaceName` (`NamespaceName`(191)) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='commit 历史表'; -- Dump of table grayreleaserule -- ------------------------------------------------------------ DROP TABLE IF EXISTS `GrayReleaseRule`; CREATE TABLE `GrayReleaseRule` ( `Id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键', `AppId` varchar(32) NOT NULL DEFAULT 'default' COMMENT 'AppID', `ClusterName` varchar(32) NOT NULL DEFAULT 'default' COMMENT 'Cluster Name', `NamespaceName` varchar(32) NOT NULL DEFAULT 'default' COMMENT 'Namespace Name', `BranchName` varchar(32) NOT NULL DEFAULT 'default' COMMENT 'branch name', `Rules` varchar(16000) DEFAULT '[]' COMMENT '灰度规则', `ReleaseId` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '灰度对应的release', `BranchStatus` tinyint(2) DEFAULT '1' COMMENT '灰度分支状态: 0:删除分支,1:正在使用的规则 2:全量发布', `IsDeleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '1: deleted, 0: normal', `DataChange_CreatedBy` varchar(32) NOT NULL DEFAULT 'default' COMMENT '创建人邮箱前缀', `DataChange_CreatedTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `DataChange_LastModifiedBy` varchar(32) DEFAULT '' COMMENT '最后修改人邮箱前缀', `DataChange_LastTime` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改时间', PRIMARY KEY (`Id`), KEY `DataChange_LastTime` (`DataChange_LastTime`), KEY `IX_Namespace` (`AppId`,`ClusterName`,`NamespaceName`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='灰度规则表'; -- Dump of table instance -- ------------------------------------------------------------ DROP TABLE IF EXISTS `Instance`; CREATE TABLE `Instance` ( `Id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增Id', `AppId` varchar(32) NOT NULL DEFAULT 'default' COMMENT 'AppID', `ClusterName` varchar(32) NOT NULL DEFAULT 'default' COMMENT 'ClusterName', `DataCenter` varchar(64) NOT NULL DEFAULT 'default' COMMENT 'Data Center Name', `Ip` varchar(32) NOT NULL DEFAULT '' COMMENT 'instance ip', `DataChange_CreatedTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `DataChange_LastTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改时间', PRIMARY KEY (`Id`), UNIQUE KEY `IX_UNIQUE_KEY` (`AppId`,`ClusterName`,`Ip`,`DataCenter`), KEY `IX_IP` (`Ip`), KEY `IX_DataChange_LastTime` (`DataChange_LastTime`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='使用配置的应用实例'; -- Dump of table instanceconfig -- ------------------------------------------------------------ DROP TABLE IF EXISTS `InstanceConfig`; CREATE TABLE `InstanceConfig` ( `Id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增Id', `InstanceId` int(11) unsigned DEFAULT NULL COMMENT 'Instance Id', `ConfigAppId` varchar(32) NOT NULL DEFAULT 'default' COMMENT 'Config App Id', `ConfigClusterName` varchar(32) NOT NULL DEFAULT 'default' COMMENT 'Config Cluster Name', `ConfigNamespaceName` varchar(32) NOT NULL DEFAULT 'default' COMMENT 'Config Namespace Name', `ReleaseKey` varchar(64) NOT NULL DEFAULT '' COMMENT '发布的Key', `ReleaseDeliveryTime` timestamp NULL DEFAULT NULL COMMENT '配置获取时间', `DataChange_CreatedTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `DataChange_LastTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改时间', PRIMARY KEY (`Id`), UNIQUE KEY `IX_UNIQUE_KEY` (`InstanceId`,`ConfigAppId`,`ConfigNamespaceName`), KEY `IX_ReleaseKey` (`ReleaseKey`), KEY `IX_DataChange_LastTime` (`DataChange_LastTime`), KEY `IX_Valid_Namespace` (`ConfigAppId`,`ConfigClusterName`,`ConfigNamespaceName`,`DataChange_LastTime`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='应用实例的配置信息'; -- Dump of table item -- ------------------------------------------------------------ DROP TABLE IF EXISTS `Item`; CREATE TABLE `Item` ( `Id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增Id', `NamespaceId` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '集群NamespaceId', `Key` varchar(128) NOT NULL DEFAULT 'default' COMMENT '配置项Key', `Value` longtext NOT NULL COMMENT '配置项值', `Comment` varchar(1024) DEFAULT '' COMMENT '注释', `LineNum` int(10) unsigned DEFAULT '0' COMMENT '行号', `IsDeleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '1: deleted, 0: normal', `DataChange_CreatedBy` varchar(32) NOT NULL DEFAULT 'default' COMMENT '创建人邮箱前缀', `DataChange_CreatedTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `DataChange_LastModifiedBy` varchar(32) DEFAULT '' COMMENT '最后修改人邮箱前缀', `DataChange_LastTime` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改时间', PRIMARY KEY (`Id`), KEY `IX_GroupId` (`NamespaceId`), KEY `DataChange_LastTime` (`DataChange_LastTime`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='配置项目'; -- Dump of table namespace -- ------------------------------------------------------------ DROP TABLE IF EXISTS `Namespace`; CREATE TABLE `Namespace` ( `Id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主键', `AppId` varchar(500) NOT NULL DEFAULT 'default' COMMENT 'AppID', `ClusterName` varchar(500) NOT NULL DEFAULT 'default' COMMENT 'Cluster Name', `NamespaceName` varchar(500) NOT NULL DEFAULT 'default' COMMENT 'Namespace Name', `IsDeleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '1: deleted, 0: normal', `DataChange_CreatedBy` varchar(32) NOT NULL DEFAULT 'default' COMMENT '创建人邮箱前缀', `DataChange_CreatedTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `DataChange_LastModifiedBy` varchar(32) DEFAULT '' COMMENT '最后修改人邮箱前缀', `DataChange_LastTime` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改时间', PRIMARY KEY (`Id`), KEY `AppId_ClusterName_NamespaceName` (`AppId`(191),`ClusterName`(191),`NamespaceName`(191)), KEY `DataChange_LastTime` (`DataChange_LastTime`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='命名空间'; -- Dump of table namespacelock -- ------------------------------------------------------------ DROP TABLE IF EXISTS `NamespaceLock`; CREATE TABLE `NamespaceLock` ( `Id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增id', `NamespaceId` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '集群NamespaceId', `DataChange_CreatedBy` varchar(32) NOT NULL DEFAULT 'default' COMMENT '创建人邮箱前缀', `DataChange_CreatedTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `DataChange_LastModifiedBy` varchar(32) DEFAULT 'default' COMMENT '最后修改人邮箱前缀', `DataChange_LastTime` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改时间', `IsDeleted` bit(1) DEFAULT b'0' COMMENT '软删除', PRIMARY KEY (`Id`), UNIQUE KEY `IX_NamespaceId` (`NamespaceId`), KEY `DataChange_LastTime` (`DataChange_LastTime`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='namespace的编辑锁'; -- Dump of table privilege -- ------------------------------------------------------------ DROP TABLE IF EXISTS `Privilege`; CREATE TABLE `Privilege` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键', `Name` varchar(500) NOT NULL DEFAULT 'default' COMMENT 'Name', `PrivilType` varchar(50) NOT NULL DEFAULT 'default' COMMENT 'PrivilType', `NamespaceId` int(10) unsigned NOT NULL DEFAULT '0' COMMENT 'NamespaceId', `IsDeleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '1: deleted, 0: normal', `DataChange_CreatedBy` varchar(32) NOT NULL DEFAULT 'default' COMMENT '创建人邮箱前缀', `DataChange_CreatedTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `DataChange_LastModifiedBy` varchar(32) DEFAULT '' COMMENT '最后修改人邮箱前缀', `DataChange_LastTime` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改时间', PRIMARY KEY (`id`), KEY `Name_PrivilType_NamespaceId` (`Name`(191),`PrivilType`,`NamespaceId`), KEY `DataChange_LastTime` (`DataChange_LastTime`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='权限'; -- Dump of table release -- ------------------------------------------------------------ DROP TABLE IF EXISTS `Release`; CREATE TABLE `Release` ( `Id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主键', `ReleaseKey` varchar(64) NOT NULL DEFAULT '' COMMENT '发布的Key', `Name` varchar(64) NOT NULL DEFAULT 'default' COMMENT '发布名字', `Comment` varchar(256) DEFAULT NULL COMMENT '发布说明', `AppId` varchar(500) NOT NULL DEFAULT 'default' COMMENT 'AppID', `ClusterName` varchar(500) NOT NULL DEFAULT 'default' COMMENT 'ClusterName', `NamespaceName` varchar(500) NOT NULL DEFAULT 'default' COMMENT 'namespaceName', `Configurations` longtext NOT NULL COMMENT '发布配置', `IsAbandoned` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否废弃', `Status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '发布的状态,0:废弃 1:正常 2:灰度', `IsDeleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '1: deleted, 0: normal', `DataChange_CreatedBy` varchar(32) NOT NULL DEFAULT 'default' COMMENT '创建人邮箱前缀', `DataChange_CreatedTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `DataChange_LastModifiedBy` varchar(32) DEFAULT '' COMMENT '最后修改人邮箱前缀', `DataChange_LastTime` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改时间', PRIMARY KEY (`Id`), KEY `AppId_ClusterName_GroupName` (`AppId`(191),`ClusterName`(191),`NamespaceName`(191)), KEY `DataChange_LastTime` (`DataChange_LastTime`), KEY `IX_ReleaseKey` (`ReleaseKey`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='发布'; -- Dump of table releasehistory -- ------------------------------------------------------------ DROP TABLE IF EXISTS `ReleaseHistory`; CREATE TABLE `ReleaseHistory` ( `Id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增Id', `AppId` varchar(32) NOT NULL DEFAULT 'default' COMMENT 'AppID', `ClusterName` varchar(32) NOT NULL DEFAULT 'default' COMMENT 'ClusterName', `NamespaceName` varchar(32) NOT NULL DEFAULT 'default' COMMENT 'namespaceName', `BranchName` varchar(32) NOT NULL DEFAULT 'default' COMMENT '发布分支名', `ReleaseId` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '关联的Release Id', `PreviousReleaseId` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '前一次发布的ReleaseId', `Operation` tinyint(3) unsigned NOT NULL DEFAULT '0' COMMENT '发布类型,0: 普通发布,1: 回滚,2: 灰度发布,3: 灰度规则更新,4: 灰度合并回主分支发布,5: 主分支发布灰度自动发布,6: 主分支回滚灰度自动发布,7: 放弃灰度', `OperationContext` longtext NOT NULL COMMENT '发布上下文信息', `IsDeleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '1: deleted, 0: normal', `DataChange_CreatedBy` varchar(32) NOT NULL DEFAULT 'default' COMMENT '创建人邮箱前缀', `DataChange_CreatedTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `DataChange_LastModifiedBy` varchar(32) DEFAULT '' COMMENT '最后修改人邮箱前缀', `DataChange_LastTime` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改时间', PRIMARY KEY (`Id`), KEY `IX_Namespace` (`AppId`,`ClusterName`,`NamespaceName`,`BranchName`), KEY `IX_ReleaseId` (`ReleaseId`), KEY `IX_DataChange_LastTime` (`DataChange_LastTime`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='发布历史'; -- Dump of table releasemessage -- ------------------------------------------------------------ DROP TABLE IF EXISTS `ReleaseMessage`; CREATE TABLE `ReleaseMessage` ( `Id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主键', `Message` varchar(1024) NOT NULL DEFAULT '' COMMENT '发布的消息内容', `DataChange_LastTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改时间', PRIMARY KEY (`Id`), KEY `DataChange_LastTime` (`DataChange_LastTime`), KEY `IX_Message` (`Message`(191)) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='发布消息'; -- Dump of table serverconfig -- ------------------------------------------------------------ DROP TABLE IF EXISTS `ServerConfig`; CREATE TABLE `ServerConfig` ( `Id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增Id', `Key` varchar(64) NOT NULL DEFAULT 'default' COMMENT '配置项Key', `Cluster` varchar(32) NOT NULL DEFAULT 'default' COMMENT '配置对应的集群,default为不针对特定的集群', `Value` varchar(2048) NOT NULL DEFAULT 'default' COMMENT '配置项值', `Comment` varchar(1024) DEFAULT '' COMMENT '注释', `IsDeleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '1: deleted, 0: normal', `DataChange_CreatedBy` varchar(32) NOT NULL DEFAULT 'default' COMMENT '创建人邮箱前缀', `DataChange_CreatedTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `DataChange_LastModifiedBy` varchar(32) DEFAULT '' COMMENT '最后修改人邮箱前缀', `DataChange_LastTime` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改时间', PRIMARY KEY (`Id`), KEY `IX_Key` (`Key`), KEY `DataChange_LastTime` (`DataChange_LastTime`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='配置服务自身配置'; -- Config -- ------------------------------------------------------------ INSERT INTO `ServerConfig` (`Key`, `Cluster`, `Value`, `Comment`) VALUES ('eureka.service.url', 'default', 'http://localhost:8080/eureka/', 'Eureka服务Url'), ('namespace.lock.switch', 'default', 'false', '一次发布只能有一个人修改开关'), ('item.value.length.limit', 'default', '20000', 'item value最大长度限制'), ('appnamespace.private.enable', 'default', 'false', '是否开启private namespace'), ('item.key.length.limit', 'default', '128', 'item key 最大长度限制'); -- ${gists.autoGeneratedDeclaration} /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; /*!40101 SET SQL_MODE=@OLD_SQL_MODE */; /*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; /*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; ================================================ FILE: apollo-build-sql-converter/src/test/resources/META-INF/sql/h2-test/delta/v000-v010/apolloconfigdb-v000-v010-before.sql ================================================ -- -- Copyright 2024 Apollo Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. -- You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. -- /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; /*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; /*!40101 SET NAMES utf8 */; /*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; /*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; /*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; -- ${gists.autoGeneratedDeclaration} -- ${gists.h2Function} -- ${gists.setupDatabase} -- ${gists.autoGeneratedDeclaration} ================================================ FILE: apollo-build-sql-converter/src/test/resources/META-INF/sql/h2-test/delta/v000-v010/apolloconfigdb-v000-v010.sql ================================================ -- -- Copyright 2024 Apollo Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. -- You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. -- -- delta schema to upgrade apollo config db from v0.0.0 to v0.1.0 -- ${gists.autoGeneratedDeclaration} -- ${gists.h2Function} -- ${gists.useDatabase} -- ${gists.autoGeneratedDeclaration} ================================================ FILE: apollo-build-sql-converter/src/test/resources/META-INF/sql/h2-test/delta/v000-v010/apolloportaldb-v000-v010-base.sql ================================================ -- -- Copyright 2024 Apollo Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. -- You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. -- /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; /*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; /*!40101 SET NAMES utf8 */; /*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; /*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; /*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; -- ${gists.autoGeneratedDeclaration} -- ${gists.h2Function} -- ${gists.setupDatabase} -- Dump of table app -- ------------------------------------------------------------ DROP TABLE IF EXISTS `App`; CREATE TABLE `App` ( `Id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键', `AppId` varchar(500) NOT NULL DEFAULT 'default' COMMENT 'AppID', `Name` varchar(500) NOT NULL DEFAULT 'default' COMMENT '应用名', `OrgId` varchar(32) NOT NULL DEFAULT 'default' COMMENT '部门Id', `OrgName` varchar(64) NOT NULL DEFAULT 'default' COMMENT '部门名字', `OwnerName` varchar(500) NOT NULL DEFAULT 'default' COMMENT 'ownerName', `OwnerEmail` varchar(500) NOT NULL DEFAULT 'default' COMMENT 'ownerEmail', `IsDeleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '1: deleted, 0: normal', `DataChange_CreatedBy` varchar(32) NOT NULL DEFAULT 'default' COMMENT '创建人邮箱前缀', `DataChange_CreatedTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间', `DataChange_LastModifiedBy` varchar(32) DEFAULT '' COMMENT '最后修改人邮箱前缀', `DataChange_LastTime` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改时间', PRIMARY KEY (`Id`), KEY `AppId` (`AppId`(191)), KEY `DataChange_LastTime` (`DataChange_LastTime`), KEY `Name` (`Name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='应用表'; -- Dump of table appnamespace -- ------------------------------------------------------------ DROP TABLE IF EXISTS `AppNamespace`; CREATE TABLE `AppNamespace` ( `Id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主键', `Name` varchar(32) NOT NULL DEFAULT '' COMMENT 'namespace名字,注意,需要全局唯一', `AppId` varchar(32) NOT NULL DEFAULT '' COMMENT 'app id', `Format` varchar(32) NOT NULL DEFAULT 'properties' COMMENT 'namespace的format类型', `IsPublic` bit(1) NOT NULL DEFAULT b'0' COMMENT 'namespace是否为公共', `Comment` varchar(64) NOT NULL DEFAULT '' COMMENT '注释', `IsDeleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '1: deleted, 0: normal', `DataChange_CreatedBy` varchar(32) NOT NULL DEFAULT '' COMMENT '创建人邮箱前缀', `DataChange_CreatedTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `DataChange_LastModifiedBy` varchar(32) DEFAULT '' COMMENT '最后修改人邮箱前缀', `DataChange_LastTime` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改时间', PRIMARY KEY (`Id`), KEY `Name_AppId` (`Name`,`AppId`), KEY `DataChange_LastTime` (`DataChange_LastTime`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='应用namespace定义'; -- Dump of table consumer -- ------------------------------------------------------------ DROP TABLE IF EXISTS `Consumer`; CREATE TABLE `Consumer` ( `Id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增Id', `AppId` varchar(500) NOT NULL DEFAULT 'default' COMMENT 'AppID', `Name` varchar(500) NOT NULL DEFAULT 'default' COMMENT '应用名', `OrgId` varchar(32) NOT NULL DEFAULT 'default' COMMENT '部门Id', `OrgName` varchar(64) NOT NULL DEFAULT 'default' COMMENT '部门名字', `OwnerName` varchar(500) NOT NULL DEFAULT 'default' COMMENT 'ownerName', `OwnerEmail` varchar(500) NOT NULL DEFAULT 'default' COMMENT 'ownerEmail', `IsDeleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '1: deleted, 0: normal', `DataChange_CreatedBy` varchar(32) NOT NULL DEFAULT 'default' COMMENT '创建人邮箱前缀', `DataChange_CreatedTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间', `DataChange_LastModifiedBy` varchar(32) DEFAULT '' COMMENT '最后修改人邮箱前缀', `DataChange_LastTime` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改时间', PRIMARY KEY (`Id`), KEY `AppId` (`AppId`(191)), KEY `DataChange_LastTime` (`DataChange_LastTime`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='开放API消费者'; -- Dump of table consumeraudit -- ------------------------------------------------------------ DROP TABLE IF EXISTS `ConsumerAudit`; CREATE TABLE `ConsumerAudit` ( `Id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增Id', `ConsumerId` int(11) unsigned DEFAULT NULL COMMENT 'Consumer Id', `Uri` varchar(1024) NOT NULL DEFAULT '' COMMENT '访问的Uri', `Method` varchar(16) NOT NULL DEFAULT '' COMMENT '访问的Method', `DataChange_CreatedTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `DataChange_LastTime` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改时间', PRIMARY KEY (`Id`), KEY `IX_DataChange_LastTime` (`DataChange_LastTime`), KEY `IX_ConsumerId` (`ConsumerId`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='consumer审计表'; -- Dump of table consumerrole -- ------------------------------------------------------------ DROP TABLE IF EXISTS `ConsumerRole`; CREATE TABLE `ConsumerRole` ( `Id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增Id', `ConsumerId` int(11) unsigned DEFAULT NULL COMMENT 'Consumer Id', `RoleId` int(10) unsigned DEFAULT NULL COMMENT 'Role Id', `IsDeleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '1: deleted, 0: normal', `DataChange_CreatedBy` varchar(32) DEFAULT '' COMMENT '创建人邮箱前缀', `DataChange_CreatedTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `DataChange_LastModifiedBy` varchar(32) DEFAULT '' COMMENT '最后修改人邮箱前缀', `DataChange_LastTime` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改时间', PRIMARY KEY (`Id`), KEY `IX_DataChange_LastTime` (`DataChange_LastTime`), KEY `IX_RoleId` (`RoleId`), KEY `IX_ConsumerId_RoleId` (`ConsumerId`,`RoleId`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='consumer和role的绑定表'; -- Dump of table consumertoken -- ------------------------------------------------------------ DROP TABLE IF EXISTS `ConsumerToken`; CREATE TABLE `ConsumerToken` ( `Id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增Id', `ConsumerId` int(11) unsigned DEFAULT NULL COMMENT 'ConsumerId', `Token` varchar(128) NOT NULL DEFAULT '' COMMENT 'token', `Expires` datetime NOT NULL DEFAULT '2099-01-01 00:00:00' COMMENT 'token失效时间', `IsDeleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '1: deleted, 0: normal', `DataChange_CreatedBy` varchar(32) NOT NULL DEFAULT 'default' COMMENT '创建人邮箱前缀', `DataChange_CreatedTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间', `DataChange_LastModifiedBy` varchar(32) DEFAULT '' COMMENT '最后修改人邮箱前缀', `DataChange_LastTime` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改时间', PRIMARY KEY (`Id`), UNIQUE KEY `IX_Token` (`Token`), KEY `DataChange_LastTime` (`DataChange_LastTime`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='consumer token表'; -- Dump of table favorite -- ------------------------------------------------------------ DROP TABLE IF EXISTS `Favorite`; CREATE TABLE `Favorite` ( `Id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键', `UserId` varchar(32) NOT NULL DEFAULT 'default' COMMENT '收藏的用户', `AppId` varchar(500) NOT NULL DEFAULT 'default' COMMENT 'AppID', `Position` int(32) NOT NULL DEFAULT '10000' COMMENT '收藏顺序', `IsDeleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '1: deleted, 0: normal', `DataChange_CreatedBy` varchar(32) NOT NULL DEFAULT 'default' COMMENT '创建人邮箱前缀', `DataChange_CreatedTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间', `DataChange_LastModifiedBy` varchar(32) DEFAULT '' COMMENT '最后修改人邮箱前缀', `DataChange_LastTime` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改时间', PRIMARY KEY (`Id`), KEY `AppId` (`AppId`(191)), KEY `IX_UserId` (`UserId`), KEY `DataChange_LastTime` (`DataChange_LastTime`) ) ENGINE=InnoDB AUTO_INCREMENT=23 DEFAULT CHARSET=utf8mb4 COMMENT='应用收藏表'; -- Dump of table permission -- ------------------------------------------------------------ DROP TABLE IF EXISTS `Permission`; CREATE TABLE `Permission` ( `Id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增Id', `PermissionType` varchar(32) NOT NULL DEFAULT '' COMMENT '权限类型', `TargetId` varchar(256) NOT NULL DEFAULT '' COMMENT '权限对象类型', `IsDeleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '1: deleted, 0: normal', `DataChange_CreatedBy` varchar(32) NOT NULL DEFAULT '' COMMENT '创建人邮箱前缀', `DataChange_CreatedTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `DataChange_LastModifiedBy` varchar(32) DEFAULT '' COMMENT '最后修改人邮箱前缀', `DataChange_LastTime` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改时间', PRIMARY KEY (`Id`), KEY `IX_TargetId_PermissionType` (`TargetId`(191),`PermissionType`), KEY `IX_DataChange_LastTime` (`DataChange_LastTime`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='permission表'; -- Dump of table role -- ------------------------------------------------------------ DROP TABLE IF EXISTS `Role`; CREATE TABLE `Role` ( `Id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增Id', `RoleName` varchar(256) NOT NULL DEFAULT '' COMMENT 'Role name', `IsDeleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '1: deleted, 0: normal', `DataChange_CreatedBy` varchar(32) NOT NULL DEFAULT 'default' COMMENT '创建人邮箱前缀', `DataChange_CreatedTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `DataChange_LastModifiedBy` varchar(32) DEFAULT '' COMMENT '最后修改人邮箱前缀', `DataChange_LastTime` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改时间', PRIMARY KEY (`Id`), KEY `IX_RoleName` (`RoleName`(191)), KEY `IX_DataChange_LastTime` (`DataChange_LastTime`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色表'; -- Dump of table rolepermission -- ------------------------------------------------------------ DROP TABLE IF EXISTS `RolePermission`; CREATE TABLE `RolePermission` ( `Id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增Id', `RoleId` int(10) unsigned DEFAULT NULL COMMENT 'Role Id', `PermissionId` int(10) unsigned DEFAULT NULL COMMENT 'Permission Id', `IsDeleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '1: deleted, 0: normal', `DataChange_CreatedBy` varchar(32) DEFAULT '' COMMENT '创建人邮箱前缀', `DataChange_CreatedTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `DataChange_LastModifiedBy` varchar(32) DEFAULT '' COMMENT '最后修改人邮箱前缀', `DataChange_LastTime` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改时间', PRIMARY KEY (`Id`), KEY `IX_DataChange_LastTime` (`DataChange_LastTime`), KEY `IX_RoleId` (`RoleId`), KEY `IX_PermissionId` (`PermissionId`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色和权限的绑定表'; -- Dump of table serverconfig -- ------------------------------------------------------------ DROP TABLE IF EXISTS `ServerConfig`; CREATE TABLE `ServerConfig` ( `Id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增Id', `Key` varchar(64) NOT NULL DEFAULT 'default' COMMENT '配置项Key', `Value` varchar(2048) NOT NULL DEFAULT 'default' COMMENT '配置项值', `Comment` varchar(1024) DEFAULT '' COMMENT '注释', `IsDeleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '1: deleted, 0: normal', `DataChange_CreatedBy` varchar(32) NOT NULL DEFAULT 'default' COMMENT '创建人邮箱前缀', `DataChange_CreatedTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `DataChange_LastModifiedBy` varchar(32) DEFAULT '' COMMENT '最后修改人邮箱前缀', `DataChange_LastTime` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改时间', PRIMARY KEY (`Id`), KEY `IX_Key` (`Key`), KEY `DataChange_LastTime` (`DataChange_LastTime`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='配置服务自身配置'; -- Dump of table userrole -- ------------------------------------------------------------ DROP TABLE IF EXISTS `UserRole`; CREATE TABLE `UserRole` ( `Id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增Id', `UserId` varchar(128) DEFAULT '' COMMENT '用户身份标识', `RoleId` int(10) unsigned DEFAULT NULL COMMENT 'Role Id', `IsDeleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '1: deleted, 0: normal', `DataChange_CreatedBy` varchar(32) DEFAULT '' COMMENT '创建人邮箱前缀', `DataChange_CreatedTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `DataChange_LastModifiedBy` varchar(32) DEFAULT '' COMMENT '最后修改人邮箱前缀', `DataChange_LastTime` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改时间', PRIMARY KEY (`Id`), KEY `IX_DataChange_LastTime` (`DataChange_LastTime`), KEY `IX_RoleId` (`RoleId`), KEY `IX_UserId_RoleId` (`UserId`,`RoleId`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户和role的绑定表'; -- Config -- ------------------------------------------------------------ INSERT INTO `ServerConfig` (`Key`, `Value`, `Comment`) VALUES ('apollo.portal.envs', 'dev', '可支持的环境列表'), ('organizations', '[{\"orgId\":\"TEST1\",\"orgName\":\"样例部门1\"},{\"orgId\":\"TEST2\",\"orgName\":\"样例部门2\"}]', '部门列表'), ('superAdmin', 'apollo', 'Portal超级管理员'), ('api.readTimeout', '10000', 'http接口read timeout'), ('consumer.token.salt', 'someSalt', 'consumer token salt'); -- ${gists.autoGeneratedDeclaration} /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; /*!40101 SET SQL_MODE=@OLD_SQL_MODE */; /*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; /*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; ================================================ FILE: apollo-build-sql-converter/src/test/resources/META-INF/sql/h2-test/delta/v000-v010/apolloportaldb-v000-v010.sql ================================================ -- -- Copyright 2024 Apollo Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. -- You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. -- -- delta schema to upgrade apollo portal db from v0.4.0 to v0.5.0 -- ${gists.autoGeneratedDeclaration} -- ${gists.h2Function} -- ${gists.useDatabase} -- ${gists.autoGeneratedDeclaration} ================================================ FILE: apollo-build-sql-converter/src/test/resources/META-INF/sql/h2-test/delta/v040-v050/apolloconfigdb-v040-v050.sql ================================================ -- -- Copyright 2024 Apollo Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. -- You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. -- -- delta schema to upgrade apollo config db from v0.4.0 to v0.5.0 -- ${gists.autoGeneratedDeclaration} -- ${gists.h2Function} -- ${gists.useDatabase} DROP TABLE `Privilege`; ALTER TABLE `Release` DROP `Status`; ALTER TABLE `Namespace` ADD KEY `IX_NamespaceName` (`NamespaceName`(191)); ALTER TABLE `Cluster` ADD KEY `IX_ParentClusterId` (`ParentClusterId`); ALTER TABLE `AppNamespace` ADD KEY `IX_AppId` (`AppId`); ALTER TABLE `App` DROP INDEX `Name`; ALTER TABLE `App` ADD KEY `Name` (`Name`); -- ${gists.autoGeneratedDeclaration} ================================================ FILE: apollo-build-sql-converter/src/test/resources/META-INF/sql/h2-test/delta/v040-v050/apolloportaldb-v040-v050.sql ================================================ -- -- Copyright 2024 Apollo Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. -- You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. -- -- delta schema to upgrade apollo portal db from v0.4.0 to v0.5.0 -- ${gists.autoGeneratedDeclaration} -- ${gists.h2Function} -- ${gists.useDatabase} ALTER TABLE `AppNamespace` ADD KEY `IX_AppId` (`AppId`); ALTER TABLE `App` DROP INDEX `Name`; ALTER TABLE `App` ADD KEY `Name` (`Name`); -- ${gists.autoGeneratedDeclaration} ================================================ FILE: apollo-build-sql-converter/src/test/resources/META-INF/sql/h2-test/delta/v060-v062/apolloconfigdb-v060-v062.sql ================================================ -- -- Copyright 2024 Apollo Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. -- You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. -- -- delta schema to upgrade apollo config db from v0.6.0 to v0.6.2 -- ${gists.autoGeneratedDeclaration} -- ${gists.h2Function} -- ${gists.useDatabase} ALTER TABLE `App` DROP INDEX `Name`; CREATE INDEX `IX_NAME` ON App (`Name`(191)); -- ${gists.autoGeneratedDeclaration} ================================================ FILE: apollo-build-sql-converter/src/test/resources/META-INF/sql/h2-test/delta/v060-v062/apolloportaldb-v060-v062.sql ================================================ -- -- Copyright 2024 Apollo Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. -- You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. -- -- delta schema to upgrade apollo portal db from v0.6.0 to v0.6.2 -- ${gists.autoGeneratedDeclaration} -- ${gists.h2Function} -- ${gists.useDatabase} ALTER TABLE `App` DROP INDEX `Name`; CREATE INDEX `IX_NAME` ON App (`Name`(191)); -- ${gists.autoGeneratedDeclaration} ================================================ FILE: apollo-build-sql-converter/src/test/resources/META-INF/sql/h2-test/delta/v080-v090/apolloportaldb-v080-v090.sql ================================================ -- -- Copyright 2024 Apollo Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. -- You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. -- -- delta schema to upgrade apollo portal db from v0.8.0 to v0.9.0 -- ${gists.autoGeneratedDeclaration} -- ${gists.h2Function} -- ${gists.useDatabase} CREATE TABLE `Users` ( `Id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增Id', `Username` varchar(64) NOT NULL DEFAULT 'default' COMMENT '用户名', `Password` varchar(64) NOT NULL DEFAULT 'default' COMMENT '密码', `Email` varchar(64) NOT NULL DEFAULT 'default' COMMENT '邮箱地址', `Enabled` tinyint(4) DEFAULT NULL COMMENT '是否有效', PRIMARY KEY (`Id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; CREATE TABLE `Authorities` ( `Id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增Id', `Username` varchar(50) NOT NULL, `Authority` varchar(50) NOT NULL, PRIMARY KEY (`Id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; INSERT INTO `Users` (`Username`, `Password`, `Email`, `Enabled`) VALUES ('apollo', '$2a$10$7r20uS.BQ9uBpf3Baj3uQOZvMVvB1RN3PYoKE94gtz2.WAOuiiwXS', 'apollo@acme.com', 1); INSERT INTO `Authorities` (`Username`, `Authority`) VALUES ('apollo', 'ROLE_user'); -- ${gists.autoGeneratedDeclaration} ================================================ FILE: apollo-build-sql-converter/src/test/resources/META-INF/sql/h2-test/delta/v151-v160/apolloconfigdb-v151-v160.sql ================================================ -- -- Copyright 2024 Apollo Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. -- You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. -- -- delta schema to upgrade apollo config db from v1.5.1 to v1.6.0 -- ${gists.autoGeneratedDeclaration} -- ${gists.h2Function} -- ${gists.useDatabase} CREATE TABLE `AccessKey` ( `Id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主键', `AppId` varchar(500) NOT NULL DEFAULT 'default' COMMENT 'AppID', `Secret` varchar(128) NOT NULL DEFAULT '' COMMENT 'Secret', `IsEnabled` bit(1) NOT NULL DEFAULT b'0' COMMENT '1: enabled, 0: disabled', `IsDeleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '1: deleted, 0: normal', `DataChange_CreatedBy` varchar(32) NOT NULL DEFAULT 'default' COMMENT '创建人邮箱前缀', `DataChange_CreatedTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `DataChange_LastModifiedBy` varchar(32) NOT NULL DEFAULT '' COMMENT '最后修改人邮箱前缀', `DataChange_LastTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改时间', PRIMARY KEY (`Id`), KEY `AppId` (`AppId`(191)), KEY `DataChange_LastTime` (`DataChange_LastTime`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='访问密钥'; -- ${gists.autoGeneratedDeclaration} ================================================ FILE: apollo-build-sql-converter/src/test/resources/META-INF/sql/h2-test/delta/v170-v180/apolloconfigdb-v170-v180.sql ================================================ -- -- Copyright 2024 Apollo Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. -- You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. -- -- delta schema to upgrade apollo config db from v1.7.0 to v1.8.0 -- ${gists.autoGeneratedDeclaration} -- ${gists.h2Function} -- ${gists.useDatabase} alter table `AppNamespace` change AppId AppId varchar(64) NOT NULL DEFAULT 'default' COMMENT 'app id'; alter table `Cluster` change AppId AppId varchar(64) NOT NULL DEFAULT 'default' COMMENT 'app id'; alter table `GrayReleaseRule` change AppId AppId varchar(64) NOT NULL DEFAULT 'default' COMMENT 'app id'; alter table `Instance` change AppId AppId varchar(64) NOT NULL DEFAULT 'default' COMMENT 'app id'; alter table `InstanceConfig` change ConfigAppId ConfigAppId varchar(64) NOT NULL DEFAULT 'default' COMMENT 'Config App Id'; alter table `ReleaseHistory` change AppId AppId varchar(64) NOT NULL DEFAULT 'default' COMMENT 'app id'; -- ${gists.autoGeneratedDeclaration} ================================================ FILE: apollo-build-sql-converter/src/test/resources/META-INF/sql/h2-test/delta/v170-v180/apolloportaldb-v170-v180.sql ================================================ -- -- Copyright 2024 Apollo Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. -- You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. -- -- delta schema to upgrade apollo portal db from v1.7.0 to v1.8.0 -- ${gists.autoGeneratedDeclaration} -- ${gists.h2Function} -- ${gists.useDatabase} alter table `AppNamespace` change AppId AppId varchar(64) NOT NULL DEFAULT 'default' COMMENT 'app id'; -- ${gists.autoGeneratedDeclaration} ================================================ FILE: apollo-build-sql-converter/src/test/resources/META-INF/sql/h2-test/delta/v180-v190/apolloconfigdb-v180-v190.sql ================================================ -- -- Copyright 2024 Apollo Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. -- You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. -- -- delta schema to upgrade apollo config db from v1.8.0 to v1.9.0 -- ${gists.autoGeneratedDeclaration} -- ${gists.h2Function} -- ${gists.useDatabase} ALTER TABLE `App` MODIFY COLUMN `DataChange_CreatedBy` VARCHAR(64) NOT NULL DEFAULT 'default' COMMENT '创建人邮箱前缀', MODIFY COLUMN `DataChange_LastModifiedBy` VARCHAR(64) DEFAULT '' COMMENT '最后修改人邮箱前缀'; ALTER TABLE `AppNamespace` MODIFY COLUMN `DataChange_CreatedBy` VARCHAR(64) NOT NULL DEFAULT 'default' COMMENT '创建人邮箱前缀', MODIFY COLUMN `DataChange_LastModifiedBy` VARCHAR(64) DEFAULT '' COMMENT '最后修改人邮箱前缀'; ALTER TABLE `Audit` MODIFY COLUMN `DataChange_CreatedBy` VARCHAR(64) NOT NULL DEFAULT 'default' COMMENT '创建人邮箱前缀', MODIFY COLUMN `DataChange_LastModifiedBy` VARCHAR(64) DEFAULT '' COMMENT '最后修改人邮箱前缀'; ALTER TABLE `Cluster` MODIFY COLUMN `DataChange_CreatedBy` VARCHAR(64) NOT NULL DEFAULT 'default' COMMENT '创建人邮箱前缀', MODIFY COLUMN `DataChange_LastModifiedBy` VARCHAR(64) DEFAULT '' COMMENT '最后修改人邮箱前缀'; ALTER TABLE `Commit` MODIFY COLUMN `DataChange_CreatedBy` VARCHAR(64) NOT NULL DEFAULT 'default' COMMENT '创建人邮箱前缀', MODIFY COLUMN `DataChange_LastModifiedBy` VARCHAR(64) DEFAULT '' COMMENT '最后修改人邮箱前缀'; ALTER TABLE `GrayReleaseRule` MODIFY COLUMN `DataChange_CreatedBy` VARCHAR(64) NOT NULL DEFAULT 'default' COMMENT '创建人邮箱前缀', MODIFY COLUMN `DataChange_LastModifiedBy` VARCHAR(64) DEFAULT '' COMMENT '最后修改人邮箱前缀'; ALTER TABLE `Item` MODIFY COLUMN `DataChange_CreatedBy` VARCHAR(64) NOT NULL DEFAULT 'default' COMMENT '创建人邮箱前缀', MODIFY COLUMN `DataChange_LastModifiedBy` VARCHAR(64) DEFAULT '' COMMENT '最后修改人邮箱前缀'; ALTER TABLE `Namespace` MODIFY COLUMN `DataChange_CreatedBy` VARCHAR(64) NOT NULL DEFAULT 'default' COMMENT '创建人邮箱前缀', MODIFY COLUMN `DataChange_LastModifiedBy` VARCHAR(64) DEFAULT '' COMMENT '最后修改人邮箱前缀'; ALTER TABLE `NamespaceLock` MODIFY COLUMN `DataChange_CreatedBy` VARCHAR(64) NOT NULL DEFAULT 'default' COMMENT '创建人邮箱前缀', MODIFY COLUMN `DataChange_LastModifiedBy` VARCHAR(64) DEFAULT '' COMMENT '最后修改人邮箱前缀'; ALTER TABLE `Release` MODIFY COLUMN `DataChange_CreatedBy` VARCHAR(64) NOT NULL DEFAULT 'default' COMMENT '创建人邮箱前缀', MODIFY COLUMN `DataChange_LastModifiedBy` VARCHAR(64) DEFAULT '' COMMENT '最后修改人邮箱前缀'; ALTER TABLE `ReleaseHistory` MODIFY COLUMN `DataChange_CreatedBy` VARCHAR(64) NOT NULL DEFAULT 'default' COMMENT '创建人邮箱前缀', MODIFY COLUMN `DataChange_LastModifiedBy` VARCHAR(64) DEFAULT '' COMMENT '最后修改人邮箱前缀'; ALTER TABLE `ServerConfig` MODIFY COLUMN `DataChange_CreatedBy` VARCHAR(64) NOT NULL DEFAULT 'default' COMMENT '创建人邮箱前缀', MODIFY COLUMN `DataChange_LastModifiedBy` VARCHAR(64) DEFAULT '' COMMENT '最后修改人邮箱前缀'; ALTER TABLE `AccessKey` MODIFY COLUMN `DataChange_CreatedBy` VARCHAR(64) NOT NULL DEFAULT 'default' COMMENT '创建人邮箱前缀', MODIFY COLUMN `DataChange_LastModifiedBy` VARCHAR(64) DEFAULT '' COMMENT '最后修改人邮箱前缀'; -- ${gists.autoGeneratedDeclaration} ================================================ FILE: apollo-build-sql-converter/src/test/resources/META-INF/sql/h2-test/delta/v180-v190/apolloportaldb-v180-v190.sql ================================================ -- -- Copyright 2024 Apollo Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. -- You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. -- -- delta schema to upgrade apollo portal db from v1.8.0 to v1.9.0 -- ${gists.autoGeneratedDeclaration} -- ${gists.h2Function} -- ${gists.useDatabase} ALTER TABLE `App` MODIFY COLUMN `DataChange_CreatedBy` VARCHAR(64) NOT NULL DEFAULT 'default' COMMENT '创建人邮箱前缀', MODIFY COLUMN `DataChange_LastModifiedBy` VARCHAR(64) DEFAULT '' COMMENT '最后修改人邮箱前缀'; ALTER TABLE `AppNamespace` MODIFY COLUMN `DataChange_CreatedBy` VARCHAR(64) NOT NULL DEFAULT 'default' COMMENT '创建人邮箱前缀', MODIFY COLUMN `DataChange_LastModifiedBy` VARCHAR(64) DEFAULT '' COMMENT '最后修改人邮箱前缀'; ALTER TABLE `Consumer` MODIFY COLUMN `DataChange_CreatedBy` VARCHAR(64) NOT NULL DEFAULT 'default' COMMENT '创建人邮箱前缀', MODIFY COLUMN `DataChange_LastModifiedBy` VARCHAR(64) DEFAULT '' COMMENT '最后修改人邮箱前缀'; ALTER TABLE `ConsumerRole` MODIFY COLUMN `DataChange_CreatedBy` VARCHAR(64) NOT NULL DEFAULT 'default' COMMENT '创建人邮箱前缀', MODIFY COLUMN `DataChange_LastModifiedBy` VARCHAR(64) DEFAULT '' COMMENT '最后修改人邮箱前缀'; ALTER TABLE `ConsumerToken` MODIFY COLUMN `DataChange_CreatedBy` VARCHAR(64) NOT NULL DEFAULT 'default' COMMENT '创建人邮箱前缀', MODIFY COLUMN `DataChange_LastModifiedBy` VARCHAR(64) DEFAULT '' COMMENT '最后修改人邮箱前缀'; ALTER TABLE `Favorite` MODIFY COLUMN `DataChange_CreatedBy` VARCHAR(64) NOT NULL DEFAULT 'default' COMMENT '创建人邮箱前缀', MODIFY COLUMN `DataChange_LastModifiedBy` VARCHAR(64) DEFAULT '' COMMENT '最后修改人邮箱前缀'; ALTER TABLE `Permission` MODIFY COLUMN `DataChange_CreatedBy` VARCHAR(64) NOT NULL DEFAULT 'default' COMMENT '创建人邮箱前缀', MODIFY COLUMN `DataChange_LastModifiedBy` VARCHAR(64) DEFAULT '' COMMENT '最后修改人邮箱前缀'; ALTER TABLE `Role` MODIFY COLUMN `DataChange_CreatedBy` VARCHAR(64) NOT NULL DEFAULT 'default' COMMENT '创建人邮箱前缀', MODIFY COLUMN `DataChange_LastModifiedBy` VARCHAR(64) DEFAULT '' COMMENT '最后修改人邮箱前缀'; ALTER TABLE `RolePermission` MODIFY COLUMN `DataChange_CreatedBy` VARCHAR(64) NOT NULL DEFAULT 'default' COMMENT '创建人邮箱前缀', MODIFY COLUMN `DataChange_LastModifiedBy` VARCHAR(64) DEFAULT '' COMMENT '最后修改人邮箱前缀'; ALTER TABLE `ServerConfig` MODIFY COLUMN `DataChange_CreatedBy` VARCHAR(64) NOT NULL DEFAULT 'default' COMMENT '创建人邮箱前缀', MODIFY COLUMN `DataChange_LastModifiedBy` VARCHAR(64) DEFAULT '' COMMENT '最后修改人邮箱前缀'; ALTER TABLE `UserRole` MODIFY COLUMN `DataChange_CreatedBy` VARCHAR(64) NOT NULL DEFAULT 'default' COMMENT '创建人邮箱前缀', MODIFY COLUMN `DataChange_LastModifiedBy` VARCHAR(64) DEFAULT '' COMMENT '最后修改人邮箱前缀'; ALTER TABLE `Users` MODIFY COLUMN `Username` varchar(64) NOT NULL DEFAULT 'default' COMMENT '用户登录账户', ADD COLUMN `UserDisplayName` varchar(512) NOT NULL DEFAULT 'default' COMMENT '用户名称' AFTER `Password`; UPDATE `Users` SET `UserDisplayName`=`Username` WHERE `UserDisplayName` = 'default'; ALTER TABLE `Users` MODIFY COLUMN `Password` varchar(512) NOT NULL DEFAULT 'default' COMMENT '密码'; UPDATE `Users` SET `Password` = REPLACE(`Password`, '{nonsensical}', '{placeholder}') WHERE `Password` LIKE '{nonsensical}%'; -- note: add the {bcrypt} prefix for `Users`.`Password` is not mandatory, and it may break the old version of apollo-portal while upgrading. -- 注意: 向 `Users`.`Password` 添加 {bcrypt} 是非必须操作, 并且这个操作会导致升级 apollo-portal 集群的过程中旧版的 apollo-portal 无法使用. -- UPDATE `Users` SET `Password` = CONCAT('{bcrypt}', `Password`) WHERE `Password` NOT LIKE '{%}%'; -- spring session (https://github.com/spring-projects/spring-session/blob/faee8f1bdb8822a5653a81eba838dddf224d92d6/spring-session-jdbc/src/main/resources/org/springframework/session/jdbc/schema-mysql.sql) CREATE TABLE SPRING_SESSION ( PRIMARY_ID CHAR(36) NOT NULL, SESSION_ID CHAR(36) NOT NULL, CREATION_TIME BIGINT NOT NULL, LAST_ACCESS_TIME BIGINT NOT NULL, MAX_INACTIVE_INTERVAL INT NOT NULL, EXPIRY_TIME BIGINT NOT NULL, PRINCIPAL_NAME VARCHAR(100), CONSTRAINT SPRING_SESSION_PK PRIMARY KEY (PRIMARY_ID) ) ENGINE=InnoDB ROW_FORMAT=DYNAMIC; CREATE UNIQUE INDEX SPRING_SESSION_IX1 ON SPRING_SESSION (SESSION_ID); CREATE INDEX SPRING_SESSION_IX2 ON SPRING_SESSION (EXPIRY_TIME); CREATE INDEX SPRING_SESSION_IX3 ON SPRING_SESSION (PRINCIPAL_NAME); CREATE TABLE SPRING_SESSION_ATTRIBUTES ( SESSION_PRIMARY_ID CHAR(36) NOT NULL, ATTRIBUTE_NAME VARCHAR(200) NOT NULL, ATTRIBUTE_BYTES BLOB NOT NULL, CONSTRAINT SPRING_SESSION_ATTRIBUTES_PK PRIMARY KEY (SESSION_PRIMARY_ID, ATTRIBUTE_NAME), CONSTRAINT SPRING_SESSION_ATTRIBUTES_FK FOREIGN KEY (SESSION_PRIMARY_ID) REFERENCES SPRING_SESSION(PRIMARY_ID) ON DELETE CASCADE ) ENGINE=InnoDB ROW_FORMAT=DYNAMIC; -- ${gists.autoGeneratedDeclaration} ================================================ FILE: apollo-build-sql-converter/src/test/resources/META-INF/sql/h2-test/delta/v190-v200/apolloconfigdb-v190-v200-after.sql ================================================ -- -- Copyright 2024 Apollo Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. -- You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. -- -- delta schema to upgrade apollo config db from v1.9.0 to v2.0.0 -- ${gists.autoGeneratedDeclaration} -- ${gists.h2Function} -- ${gists.useDatabase} -- Begin:Create indexes to solve the problem of updating large tables ALTER TABLE `Commit` ADD INDEX `idx_IsDeleted_DeletedAt` (`IsDeleted`, `DeletedAt`); ALTER TABLE `Release` ADD INDEX `idx_IsDeleted_DeletedAt` (`IsDeleted`, `DeletedAt`); -- the follow DML won't change the `DataChange_LastTime` field UPDATE `AccessKey` SET `DeletedAt` = -Id, `DataChange_LastTime` = `DataChange_LastTime` WHERE `IsDeleted` = 1 and `DeletedAt` = 0; UPDATE `App` SET `DeletedAt` = -Id, `DataChange_LastTime` = `DataChange_LastTime` WHERE `IsDeleted` = 1 and `DeletedAt` = 0; UPDATE `AppNamespace` SET `DeletedAt` = -Id, `DataChange_LastTime` = `DataChange_LastTime` WHERE `IsDeleted` = 1 and `DeletedAt` = 0; UPDATE `Audit` SET `DeletedAt` = -Id, `DataChange_LastTime` = `DataChange_LastTime` WHERE `IsDeleted` = 1 and `DeletedAt` = 0; UPDATE `Cluster` SET `DeletedAt` = -Id, `DataChange_LastTime` = `DataChange_LastTime` WHERE `IsDeleted` = 1 and `DeletedAt` = 0; UPDATE `Commit` SET `DeletedAt` = -Id, `DataChange_LastTime` = `DataChange_LastTime` WHERE `IsDeleted` = 1 and `DeletedAt` = 0; UPDATE `GrayReleaseRule` SET `DeletedAt` = -Id, `DataChange_LastTime` = `DataChange_LastTime` WHERE `IsDeleted` = 1 and `DeletedAt` = 0; UPDATE `Item` SET `DeletedAt` = -Id, `DataChange_LastTime` = `DataChange_LastTime` WHERE `IsDeleted` = 1 and `DeletedAt` = 0; UPDATE `Namespace` SET `DeletedAt` = -Id, `DataChange_LastTime` = `DataChange_LastTime` WHERE `IsDeleted` = 1 and `DeletedAt` = 0; UPDATE `NamespaceLock` SET `DeletedAt` = -Id, `DataChange_LastTime` = `DataChange_LastTime` WHERE `IsDeleted` = 1 and `DeletedAt` = 0; UPDATE `Release` SET `DeletedAt` = -Id, `DataChange_LastTime` = `DataChange_LastTime` WHERE `IsDeleted` = 1 and `DeletedAt` = 0; UPDATE `ReleaseHistory` SET `DeletedAt` = -Id, `DataChange_LastTime` = `DataChange_LastTime` WHERE `IsDeleted` = 1 and `DeletedAt` = 0; UPDATE `ServerConfig` SET `DeletedAt` = -Id, `DataChange_LastTime` = `DataChange_LastTime` WHERE `IsDeleted` = 1 and `DeletedAt` = 0; -- add UNIQUE CONSTRAINT INDEX for each table ALTER TABLE `AccessKey` ADD UNIQUE INDEX `UK_AppId_Secret_DeletedAt` (`AppId`,`Secret`,`DeletedAt`), DROP INDEX `AppId`; ALTER TABLE `App` ADD UNIQUE INDEX `UK_AppId_DeletedAt` (`AppId`,`DeletedAt`), DROP INDEX `AppId`; ALTER TABLE `AppNamespace` ADD UNIQUE INDEX `UK_AppId_Name_DeletedAt` (`AppId`,`Name`,`DeletedAt`), DROP INDEX `IX_AppId`; -- Ignore TABLE `Audit` ALTER TABLE `Cluster` ADD UNIQUE INDEX `UK_AppId_Name_DeletedAt` (`AppId`,`Name`,`DeletedAt`), DROP INDEX `IX_AppId_Name`; -- Ignore TABLE `Commit` -- Ignore TABLE `GrayReleaseRule`, add unique index in future -- Ignore TABLE `Item`, add unique index in future ALTER TABLE `Namespace` ADD UNIQUE INDEX `UK_AppId_ClusterName_NamespaceName_DeletedAt` (`AppId`(191),`ClusterName`(191),`NamespaceName`(191),`DeletedAt`), DROP INDEX `AppId_ClusterName_NamespaceName`; ALTER TABLE `NamespaceLock` ADD UNIQUE INDEX `UK_NamespaceId_DeletedAt` (`NamespaceId`,`DeletedAt`), DROP INDEX `IX_NamespaceId`; ALTER TABLE `Release` ADD UNIQUE INDEX `UK_ReleaseKey_DeletedAt` (`ReleaseKey`,`DeletedAt`), DROP INDEX `IX_ReleaseKey`; -- Ignore TABLE `ReleaseHistory` ALTER TABLE `ServerConfig` ADD UNIQUE INDEX `UK_Key_Cluster_DeletedAt` (`Key`,`Cluster`,`DeletedAt`), DROP INDEX `IX_Key`; -- End:Delete temporarily created indexes ALTER TABLE `Commit` DROP INDEX `idx_IsDeleted_DeletedAt`; ALTER TABLE `Release` DROP INDEX `idx_IsDeleted_DeletedAt`; -- ${gists.autoGeneratedDeclaration} ================================================ FILE: apollo-build-sql-converter/src/test/resources/META-INF/sql/h2-test/delta/v190-v200/apolloconfigdb-v190-v200.sql ================================================ -- -- Copyright 2024 Apollo Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. -- You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. -- -- delta schema to upgrade apollo config db from v1.9.0 to v2.0.0 -- ${gists.autoGeneratedDeclaration} -- ${gists.h2Function} -- ${gists.useDatabase} ALTER TABLE `App` ADD COLUMN `DeletedAt` BIGINT(20) NOT NULL DEFAULT '0' COMMENT 'Delete timestamp based on milliseconds' AFTER `IsDeleted`; ALTER TABLE `AppNamespace` ADD COLUMN `DeletedAt` BIGINT(20) NOT NULL DEFAULT '0' COMMENT 'Delete timestamp based on milliseconds' AFTER `IsDeleted`; ALTER TABLE `Audit` ADD COLUMN `DeletedAt` BIGINT(20) NOT NULL DEFAULT '0' COMMENT 'Delete timestamp based on milliseconds' AFTER `IsDeleted`; ALTER TABLE `Cluster` ADD COLUMN `DeletedAt` BIGINT(20) NOT NULL DEFAULT '0' COMMENT 'Delete timestamp based on milliseconds' AFTER `IsDeleted`; ALTER TABLE `Commit` ADD COLUMN `DeletedAt` BIGINT(20) NOT NULL DEFAULT '0' COMMENT 'Delete timestamp based on milliseconds' AFTER `IsDeleted`; ALTER TABLE `GrayReleaseRule` ADD COLUMN `DeletedAt` BIGINT(20) NOT NULL DEFAULT '0' COMMENT 'Delete timestamp based on milliseconds' AFTER `IsDeleted`; ALTER TABLE `Item` ADD COLUMN `DeletedAt` BIGINT(20) NOT NULL DEFAULT '0' COMMENT 'Delete timestamp based on milliseconds' AFTER `IsDeleted`, ADD INDEX IX_key (`Key`); ALTER TABLE `Namespace` ADD COLUMN `DeletedAt` BIGINT(20) NOT NULL DEFAULT '0' COMMENT 'Delete timestamp based on milliseconds' AFTER `IsDeleted`; ALTER TABLE `NamespaceLock` ADD COLUMN `DeletedAt` BIGINT(20) NOT NULL DEFAULT '0' COMMENT 'Delete timestamp based on milliseconds' AFTER `IsDeleted`; ALTER TABLE `Release` ADD COLUMN `DeletedAt` BIGINT(20) NOT NULL DEFAULT '0' COMMENT 'Delete timestamp based on milliseconds' AFTER `IsDeleted`; ALTER TABLE `ReleaseHistory` ADD COLUMN `DeletedAt` BIGINT(20) NOT NULL DEFAULT '0' COMMENT 'Delete timestamp based on milliseconds' AFTER `IsDeleted`; ALTER TABLE `ServerConfig` ADD COLUMN `DeletedAt` BIGINT(20) NOT NULL DEFAULT '0' COMMENT 'Delete timestamp based on milliseconds' AFTER `IsDeleted`; ALTER TABLE `AccessKey` ADD COLUMN `DeletedAt` BIGINT(20) NOT NULL DEFAULT '0' COMMENT 'Delete timestamp based on milliseconds' AFTER `IsDeleted`; -- ${gists.autoGeneratedDeclaration} ================================================ FILE: apollo-build-sql-converter/src/test/resources/META-INF/sql/h2-test/delta/v190-v200/apolloportaldb-v190-v200-after.sql ================================================ -- -- Copyright 2024 Apollo Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. -- You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. -- -- delta schema to upgrade apollo portal db from v1.9.0 to v2.0.0 -- ${gists.autoGeneratedDeclaration} -- ${gists.h2Function} -- ${gists.useDatabase} -- the follow DML won't change the `DataChange_LastTime` field UPDATE `App` SET `DeletedAt` = -Id, `DataChange_LastTime` = `DataChange_LastTime` WHERE `IsDeleted` = 1 and `DeletedAt` = 0; UPDATE `AppNamespace` SET `DeletedAt` = -Id, `DataChange_LastTime` = `DataChange_LastTime` WHERE `IsDeleted` = 1 and `DeletedAt` = 0; UPDATE `Consumer` SET `DeletedAt` = -Id, `DataChange_LastTime` = `DataChange_LastTime` WHERE `IsDeleted` = 1 and `DeletedAt` = 0; UPDATE `ConsumerRole` SET `DeletedAt` = -Id, `DataChange_LastTime` = `DataChange_LastTime` WHERE `IsDeleted` = 1 and `DeletedAt` = 0; UPDATE `ConsumerToken` SET `DeletedAt` = -Id, `DataChange_LastTime` = `DataChange_LastTime` WHERE `IsDeleted` = 1 and `DeletedAt` = 0; UPDATE `Favorite` SET `DeletedAt` = -Id, `DataChange_LastTime` = `DataChange_LastTime` WHERE `IsDeleted` = 1 and `DeletedAt` = 0; UPDATE `Permission` SET `DeletedAt` = -Id, `DataChange_LastTime` = `DataChange_LastTime` WHERE `IsDeleted` = 1 and `DeletedAt` = 0; UPDATE `Role` SET `DeletedAt` = -Id, `DataChange_LastTime` = `DataChange_LastTime` WHERE `IsDeleted` = 1 and `DeletedAt` = 0; UPDATE `RolePermission` SET `DeletedAt` = -Id, `DataChange_LastTime` = `DataChange_LastTime` WHERE `IsDeleted` = 1 and `DeletedAt` = 0; UPDATE `ServerConfig` SET `DeletedAt` = -Id, `DataChange_LastTime` = `DataChange_LastTime` WHERE `IsDeleted` = 1 and `DeletedAt` = 0; UPDATE `UserRole` SET `DeletedAt` = -Id, `DataChange_LastTime` = `DataChange_LastTime` WHERE `IsDeleted` = 1 and `DeletedAt` = 0; -- add UNIQUE CONSTRAINT INDEX for each table ALTER TABLE `App` ADD UNIQUE INDEX `UK_AppId_DeletedAt` (`AppId`,`DeletedAt`), DROP INDEX `AppId`; ALTER TABLE `AppNamespace` ADD UNIQUE INDEX `UK_AppId_Name_DeletedAt` (`AppId`,`Name`,`DeletedAt`), DROP INDEX `IX_AppId`; ALTER TABLE `Consumer` ADD UNIQUE INDEX `UK_AppId_DeletedAt` (`AppId`,`DeletedAt`), DROP INDEX `AppId`; ALTER TABLE `ConsumerRole` ADD UNIQUE INDEX `UK_ConsumerId_RoleId_DeletedAt` (`ConsumerId`,`RoleId`,`DeletedAt`), DROP INDEX `IX_ConsumerId_RoleId`; ALTER TABLE `ConsumerToken` ADD UNIQUE INDEX `UK_Token_DeletedAt` (`Token`,`DeletedAt`), DROP INDEX `IX_Token`; ALTER TABLE `Favorite` ADD UNIQUE INDEX `UK_UserId_AppId_DeletedAt` (`UserId`,`AppId`,`DeletedAt`), DROP INDEX `IX_UserId`; ALTER TABLE `Permission` ADD UNIQUE INDEX `UK_TargetId_PermissionType_DeletedAt` (`TargetId`,`PermissionType`,`DeletedAt`), DROP INDEX `IX_TargetId_PermissionType`; ALTER TABLE `Role` ADD UNIQUE INDEX `UK_RoleName_DeletedAt` (`RoleName`,`DeletedAt`), DROP INDEX `IX_RoleName`; ALTER TABLE `RolePermission` ADD UNIQUE INDEX `UK_RoleId_PermissionId_DeletedAt` (`RoleId`,`PermissionId`,`DeletedAt`), DROP INDEX `IX_RoleId`; ALTER TABLE `ServerConfig` ADD UNIQUE INDEX `UK_Key_DeletedAt` (`Key`,`DeletedAt`), DROP INDEX `IX_Key`; ALTER TABLE `UserRole` ADD UNIQUE INDEX `UK_UserId_RoleId_DeletedAt` (`UserId`,`RoleId`,`DeletedAt`), DROP INDEX `IX_UserId_RoleId`; ALTER TABLE `Users` ADD UNIQUE INDEX `UK_Username` (`Username`); -- ${gists.autoGeneratedDeclaration} ================================================ FILE: apollo-build-sql-converter/src/test/resources/META-INF/sql/h2-test/delta/v190-v200/apolloportaldb-v190-v200.sql ================================================ -- -- Copyright 2024 Apollo Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. -- You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. -- -- delta schema to upgrade apollo portal db from v1.9.0 to v2.0.0 -- ${gists.autoGeneratedDeclaration} -- ${gists.h2Function} -- ${gists.useDatabase} ALTER TABLE `App` ADD COLUMN `DeletedAt` BIGINT(20) NOT NULL DEFAULT '0' COMMENT 'Delete timestamp based on milliseconds' AFTER `IsDeleted`; ALTER TABLE `AppNamespace` ADD COLUMN `DeletedAt` BIGINT(20) NOT NULL DEFAULT '0' COMMENT 'Delete timestamp based on milliseconds' AFTER `IsDeleted`; ALTER TABLE `Consumer` ADD COLUMN `DeletedAt` BIGINT(20) NOT NULL DEFAULT '0' COMMENT 'Delete timestamp based on milliseconds' AFTER `IsDeleted`; ALTER TABLE `ConsumerRole` ADD COLUMN `DeletedAt` BIGINT(20) NOT NULL DEFAULT '0' COMMENT 'Delete timestamp based on milliseconds' AFTER `IsDeleted`; ALTER TABLE `ConsumerToken` ADD COLUMN `DeletedAt` BIGINT(20) NOT NULL DEFAULT '0' COMMENT 'Delete timestamp based on milliseconds' AFTER `IsDeleted`; ALTER TABLE `Favorite` ADD COLUMN `DeletedAt` BIGINT(20) NOT NULL DEFAULT '0' COMMENT 'Delete timestamp based on milliseconds' AFTER `IsDeleted`; ALTER TABLE `Permission` ADD COLUMN `DeletedAt` BIGINT(20) NOT NULL DEFAULT '0' COMMENT 'Delete timestamp based on milliseconds' AFTER `IsDeleted`; ALTER TABLE `Role` ADD COLUMN `DeletedAt` BIGINT(20) NOT NULL DEFAULT '0' COMMENT 'Delete timestamp based on milliseconds' AFTER `IsDeleted`; ALTER TABLE `RolePermission` ADD COLUMN `DeletedAt` BIGINT(20) NOT NULL DEFAULT '0' COMMENT 'Delete timestamp based on milliseconds' AFTER `IsDeleted`; ALTER TABLE `ServerConfig` ADD COLUMN `DeletedAt` BIGINT(20) NOT NULL DEFAULT '0' COMMENT 'Delete timestamp based on milliseconds' AFTER `IsDeleted`; ALTER TABLE `UserRole` ADD COLUMN `DeletedAt` BIGINT(20) NOT NULL DEFAULT '0' COMMENT 'Delete timestamp based on milliseconds' AFTER `IsDeleted`; -- ${gists.autoGeneratedDeclaration} ================================================ FILE: apollo-build-sql-converter/src/test/resources/META-INF/sql/h2-test/delta/v200-v210/apolloconfigdb-v200-v210.sql ================================================ -- -- Copyright 2024 Apollo Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. -- You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. -- -- delta schema to upgrade apollo config db from v2.0.0 to v2.1.0 -- ${gists.autoGeneratedDeclaration} -- ${gists.h2Function} -- ${gists.useDatabase} -- add INDEX for ReleaseHistory table CREATE INDEX IX_PreviousReleaseId ON ReleaseHistory(PreviousReleaseId); ALTER TABLE `Item` ADD COLUMN `Type` tinyint(3) unsigned NOT NULL DEFAULT '0' COMMENT '配置项类型,0: String,1: Number,2: Boolean,3: JSON' AFTER `Key`; CREATE TABLE `ServiceRegistry` ( `Id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增Id', `ServiceName` VARCHAR(64) NOT NULL COMMENT '服务名', `Uri` VARCHAR(64) NOT NULL COMMENT '服务地址', `Cluster` VARCHAR(64) NOT NULL COMMENT '集群,可以用来标识apollo.cluster或者网络分区', `Metadata` VARCHAR(1024) NOT NULL DEFAULT '{}' COMMENT '元数据,key value结构的json object,为了方面后面扩展功能而不需要修改表结构', `DataChange_CreatedTime` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `DataChange_LastTime` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改时间', PRIMARY KEY (`Id`), UNIQUE INDEX `IX_UNIQUE_KEY` (`ServiceName`, `Uri`), INDEX `IX_DataChange_LastTime` (`DataChange_LastTime`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='注册中心'; -- ${gists.autoGeneratedDeclaration} ================================================ FILE: apollo-build-sql-converter/src/test/resources/META-INF/sql/h2-test/delta/v210-v220/apolloconfigdb-v210-v220.sql ================================================ -- -- Copyright 2024 Apollo Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. -- You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. -- -- delta schema to upgrade apollo config db from v2.1.0 to v2.2.0 -- ${gists.autoGeneratedDeclaration} -- ${gists.h2Function} -- ${gists.useDatabase} ALTER TABLE `App` MODIFY COLUMN `AppId` VARCHAR(64) NOT NULL DEFAULT 'default' COMMENT 'AppID'; ALTER TABLE `Commit` MODIFY COLUMN `AppId` VARCHAR(64) NOT NULL DEFAULT 'default' COMMENT 'AppID'; ALTER TABLE `Namespace` MODIFY COLUMN `AppId` VARCHAR(64) NOT NULL DEFAULT 'default' COMMENT 'AppID'; ALTER TABLE `Release` MODIFY COLUMN `AppId` VARCHAR(64) NOT NULL DEFAULT 'default' COMMENT 'AppID'; ALTER TABLE `AccessKey` MODIFY COLUMN `AppId` VARCHAR(64) NOT NULL DEFAULT 'default' COMMENT 'AppID'; ALTER TABLE `Commit` DROP INDEX `AppId`, ADD INDEX `AppId` (`AppId`); ALTER TABLE `Namespace` DROP INDEX `UK_AppId_ClusterName_NamespaceName_DeletedAt`, ADD UNIQUE INDEX `UK_AppId_ClusterName_NamespaceName_DeletedAt` (`AppId`,`ClusterName`(191),`NamespaceName`(191),`DeletedAt`); ALTER TABLE `Release` DROP INDEX `AppId_ClusterName_GroupName`, ADD INDEX `AppId_ClusterName_GroupName` (`AppId`,`ClusterName`(191),`NamespaceName`(191),`DeletedAt`); DROP TABLE IF EXISTS `AuditLog`; CREATE TABLE `AuditLog` ( `Id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键', `TraceId` varchar(32) NOT NULL DEFAULT '' COMMENT '链路全局唯一ID', `SpanId` varchar(32) NOT NULL DEFAULT '' COMMENT '跨度ID', `ParentSpanId` varchar(32) DEFAULT NULL COMMENT '父跨度ID', `FollowsFromSpanId` varchar(32) DEFAULT NULL COMMENT '上一个兄弟跨度ID', `Operator` varchar(64) NOT NULL DEFAULT 'anonymous' COMMENT '操作人', `OpType` varchar(50) NOT NULL DEFAULT 'default' COMMENT '操作类型', `OpName` varchar(150) NOT NULL DEFAULT 'default' COMMENT '操作名称', `Description` varchar(200) DEFAULT NULL COMMENT '备注', `IsDeleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '1: deleted, 0: normal', `DeletedAt` BIGINT(20) NOT NULL DEFAULT '0' COMMENT 'Delete timestamp based on milliseconds', `DataChange_CreatedBy` varchar(64) DEFAULT NULL COMMENT '创建人邮箱前缀', `DataChange_CreatedTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `DataChange_LastModifiedBy` varchar(64) DEFAULT '' COMMENT '最后修改人邮箱前缀', `DataChange_LastTime` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改时间', PRIMARY KEY (`Id`), KEY `IX_TraceId` (`TraceId`), KEY `IX_OpName` (`OpName`), KEY `IX_DataChange_CreatedTime` (`DataChange_CreatedTime`), KEY `IX_Operator` (`Operator`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='审计日志表'; DROP TABLE IF EXISTS `AuditLogDataInfluence`; CREATE TABLE `AuditLogDataInfluence` ( `Id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键', `SpanId` char(32) NOT NULL DEFAULT '' COMMENT '跨度ID', `InfluenceEntityId` varchar(50) NOT NULL DEFAULT '0' COMMENT '记录ID', `InfluenceEntityName` varchar(50) NOT NULL DEFAULT 'default' COMMENT '表名', `FieldName` varchar(50) DEFAULT NULL COMMENT '字段名称', `FieldOldValue` varchar(500) DEFAULT NULL COMMENT '字段旧值', `FieldNewValue` varchar(500) DEFAULT NULL COMMENT '字段新值', `IsDeleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '1: deleted, 0: normal', `DeletedAt` BIGINT(20) NOT NULL DEFAULT '0' COMMENT 'Delete timestamp based on milliseconds', `DataChange_CreatedBy` varchar(64) DEFAULT NULL COMMENT '创建人邮箱前缀', `DataChange_CreatedTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `DataChange_LastModifiedBy` varchar(64) DEFAULT '' COMMENT '最后修改人邮箱前缀', `DataChange_LastTime` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改时间', PRIMARY KEY (`Id`), KEY `IX_SpanId` (`SpanId`), KEY `IX_DataChange_CreatedTime` (`DataChange_CreatedTime`), KEY `IX_EntityId` (`InfluenceEntityId`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='审计日志数据变动表'; -- ${gists.autoGeneratedDeclaration} ================================================ FILE: apollo-build-sql-converter/src/test/resources/META-INF/sql/h2-test/delta/v210-v220/apolloportaldb-v210-v220.sql ================================================ -- -- Copyright 2024 Apollo Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. -- You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. -- -- delta schema to upgrade apollo portal db from v2.1.0 to v2.2.0 -- ${gists.autoGeneratedDeclaration} -- ${gists.h2Function} -- ${gists.useDatabase} ALTER TABLE `App` MODIFY COLUMN `AppId` VARCHAR(64) NOT NULL DEFAULT 'default' COMMENT 'AppID'; ALTER TABLE `Consumer` MODIFY COLUMN `AppId` VARCHAR(64) NOT NULL DEFAULT 'default' COMMENT 'AppID'; ALTER TABLE `Favorite` MODIFY COLUMN `AppId` VARCHAR(64) NOT NULL DEFAULT 'default' COMMENT 'AppID'; ALTER TABLE `Favorite` DROP INDEX `AppId`, ADD INDEX `AppId` (`AppId`); DROP TABLE IF EXISTS `AuditLog`; CREATE TABLE `AuditLog` ( `Id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键', `TraceId` varchar(32) NOT NULL DEFAULT '' COMMENT '链路全局唯一ID', `SpanId` varchar(32) NOT NULL DEFAULT '' COMMENT '跨度ID', `ParentSpanId` varchar(32) DEFAULT NULL COMMENT '父跨度ID', `FollowsFromSpanId` varchar(32) DEFAULT NULL COMMENT '上一个兄弟跨度ID', `Operator` varchar(64) NOT NULL DEFAULT 'anonymous' COMMENT '操作人', `OpType` varchar(50) NOT NULL DEFAULT 'default' COMMENT '操作类型', `OpName` varchar(150) NOT NULL DEFAULT 'default' COMMENT '操作名称', `Description` varchar(200) DEFAULT NULL COMMENT '备注', `IsDeleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '1: deleted, 0: normal', `DeletedAt` BIGINT(20) NOT NULL DEFAULT '0' COMMENT 'Delete timestamp based on milliseconds', `DataChange_CreatedBy` varchar(64) DEFAULT NULL COMMENT '创建人邮箱前缀', `DataChange_CreatedTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `DataChange_LastModifiedBy` varchar(64) DEFAULT '' COMMENT '最后修改人邮箱前缀', `DataChange_LastTime` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改时间', PRIMARY KEY (`Id`), KEY `IX_TraceId` (`TraceId`), KEY `IX_OpName` (`OpName`), KEY `IX_DataChange_CreatedTime` (`DataChange_CreatedTime`), KEY `IX_Operator` (`Operator`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='审计日志表'; DROP TABLE IF EXISTS `AuditLogDataInfluence`; CREATE TABLE `AuditLogDataInfluence` ( `Id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键', `SpanId` char(32) NOT NULL DEFAULT '' COMMENT '跨度ID', `InfluenceEntityId` varchar(50) NOT NULL DEFAULT '0' COMMENT '记录ID', `InfluenceEntityName` varchar(50) NOT NULL DEFAULT 'default' COMMENT '表名', `FieldName` varchar(50) DEFAULT NULL COMMENT '字段名称', `FieldOldValue` varchar(500) DEFAULT NULL COMMENT '字段旧值', `FieldNewValue` varchar(500) DEFAULT NULL COMMENT '字段新值', `IsDeleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '1: deleted, 0: normal', `DeletedAt` BIGINT(20) NOT NULL DEFAULT '0' COMMENT 'Delete timestamp based on milliseconds', `DataChange_CreatedBy` varchar(64) DEFAULT NULL COMMENT '创建人邮箱前缀', `DataChange_CreatedTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `DataChange_LastModifiedBy` varchar(64) DEFAULT '' COMMENT '最后修改人邮箱前缀', `DataChange_LastTime` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改时间', PRIMARY KEY (`Id`), KEY `IX_SpanId` (`SpanId`), KEY `IX_DataChange_CreatedTime` (`DataChange_CreatedTime`), KEY `IX_EntityId` (`InfluenceEntityId`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='审计日志数据变动表'; -- ${gists.autoGeneratedDeclaration} ================================================ FILE: apollo-buildtools/.gitignore ================================================ /target/ ================================================ FILE: apollo-buildtools/pom.xml ================================================ com.ctrip.framework.apollo apollo ${revision} ../pom.xml 4.0.0 apollo-buildtools Apollo BuildTools maven-resources-plugin 2.6 copy-resources validate copy-resources ${basedir}/target src/main/scripts ================================================ FILE: apollo-buildtools/src/main/resources/LICENSE-2.0.txt ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: apollo-buildtools/src/main/resources/google_checks.xml ================================================ ================================================ FILE: apollo-buildtools/src/main/scripts/deploy_jenkins.sh ================================================ #!/bin/bash # # Copyright 2024 Apollo Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # set -e set -u cd `dirname $0` if [ $# -lt 1 ];then echo "usage: `basename $0` 100003171|100003172|100003173" exit 1 fi valid_app=false for supported_app in 100003171 100003172 100003173 ;do if [ $1 == $supported_app ];then valid_app=true break; fi done if [ $valid_app == false ];then echo "$1 is not a supported app id" exit 1 else echo "Upgrading $1" fi APP_BASE_DIR=/opt/ctrip/app APP_NAME=$1 APP_DIR=$APP_BASE_DIR/$APP_NAME APP_RELEASE_DIR=$APP_BASE_DIR/apollo-$APP_NAME.releases/`date "+%Y-%m-%d.%H.%M.%S"` APP_STARTUP_SCRIPT=$APP_DIR/scripts/startup.sh APP_SHUTDOWN_SCRIPT=$APP_DIR/scripts/shutdown.sh chmod +x $APP_SHUTDOWN_SCRIPT if [ -e $APP_STARTUP_SCRIPT ];then $APP_SHUTDOWN_SCRIPT echo "Sleeping 5s to wait shutting down" sleep 5s fi mkdir -p $APP_RELEASE_DIR unzip *.zip -d $APP_RELEASE_DIR if [ -d $APP_DIR ];then rm -rf $APP_DIR fi ln -s $APP_RELEASE_DIR $APP_DIR chmod +x $APP_STARTUP_SCRIPT BUILD_ID=dontKillMe $APP_STARTUP_SCRIPT ================================================ FILE: apollo-buildtools/style/eclipse-java-google-style.xml ================================================ ================================================ FILE: apollo-buildtools/style/intellij-java-google-style.xml ================================================

xmlns:android ^$
xmlns:.* ^$ BY_NAME
.*:id http://schemas.android.com/apk/res/android
style ^$
.* ^$ BY_NAME
.*:.*Style http://schemas.android.com/apk/res/android BY_NAME
.*:layout_width http://schemas.android.com/apk/res/android
.*:layout_height http://schemas.android.com/apk/res/android
.*:layout_weight http://schemas.android.com/apk/res/android
.*:layout_margin http://schemas.android.com/apk/res/android
.*:layout_marginTop http://schemas.android.com/apk/res/android
.*:layout_marginBottom http://schemas.android.com/apk/res/android
.*:layout_marginStart http://schemas.android.com/apk/res/android
.*:layout_marginEnd http://schemas.android.com/apk/res/android
.*:layout_marginLeft http://schemas.android.com/apk/res/android
.*:layout_marginRight http://schemas.android.com/apk/res/android
.*:layout_.* http://schemas.android.com/apk/res/android BY_NAME
.*:padding http://schemas.android.com/apk/res/android
.*:paddingTop http://schemas.android.com/apk/res/android
.*:paddingBottom http://schemas.android.com/apk/res/android
.*:paddingStart http://schemas.android.com/apk/res/android
.*:paddingEnd http://schemas.android.com/apk/res/android
.*:paddingLeft http://schemas.android.com/apk/res/android
.*:paddingRight http://schemas.android.com/apk/res/android
.* http://schemas.android.com/apk/res/android BY_NAME
.* http://schemas.android.com/apk/res-auto BY_NAME
.* http://schemas.android.com/tools BY_NAME
.* .* BY_NAME
================================================ FILE: apollo-buildtools/style/license/apollo-license ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ ================================================ FILE: apollo-common/pom.xml ================================================ com.ctrip.framework.apollo apollo ${revision} ../pom.xml 4.0.0 apollo-common Apollo Common ${project.artifactId} com.ctrip.framework.apollo apollo-core com.ctrip.framework.apollo apollo-audit-api org.springframework.boot spring-boot-starter-actuator org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-validation org.springframework.boot spring-boot-starter-security org.springframework.boot spring-boot-starter-data-jpa com.mysql mysql-connector-j org.postgresql postgresql com.h2database h2 org.springframework.data spring-data-commons org.apache.httpcomponents httpclient commons-logging commons-logging org.codehaus.janino janino org.apache.commons commons-lang3 io.micrometer micrometer-core io.micrometer micrometer-registry-prometheus ================================================ FILE: apollo-common/src/main/java/com/ctrip/framework/apollo/common/ApolloCommonConfig.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.common; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpStatus; import org.springframework.security.web.firewall.HttpStatusRequestRejectedHandler; import org.springframework.security.web.firewall.RequestRejectedHandler; @EnableAutoConfiguration @Configuration @ComponentScan(basePackageClasses = ApolloCommonConfig.class) public class ApolloCommonConfig { /** * Spring-Security Firewall Deny Request Response 400 * @return RequestRejectedHandler */ @Bean public RequestRejectedHandler requestRejectedHandler() { return new HttpStatusRequestRejectedHandler(HttpStatus.BAD_REQUEST.value()); } } ================================================ FILE: apollo-common/src/main/java/com/ctrip/framework/apollo/common/aop/RepositoryAspect.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.common.aop; import com.ctrip.framework.apollo.tracer.Tracer; import com.ctrip.framework.apollo.tracer.spi.Transaction; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.springframework.stereotype.Component; @Aspect @Component public class RepositoryAspect { @Pointcut("execution(public * org.springframework.data.repository.Repository+.*(..))") public void anyRepositoryMethod() {} @Around("anyRepositoryMethod()") public Object invokeWithCatTransaction(ProceedingJoinPoint joinPoint) throws Throwable { String name = joinPoint.getSignature().getDeclaringType().getSimpleName() + "." + joinPoint.getSignature().getName(); Transaction catTransaction = Tracer.newTransaction("SQL", name); try { Object result = joinPoint.proceed(); catTransaction.setStatus(Transaction.SUCCESS); return result; } catch (Throwable ex) { catTransaction.setStatus(ex); throw ex; } finally { catTransaction.complete(); } } } ================================================ FILE: apollo-common/src/main/java/com/ctrip/framework/apollo/common/condition/ConditionalOnMissingProfile.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.common.condition; import org.springframework.context.annotation.Conditional; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * {@link Conditional} that only matches when the specified profiles are inactive. * * @author Jason Song(song_s@ctrip.com) */ @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented @Conditional(OnProfileCondition.class) public @interface ConditionalOnMissingProfile { /** * The profiles that should be inactive * @return */ String[] value() default {}; } ================================================ FILE: apollo-common/src/main/java/com/ctrip/framework/apollo/common/condition/ConditionalOnProfile.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.common.condition; import org.springframework.context.annotation.Conditional; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * {@link Conditional} that only matches when the specified profiles are active. * * @author Jason Song(song_s@ctrip.com) */ @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented @Conditional(OnProfileCondition.class) public @interface ConditionalOnProfile { /** * The profiles that should be active * @return */ String[] value() default {}; } ================================================ FILE: apollo-common/src/main/java/com/ctrip/framework/apollo/common/condition/OnProfileCondition.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.common.condition; import com.google.common.collect.Sets; import org.springframework.context.annotation.Condition; import org.springframework.context.annotation.ConditionContext; import org.springframework.core.type.AnnotatedTypeMetadata; import org.springframework.util.MultiValueMap; import java.util.Collections; import java.util.List; import java.util.Set; /** * @author Jason Song(song_s@ctrip.com) */ public class OnProfileCondition implements Condition { @Override public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { Set activeProfiles = Sets.newHashSet(context.getEnvironment().getActiveProfiles()); Set requiredActiveProfiles = retrieveAnnotatedProfiles(metadata, ConditionalOnProfile.class.getName()); Set requiredInactiveProfiles = retrieveAnnotatedProfiles(metadata, ConditionalOnMissingProfile.class.getName()); return Sets.difference(requiredActiveProfiles, activeProfiles).isEmpty() && Sets.intersection(requiredInactiveProfiles, activeProfiles).isEmpty(); } private Set retrieveAnnotatedProfiles(AnnotatedTypeMetadata metadata, String annotationType) { if (!metadata.isAnnotated(annotationType)) { return Collections.emptySet(); } MultiValueMap attributes = metadata.getAllAnnotationAttributes(annotationType); if (attributes == null) { return Collections.emptySet(); } Set profiles = Sets.newHashSet(); List values = attributes.get("value"); if (values != null) { for (Object value : values) { if (value instanceof String[]) { Collections.addAll(profiles, (String[]) value); } else { profiles.add((String) value); } } } return profiles; } } ================================================ FILE: apollo-common/src/main/java/com/ctrip/framework/apollo/common/config/RefreshableConfig.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.common.config; import com.google.common.base.Splitter; import com.google.common.base.Strings; import com.ctrip.framework.apollo.core.utils.ApolloThreadFactory; import com.ctrip.framework.apollo.tracer.Tracer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.util.CollectionUtils; import java.util.List; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import jakarta.annotation.PostConstruct; public abstract class RefreshableConfig implements DisposableBean { private static final Logger logger = LoggerFactory.getLogger(RefreshableConfig.class); private static final String LIST_SEPARATOR = ","; // TimeUnit: second private static final int CONFIG_REFRESH_INTERVAL = 60; protected Splitter splitter = Splitter.on(LIST_SEPARATOR).omitEmptyStrings().trimResults(); @Autowired private ConfigurableEnvironment environment; private List propertySources; private ScheduledExecutorService executorService; /** * register refreshable property source. * Notice: The front property source has higher priority. */ protected abstract List getRefreshablePropertySources(); @PostConstruct public void setup() { propertySources = getRefreshablePropertySources(); if (CollectionUtils.isEmpty(propertySources)) { throw new IllegalStateException("Property sources can not be empty."); } // add property source to environment for (RefreshablePropertySource propertySource : propertySources) { propertySource.refresh(); environment.getPropertySources().addLast(propertySource); } // task to update configs executorService = Executors.newScheduledThreadPool(1, ApolloThreadFactory.create("ConfigRefresher", true)); executorService.scheduleWithFixedDelay(() -> { try { propertySources.forEach(RefreshablePropertySource::refresh); } catch (Throwable t) { logger.error("Refresh configs failed.", t); Tracer.logError("Refresh configs failed.", t); } }, CONFIG_REFRESH_INTERVAL, CONFIG_REFRESH_INTERVAL, TimeUnit.SECONDS); } @Override public void destroy() { if (executorService != null) { executorService.shutdownNow(); } } public int getIntProperty(String key, int defaultValue) { try { String value = getValue(key); return value == null ? defaultValue : Integer.parseInt(value); } catch (Throwable e) { Tracer.logError("Get int property failed.", e); return defaultValue; } } public boolean getBooleanProperty(String key, boolean defaultValue) { try { String value = getValue(key); return value == null ? defaultValue : "true".equals(value); } catch (Throwable e) { Tracer.logError("Get boolean property failed.", e); return defaultValue; } } public String[] getArrayProperty(String key, String[] defaultValue) { try { String value = getValue(key); return Strings.isNullOrEmpty(value) ? defaultValue : value.split(LIST_SEPARATOR); } catch (Throwable e) { Tracer.logError("Get array property failed.", e); return defaultValue; } } public String getValue(String key, String defaultValue) { try { return environment.getProperty(key, defaultValue); } catch (Throwable e) { Tracer.logError("Get value failed.", e); return defaultValue; } } public String getValue(String key) { return environment.getProperty(key); } } ================================================ FILE: apollo-common/src/main/java/com/ctrip/framework/apollo/common/config/RefreshablePropertySource.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.common.config; import org.springframework.core.env.MapPropertySource; import java.util.Map; public abstract class RefreshablePropertySource extends MapPropertySource { public RefreshablePropertySource(String name, Map source) { super(name, source); } @Override public Object getProperty(String name) { return this.source.get(name); } /** * refresh property */ protected abstract void refresh(); } ================================================ FILE: apollo-common/src/main/java/com/ctrip/framework/apollo/common/constants/AccessKeyMode.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.common.constants; public interface AccessKeyMode { int FILTER = 0; int OBSERVER = 1; } ================================================ FILE: apollo-common/src/main/java/com/ctrip/framework/apollo/common/constants/ApolloServer.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.common.constants; /** * @author Jason Song(song_s@ctrip.com) */ public class ApolloServer { public final static String VERSION = "java-" + ApolloServer.class.getPackage().getImplementationVersion(); } ================================================ FILE: apollo-common/src/main/java/com/ctrip/framework/apollo/common/constants/GsonType.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.common.constants; import com.google.gson.reflect.TypeToken; import com.ctrip.framework.apollo.common.dto.GrayReleaseRuleItemDTO; import com.ctrip.framework.apollo.common.dto.ItemDTO; import java.lang.reflect.Type; import java.util.List; import java.util.Map; public interface GsonType { Type CONFIG = new TypeToken>() {}.getType(); Type RULE_ITEMS = new TypeToken>() {}.getType(); Type ITEM_DTOS = new TypeToken>() {}.getType(); } ================================================ FILE: apollo-common/src/main/java/com/ctrip/framework/apollo/common/constants/NamespaceBranchStatus.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.common.constants; public interface NamespaceBranchStatus { int DELETED = 0; int ACTIVE = 1; int MERGED = 2; } ================================================ FILE: apollo-common/src/main/java/com/ctrip/framework/apollo/common/constants/ReleaseOperation.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.common.constants; /** * @author Jason Song(song_s@ctrip.com) */ public interface ReleaseOperation { int NORMAL_RELEASE = 0; int ROLLBACK = 1; int GRAY_RELEASE = 2; int APPLY_GRAY_RULES = 3; int GRAY_RELEASE_MERGE_TO_MASTER = 4; int MASTER_NORMAL_RELEASE_MERGE_TO_GRAY = 5; int MATER_ROLLBACK_MERGE_TO_GRAY = 6; int ABANDON_GRAY_RELEASE = 7; int GRAY_RELEASE_DELETED_AFTER_MERGE = 8; } ================================================ FILE: apollo-common/src/main/java/com/ctrip/framework/apollo/common/constants/ReleaseOperationContext.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.common.constants; /** * @author Jason Song(song_s@ctrip.com) */ public interface ReleaseOperationContext { String SOURCE_BRANCH = "sourceBranch"; String RULES = "rules"; String OLD_RULES = "oldRules"; String BASE_RELEASE_ID = "baseReleaseId"; String IS_EMERGENCY_PUBLISH = "isEmergencyPublish"; String BRANCH_RELEASE_KEYS = "branchReleaseKeys"; } ================================================ FILE: apollo-common/src/main/java/com/ctrip/framework/apollo/common/controller/ApolloInfoController.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.common.controller; import com.ctrip.framework.apollo.common.constants.ApolloServer; import com.ctrip.framework.foundation.Foundation; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping(path = "/apollo") public class ApolloInfoController { @RequestMapping("net") public String getNet() { return Foundation.net().toString(); } @RequestMapping("server") public String getServer() { return Foundation.server().toString(); } @RequestMapping("version") public String getVersion() { return ApolloServer.VERSION; } } ================================================ FILE: apollo-common/src/main/java/com/ctrip/framework/apollo/common/controller/CharacterEncodingFilterConfiguration.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.common.controller; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.filter.CharacterEncodingFilter; import jakarta.servlet.DispatcherType; @Configuration public class CharacterEncodingFilterConfiguration { @Bean public FilterRegistrationBean encodingFilter() { FilterRegistrationBean bean = new FilterRegistrationBean(); bean.setFilter(new CharacterEncodingFilter()); bean.addInitParameter("encoding", "UTF-8"); // FIXME: https://github.com/Netflix/eureka/issues/702 // bean.addInitParameter("forceEncoding", "true"); bean.setName("encodingFilter"); bean.addUrlPatterns("/*"); bean.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.FORWARD); return bean; } } ================================================ FILE: apollo-common/src/main/java/com/ctrip/framework/apollo/common/controller/GlobalDefaultExceptionHandler.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.common.controller; import com.ctrip.framework.apollo.common.exception.AbstractApolloHttpException; import com.ctrip.framework.apollo.common.exception.BadRequestException; import com.ctrip.framework.apollo.tracer.Tracer; import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; import java.lang.reflect.Type; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.HashMap; import java.util.Map; import java.util.Optional; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.ConstraintViolationException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.slf4j.event.Level; import org.springframework.core.NestedExceptionUtils; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatusCode; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.security.access.AccessDeniedException; import org.springframework.validation.ObjectError; import org.springframework.web.HttpMediaTypeException; import org.springframework.web.HttpRequestMethodNotSupportedException; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.client.HttpStatusCodeException; import static org.slf4j.event.Level.ERROR; import static org.slf4j.event.Level.WARN; import static org.springframework.http.HttpStatus.BAD_REQUEST; import static org.springframework.http.HttpStatus.FORBIDDEN; import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR; @ControllerAdvice public class GlobalDefaultExceptionHandler { private Gson gson = new Gson(); private static Type mapType = new TypeToken>() {}.getType(); private static final Logger logger = LoggerFactory.getLogger(GlobalDefaultExceptionHandler.class); // 处理系统内置的Exception @ExceptionHandler(Throwable.class) public ResponseEntity> exception(HttpServletRequest request, Throwable ex) { return handleError(request, INTERNAL_SERVER_ERROR, ex); } @ExceptionHandler({HttpRequestMethodNotSupportedException.class, HttpMediaTypeException.class}) public ResponseEntity> badRequest(HttpServletRequest request, ServletException ex) { return handleError(request, BAD_REQUEST, ex, WARN); } @ExceptionHandler(HttpStatusCodeException.class) public ResponseEntity> restTemplateException(HttpServletRequest request, HttpStatusCodeException ex) { return handleError(request, ex.getStatusCode(), ex); } @ExceptionHandler(AccessDeniedException.class) public ResponseEntity> accessDeny(HttpServletRequest request, AccessDeniedException ex) { return handleError(request, FORBIDDEN, ex); } // 处理自定义Exception @ExceptionHandler({AbstractApolloHttpException.class}) public ResponseEntity> badRequest(HttpServletRequest request, AbstractApolloHttpException ex) { return handleError(request, ex.getHttpStatus(), ex); } @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity> handleMethodArgumentNotValidException( HttpServletRequest request, MethodArgumentNotValidException ex) { final Optional firstError = ex.getBindingResult().getAllErrors().stream().findFirst(); if (firstError.isPresent()) { final String firstErrorMessage = firstError.get().getDefaultMessage(); return handleError(request, BAD_REQUEST, new BadRequestException(firstErrorMessage)); } return handleError(request, BAD_REQUEST, ex); } @ExceptionHandler(ConstraintViolationException.class) public ResponseEntity> handleConstraintViolationException( HttpServletRequest request, ConstraintViolationException ex) { return handleError(request, BAD_REQUEST, new BadRequestException(ex.getMessage())); } private ResponseEntity> handleError(HttpServletRequest request, HttpStatusCode status, Throwable ex) { return handleError(request, status, ex, ERROR); } private ResponseEntity> handleError(HttpServletRequest request, HttpStatusCode status, Throwable ex, Level logLevel) { String message = getMessageWithRootCause(ex); printLog(message, ex, logLevel); Map errorAttributes = new HashMap<>(); boolean errorHandled = false; if (ex instanceof HttpStatusCodeException) { try { // try to extract the original error info if it is thrown from apollo programs, e.g. admin // service errorAttributes = gson.fromJson(((HttpStatusCodeException) ex).getResponseBodyAsString(), mapType); status = ((HttpStatusCodeException) ex).getStatusCode(); errorHandled = true; } catch (Throwable th) { // ignore } } if (!errorHandled) { errorAttributes.put("status", status.value()); errorAttributes.put("message", message); errorAttributes.put("timestamp", LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)); errorAttributes.put("exception", ex.getClass().getName()); } HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); return new ResponseEntity<>(errorAttributes, headers, status); } // 打印日志, 其中logLevel为日志级别: ERROR/WARN/DEBUG/INFO/TRACE private void printLog(String message, Throwable ex, Level logLevel) { switch (logLevel) { case ERROR: logger.error(message, ex); break; case WARN: logger.warn(message, ex); break; case DEBUG: logger.debug(message, ex); break; case INFO: logger.info(message, ex); break; case TRACE: logger.trace(message, ex); break; } Tracer.logError(ex); } private String getMessageWithRootCause(Throwable ex) { String message = ex.getMessage(); Throwable rootCause = NestedExceptionUtils.getMostSpecificCause(ex); if (rootCause != ex) { message += " [Cause: " + rootCause.getMessage() + "]"; } return message; } } ================================================ FILE: apollo-common/src/main/java/com/ctrip/framework/apollo/common/controller/HttpMessageConverterConfiguration.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.common.controller; import com.google.common.collect.Lists; import com.google.gson.GsonBuilder; import com.google.gson.JsonDeserializer; import com.google.gson.JsonNull; import com.google.gson.JsonPrimitive; import com.google.gson.JsonSerializer; import java.time.Instant; import org.springframework.boot.autoconfigure.http.HttpMessageConverters; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.converter.ByteArrayHttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.StringHttpMessageConverter; import org.springframework.http.converter.json.GsonHttpMessageConverter; import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter; import java.util.List; /** * Created by Jason on 5/11/16. */ @Configuration public class HttpMessageConverterConfiguration { @Bean public HttpMessageConverters messageConverters() { // Custom Gson TypeAdapter for Instant JsonSerializer instantJsonSerializer = (src, typeOfSrc, context) -> src == null ? JsonNull.INSTANCE : new JsonPrimitive(src.toString()); // Serialize // Instant // as // ISO-8601 // string JsonDeserializer instantJsonDeserializer = (json, typeOfT, context) -> { if (json == null || json.isJsonNull()) { return null; } return Instant.parse(json.getAsString()); // Deserialize from ISO-8601 string }; GsonHttpMessageConverter gsonHttpMessageConverter = new GsonHttpMessageConverter(); gsonHttpMessageConverter.setGson(new GsonBuilder().setDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ") .registerTypeAdapter(Instant.class, instantJsonSerializer) .registerTypeAdapter(Instant.class, instantJsonDeserializer).create()); final List> converters = Lists.newArrayList(new ByteArrayHttpMessageConverter(), new StringHttpMessageConverter(), new AllEncompassingFormHttpMessageConverter(), gsonHttpMessageConverter); return new HttpMessageConverters() { @Override public List> getConverters() { return converters; } }; } } ================================================ FILE: apollo-common/src/main/java/com/ctrip/framework/apollo/common/controller/WebMvcConfig.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.common.controller; import java.util.List; import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; import org.springframework.boot.web.server.MimeMappings; import org.springframework.boot.web.server.WebServerFactoryCustomizer; import org.springframework.context.annotation.Configuration; import org.springframework.data.domain.PageRequest; import org.springframework.data.web.PageableHandlerMethodArgumentResolver; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class WebMvcConfig implements WebMvcConfigurer, WebServerFactoryCustomizer { @Override public void addArgumentResolvers(List argumentResolvers) { PageableHandlerMethodArgumentResolver pageResolver = new PageableHandlerMethodArgumentResolver(); pageResolver.setFallbackPageable(PageRequest.of(0, 10)); argumentResolvers.add(pageResolver); } @Override public void configureContentNegotiation(ContentNegotiationConfigurer configurer) { configurer.favorPathExtension(false); } @Override public void customize(TomcatServletWebServerFactory factory) { MimeMappings mappings = new MimeMappings(MimeMappings.DEFAULT); mappings.add("html", "text/html;charset=utf-8"); factory.setMimeMappings(mappings); } @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { // 10 days addCacheControl(registry, "img", 864000); addCacheControl(registry, "vendor", 864000); addCacheControl(registry, "scripts", 864000); addCacheControl(registry, "styles", 864000); // 1 day addCacheControl(registry, "views", 86400); addCacheControl(registry, "i18n", 86400); } private void addCacheControl(ResourceHandlerRegistry registry, String folder, int cachePeriod) { registry.addResourceHandler(String.format("/%s/**", folder)) .addResourceLocations(String.format("classpath:/static/%s/", folder)) .setCachePeriod(cachePeriod); } } ================================================ FILE: apollo-common/src/main/java/com/ctrip/framework/apollo/common/datasource/ApolloDataSourceScriptDatabaseInitializer.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.common.datasource; import java.util.List; import javax.sql.DataSource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.jdbc.DataSourceBuilder; import org.springframework.boot.jdbc.init.DataSourceScriptDatabaseInitializer; import org.springframework.boot.sql.init.DatabaseInitializationMode; import org.springframework.boot.sql.init.DatabaseInitializationSettings; import org.springframework.jdbc.datasource.AbstractDriverBasedDataSource; import org.springframework.jdbc.datasource.SimpleDriverDataSource; public class ApolloDataSourceScriptDatabaseInitializer extends DataSourceScriptDatabaseInitializer { private static final Logger log = LoggerFactory.getLogger(ApolloDataSourceScriptDatabaseInitializer.class); public ApolloDataSourceScriptDatabaseInitializer(DataSource dataSource, DatabaseInitializationSettings settings) { super(dataSource, settings); if (this.isEnabled(settings)) { log.info("Apollo DataSource Initialize is enabled"); if (log.isDebugEnabled()) { String jdbcUrl = this.getJdbcUrl(dataSource); log.debug("Initialize jdbc url: {}", jdbcUrl); List schemaLocations = settings.getSchemaLocations(); if (!schemaLocations.isEmpty()) { for (String schemaLocation : schemaLocations) { log.debug("Initialize Schema Location: {}", schemaLocation); } } } } else { log.info("Apollo DataSource Initialize is disabled"); } } private String getJdbcUrl(DataSource dataSource) { if (dataSource instanceof AbstractDriverBasedDataSource) { AbstractDriverBasedDataSource driverBasedDataSource = (AbstractDriverBasedDataSource) dataSource; return driverBasedDataSource.getUrl(); } SimpleDriverDataSource simpleDriverDataSource = DataSourceBuilder.derivedFrom(dataSource).type(SimpleDriverDataSource.class).build(); return simpleDriverDataSource.getUrl(); } private boolean isEnabled(DatabaseInitializationSettings settings) { if (settings.getMode() == DatabaseInitializationMode.NEVER) { return false; } return settings.getMode() == DatabaseInitializationMode.ALWAYS || this.isEmbeddedDatabase(); } } ================================================ FILE: apollo-common/src/main/java/com/ctrip/framework/apollo/common/datasource/ApolloDataSourceScriptDatabaseInitializerFactory.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.common.datasource; import java.net.URL; import java.security.CodeSource; import java.sql.Connection; import java.sql.SQLException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import javax.sql.DataSource; import org.springframework.boot.jdbc.DataSourceBuilder; import org.springframework.boot.jdbc.DatabaseDriver; import org.springframework.boot.jdbc.init.PlatformPlaceholderDatabaseDriverResolver; import org.springframework.boot.sql.init.DatabaseInitializationSettings; import org.springframework.jdbc.datasource.SimpleDriverDataSource; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; public class ApolloDataSourceScriptDatabaseInitializerFactory { public static ApolloDataSourceScriptDatabaseInitializer create(DataSource dataSource, ApolloSqlInitializationProperties properties) { DataSource determinedDataSource = determineDataSource(dataSource, properties); DatabaseInitializationSettings settings = getSettings(dataSource, properties); return new ApolloDataSourceScriptDatabaseInitializer(determinedDataSource, settings); } private static DataSource determineDataSource(DataSource dataSource, ApolloSqlInitializationProperties properties) { String username = properties.getUsername(); String password = properties.getPassword(); if (StringUtils.hasText(username) && StringUtils.hasText(password)) { return DataSourceBuilder.derivedFrom(dataSource).username(username).password(password) .type(SimpleDriverDataSource.class).build(); } return dataSource; } private static DatabaseInitializationSettings getSettings(DataSource dataSource, ApolloSqlInitializationProperties properties) { PlatformPlaceholderDatabaseDriverResolver platformResolver = new PlatformPlaceholderDatabaseDriverResolver().withDriverPlatform(DatabaseDriver.MARIADB, "mysql"); List schemaLocations = resolveLocations(properties.getSchemaLocations(), platformResolver, dataSource, properties); List dataLocations = resolveLocations(properties.getDataLocations(), platformResolver, dataSource, properties); DatabaseInitializationSettings settings = new DatabaseInitializationSettings(); settings .setSchemaLocations(scriptLocations(schemaLocations, "schema", properties.getPlatform())); settings.setDataLocations(scriptLocations(dataLocations, "data", properties.getPlatform())); settings.setContinueOnError(properties.isContinueOnError()); settings.setSeparator(properties.getSeparator()); settings.setEncoding(properties.getEncoding()); settings.setMode(properties.getMode()); return settings; } private static List resolveLocations(Collection locations, PlatformPlaceholderDatabaseDriverResolver platformResolver, DataSource dataSource, ApolloSqlInitializationProperties properties) { if (CollectionUtils.isEmpty(locations)) { return null; } Collection convertedLocations = convertRepositoryLocations(locations, dataSource); if (CollectionUtils.isEmpty(convertedLocations)) { return null; } String platform = properties.getPlatform(); if (StringUtils.hasText(platform) && !"all".equals(platform)) { return platformResolver.resolveAll(platform, convertedLocations.toArray(new String[0])); } return platformResolver.resolveAll(dataSource, convertedLocations.toArray(new String[0])); } private static Collection convertRepositoryLocations(Collection locations, DataSource dataSource) { if (CollectionUtils.isEmpty(locations)) { return Collections.emptyList(); } String repositoryDir = findRepositoryDirectory(); String suffix = findSuffix(dataSource); List convertedLocations = new ArrayList<>(locations.size()); for (String location : locations) { String convertedLocation = convertRepositoryLocation(location, repositoryDir, suffix); if (StringUtils.hasText(convertedLocation)) { convertedLocations.add(convertedLocation); } } return convertedLocations; } private static String findSuffix(DataSource dataSource) { try (Connection connection = dataSource.getConnection()) { DatabaseDriver databaseDriver = DatabaseDriver.fromJdbcUrl(connection.getMetaData().getURL()); if (DatabaseDriver.H2.equals(databaseDriver)) { return "-default"; } if (DatabaseDriver.MYSQL.equals(databaseDriver)) { return "-database-not-specified"; } } catch (SQLException ex) { return ""; } return ""; } private static String findRepositoryDirectory() { CodeSource codeSource = ApolloDataSourceScriptDatabaseInitializer.class.getProtectionDomain().getCodeSource(); URL location = codeSource != null ? codeSource.getLocation() : null; if (location == null) { return null; } if ("jar".equals(location.getProtocol())) { // running with jar return "classpath:META-INF/sql"; } if ("file".equals(location.getProtocol())) { // running with ide String locationText = location.toString(); if (!locationText.endsWith("/apollo-common/target/classes/")) { throw new IllegalStateException( "can not determine repository directory from classpath: " + locationText); } return locationText.replace("/apollo-common/target/classes/", "/scripts/sql"); } return null; } private static String convertRepositoryLocation(String location, String repositoryDir, String suffix) { if (!StringUtils.hasText(location)) { return location; } if (!StringUtils.hasText(repositoryDir)) { // repository dir not found return null; } return location.replace("@@repository@@", repositoryDir).replace("@@suffix@@", suffix); } private static List scriptLocations(List locations, String fallback, String platform) { if (locations != null) { return locations; } List fallbackLocations = new ArrayList<>(); fallbackLocations.add("optional:classpath*:" + fallback + "-" + platform + ".sql"); fallbackLocations.add("optional:classpath*:" + fallback + ".sql"); return fallbackLocations; } } ================================================ FILE: apollo-common/src/main/java/com/ctrip/framework/apollo/common/datasource/ApolloSqlInitializationProperties.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.common.datasource; import java.nio.charset.Charset; import java.util.List; import org.springframework.boot.sql.init.DatabaseInitializationMode; public class ApolloSqlInitializationProperties { /** * Locations of the schema (DDL) scripts to apply to the database. */ private List schemaLocations; /** * Locations of the data (DML) scripts to apply to the database. */ private List dataLocations; /** * Platform to use in the default schema or data script locations, schema-${platform}.sql and * data-${platform}.sql. */ private String platform = "all"; /** * Username of the database to use when applying initialization scripts (if different). */ private String username; /** * Password of the database to use when applying initialization scripts (if different). */ private String password; /** * Whether initialization should continue when an error occurs. */ private boolean continueOnError = false; /** * Statement separator in the schema and data scripts. */ private String separator = ";"; /** * Encoding of the schema and data scripts. */ private Charset encoding; /** * Mode to apply when determining whether initialization should be performed. */ private DatabaseInitializationMode mode = DatabaseInitializationMode.EMBEDDED; public List getSchemaLocations() { return this.schemaLocations; } public void setSchemaLocations(List schemaLocations) { this.schemaLocations = schemaLocations; } public List getDataLocations() { return this.dataLocations; } public void setDataLocations(List dataLocations) { this.dataLocations = dataLocations; } public String getPlatform() { return this.platform; } public void setPlatform(String platform) { this.platform = platform; } public String getUsername() { return this.username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return this.password; } public void setPassword(String password) { this.password = password; } public boolean isContinueOnError() { return this.continueOnError; } public void setContinueOnError(boolean continueOnError) { this.continueOnError = continueOnError; } public String getSeparator() { return this.separator; } public void setSeparator(String separator) { this.separator = separator; } public Charset getEncoding() { return this.encoding; } public void setEncoding(Charset encoding) { this.encoding = encoding; } public DatabaseInitializationMode getMode() { return this.mode; } public void setMode(DatabaseInitializationMode mode) { this.mode = mode; } } ================================================ FILE: apollo-common/src/main/java/com/ctrip/framework/apollo/common/dto/AccessKeyDTO.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.common.dto; public class AccessKeyDTO extends BaseDTO { private Long id; private String secret; private String appId; private Integer mode; private Boolean enabled; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getSecret() { return secret; } public void setSecret(String secret) { this.secret = secret; } public String getAppId() { return appId; } public void setAppId(String appId) { this.appId = appId; } public Integer getMode() { return mode; } public void setMode(Integer mode) { this.mode = mode; } public Boolean getEnabled() { return enabled; } public void setEnabled(Boolean enabled) { this.enabled = enabled; } } ================================================ FILE: apollo-common/src/main/java/com/ctrip/framework/apollo/common/dto/AppDTO.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.common.dto; import com.ctrip.framework.apollo.common.utils.InputValidator; import jakarta.validation.constraints.Pattern; public class AppDTO extends BaseDTO { private long id; private String name; @Pattern(regexp = InputValidator.CLUSTER_NAMESPACE_VALIDATOR, message = "Invalid AppId format: " + InputValidator.INVALID_CLUSTER_NAMESPACE_MESSAGE) private String appId; private String orgId; private String orgName; private String ownerName; private String ownerDisplayName; private String ownerEmail; public long getId() { return id; } public void setId(long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getAppId() { return appId; } public void setAppId(String appId) { this.appId = appId; } public String getOrgId() { return orgId; } public void setOrgId(String orgId) { this.orgId = orgId; } public String getOrgName() { return orgName; } public void setOrgName(String orgName) { this.orgName = orgName; } public String getOwnerName() { return ownerName; } public void setOwnerName(String ownerName) { this.ownerName = ownerName; } public String getOwnerDisplayName() { return ownerDisplayName; } public void setOwnerDisplayName(String ownerDisplayName) { this.ownerDisplayName = ownerDisplayName; } public String getOwnerEmail() { return ownerEmail; } public void setOwnerEmail(String ownerEmail) { this.ownerEmail = ownerEmail; } } ================================================ FILE: apollo-common/src/main/java/com/ctrip/framework/apollo/common/dto/AppNamespaceDTO.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.common.dto; public class AppNamespaceDTO extends BaseDTO { private long id; private String name; private String appId; private String comment; private String format; private boolean isPublic = false; public long getId() { return id; } public void setId(long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getAppId() { return appId; } public void setAppId(String appId) { this.appId = appId; } public String getFormat() { return format; } public void setFormat(String format) { this.format = format; } public boolean isPublic() { return isPublic; } public void setPublic(boolean aPublic) { isPublic = aPublic; } public String getComment() { return comment; } public void setComment(String comment) { this.comment = comment; } } ================================================ FILE: apollo-common/src/main/java/com/ctrip/framework/apollo/common/dto/BaseDTO.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.common.dto; import java.util.Date; public class BaseDTO { protected String dataChangeCreatedBy; protected String dataChangeLastModifiedBy; protected String dataChangeCreatedByDisplayName; protected String dataChangeLastModifiedByDisplayName; protected Date dataChangeCreatedTime; protected Date dataChangeLastModifiedTime; public String getDataChangeCreatedBy() { return dataChangeCreatedBy; } public void setDataChangeCreatedBy(String dataChangeCreatedBy) { this.dataChangeCreatedBy = dataChangeCreatedBy; } public String getDataChangeLastModifiedBy() { return dataChangeLastModifiedBy; } public void setDataChangeLastModifiedBy(String dataChangeLastModifiedBy) { this.dataChangeLastModifiedBy = dataChangeLastModifiedBy; } public String getDataChangeCreatedByDisplayName() { return dataChangeCreatedByDisplayName; } public void setDataChangeCreatedByDisplayName(String dataChangeCreatedByDisplayName) { this.dataChangeCreatedByDisplayName = dataChangeCreatedByDisplayName; } public String getDataChangeLastModifiedByDisplayName() { return dataChangeLastModifiedByDisplayName; } public void setDataChangeLastModifiedByDisplayName(String dataChangeLastModifiedByDisplayName) { this.dataChangeLastModifiedByDisplayName = dataChangeLastModifiedByDisplayName; } public Date getDataChangeCreatedTime() { return dataChangeCreatedTime; } public void setDataChangeCreatedTime(Date dataChangeCreatedTime) { this.dataChangeCreatedTime = dataChangeCreatedTime; } public Date getDataChangeLastModifiedTime() { return dataChangeLastModifiedTime; } public void setDataChangeLastModifiedTime(Date dataChangeLastModifiedTime) { this.dataChangeLastModifiedTime = dataChangeLastModifiedTime; } } ================================================ FILE: apollo-common/src/main/java/com/ctrip/framework/apollo/common/dto/ClusterDTO.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.common.dto; import com.ctrip.framework.apollo.common.utils.InputValidator; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Pattern; public class ClusterDTO extends BaseDTO { private long id; @NotBlank(message = "cluster name cannot be blank") @Pattern(regexp = InputValidator.CLUSTER_NAMESPACE_VALIDATOR, message = "Invalid Cluster format: " + InputValidator.INVALID_CLUSTER_NAMESPACE_MESSAGE) private String name; @NotBlank(message = "appId cannot be blank") private String appId; private long parentClusterId; private String comment; public long getId() { return id; } public void setId(long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getAppId() { return appId; } public void setAppId(String appId) { this.appId = appId; } public long getParentClusterId() { return parentClusterId; } public void setParentClusterId(long parentClusterId) { this.parentClusterId = parentClusterId; } public String getComment() { return comment; } public void setComment(String comment) { this.comment = comment; } } ================================================ FILE: apollo-common/src/main/java/com/ctrip/framework/apollo/common/dto/CommitDTO.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.common.dto; public class CommitDTO extends BaseDTO { private String changeSets; private String appId; private String clusterName; private String namespaceName; private String comment; public String getChangeSets() { return changeSets; } public void setChangeSets(String changeSets) { this.changeSets = changeSets; } public String getAppId() { return appId; } public void setAppId(String appId) { this.appId = appId; } public String getClusterName() { return clusterName; } public void setClusterName(String clusterName) { this.clusterName = clusterName; } public String getNamespaceName() { return namespaceName; } public void setNamespaceName(String namespaceName) { this.namespaceName = namespaceName; } public String getComment() { return comment; } public void setComment(String comment) { this.comment = comment; } } ================================================ FILE: apollo-common/src/main/java/com/ctrip/framework/apollo/common/dto/GrayReleaseRuleDTO.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.common.dto; import com.google.common.collect.Sets; import java.util.Set; public class GrayReleaseRuleDTO extends BaseDTO { private String appId; private String clusterName; private String namespaceName; private String branchName; private Set ruleItems; private Long releaseId; public GrayReleaseRuleDTO(String appId, String clusterName, String namespaceName, String branchName) { this.appId = appId; this.clusterName = clusterName; this.namespaceName = namespaceName; this.branchName = branchName; this.ruleItems = Sets.newHashSet(); } public String getAppId() { return appId; } public String getClusterName() { return clusterName; } public String getNamespaceName() { return namespaceName; } public String getBranchName() { return branchName; } public Set getRuleItems() { return ruleItems; } public void setRuleItems(Set ruleItems) { this.ruleItems = ruleItems; } public void addRuleItem(GrayReleaseRuleItemDTO ruleItem) { this.ruleItems.add(ruleItem); } public Long getReleaseId() { return releaseId; } public void setReleaseId(Long releaseId) { this.releaseId = releaseId; } } ================================================ FILE: apollo-common/src/main/java/com/ctrip/framework/apollo/common/dto/GrayReleaseRuleItemDTO.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.common.dto; import com.google.common.collect.Sets; import java.util.Set; import static com.google.common.base.MoreObjects.toStringHelper; /** * @author Jason Song(song_s@ctrip.com) */ public class GrayReleaseRuleItemDTO { public static final String ALL_IP = "*"; public static final String ALL_Label = "*"; private String clientAppId; private Set clientIpList; private Set clientLabelList; // this default constructor is for json deserialize use, to make sure all fields are initialized public GrayReleaseRuleItemDTO() { this(""); } public GrayReleaseRuleItemDTO(String clientAppId) { this(clientAppId, Sets.newHashSet(), Sets.newHashSet()); } public GrayReleaseRuleItemDTO(String clientAppId, Set clientIpList, Set clientLabelList) { this.clientAppId = clientAppId; this.clientIpList = clientIpList; this.clientLabelList = clientLabelList; } public String getClientAppId() { return clientAppId; } public Set getClientIpList() { return clientIpList; } public Set getClientLabelList() { return clientLabelList; } public boolean matches(String clientAppId, String clientIp, String clientLabel) { return (appIdMatches(clientAppId) && ipMatches(clientIp)) || (appIdMatches(clientAppId) && labelMatches(clientLabel)); } private boolean appIdMatches(String clientAppId) { return this.clientAppId.equalsIgnoreCase(clientAppId); } private boolean ipMatches(String clientIp) { return this.clientIpList.contains(ALL_IP) || clientIpList.contains(clientIp); } private boolean labelMatches(String clientLabel) { return this.clientLabelList.contains(ALL_Label) || clientLabelList.contains(clientLabel); } @Override public String toString() { return toStringHelper(this).add("clientAppId", clientAppId).add("clientIpList", clientIpList) .add("clientLabelList", clientLabelList).toString(); } } ================================================ FILE: apollo-common/src/main/java/com/ctrip/framework/apollo/common/dto/InstanceConfigDTO.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.common.dto; import java.util.Date; /** * @author Jason Song(song_s@ctrip.com) */ public class InstanceConfigDTO { private ReleaseDTO release; private Date releaseDeliveryTime; private Date dataChangeLastModifiedTime; public ReleaseDTO getRelease() { return release; } public void setRelease(ReleaseDTO release) { this.release = release; } public Date getDataChangeLastModifiedTime() { return dataChangeLastModifiedTime; } public void setDataChangeLastModifiedTime(Date dataChangeLastModifiedTime) { this.dataChangeLastModifiedTime = dataChangeLastModifiedTime; } public Date getReleaseDeliveryTime() { return releaseDeliveryTime; } public void setReleaseDeliveryTime(Date releaseDeliveryTime) { this.releaseDeliveryTime = releaseDeliveryTime; } } ================================================ FILE: apollo-common/src/main/java/com/ctrip/framework/apollo/common/dto/InstanceDTO.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.common.dto; import java.util.Date; import java.util.List; /** * @author Jason Song(song_s@ctrip.com) */ public class InstanceDTO { private long id; private String appId; private String clusterName; private String dataCenter; private String ip; private List configs; private Date dataChangeCreatedTime; public long getId() { return id; } public void setId(long id) { this.id = id; } public String getAppId() { return appId; } public void setAppId(String appId) { this.appId = appId; } public String getClusterName() { return clusterName; } public void setClusterName(String clusterName) { this.clusterName = clusterName; } public String getDataCenter() { return dataCenter; } public void setDataCenter(String dataCenter) { this.dataCenter = dataCenter; } public String getIp() { return ip; } public void setIp(String ip) { this.ip = ip; } public List getConfigs() { return configs; } public void setConfigs(List configs) { this.configs = configs; } public Date getDataChangeCreatedTime() { return dataChangeCreatedTime; } public void setDataChangeCreatedTime(Date dataChangeCreatedTime) { this.dataChangeCreatedTime = dataChangeCreatedTime; } } ================================================ FILE: apollo-common/src/main/java/com/ctrip/framework/apollo/common/dto/ItemChangeSets.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.common.dto; import java.util.LinkedList; import java.util.List; /** * storage cud result */ public class ItemChangeSets extends BaseDTO { private List createItems = new LinkedList<>(); private List updateItems = new LinkedList<>(); private List deleteItems = new LinkedList<>(); public void addCreateItem(ItemDTO item) { createItems.add(item); } public void addUpdateItem(ItemDTO item) { updateItems.add(item); } public void addDeleteItem(ItemDTO item) { deleteItems.add(item); } public boolean isEmpty() { return createItems.isEmpty() && updateItems.isEmpty() && deleteItems.isEmpty(); } public List getCreateItems() { return createItems; } public List getUpdateItems() { return updateItems; } public List getDeleteItems() { return deleteItems; } public void setCreateItems(List createItems) { this.createItems = createItems; } public void setUpdateItems(List updateItems) { this.updateItems = updateItems; } public void setDeleteItems(List deleteItems) { this.deleteItems = deleteItems; } } ================================================ FILE: apollo-common/src/main/java/com/ctrip/framework/apollo/common/dto/ItemDTO.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.common.dto; public class ItemDTO extends BaseDTO { private long id; private long namespaceId; private String key; private int type; private String value; private String comment; private int lineNum; public ItemDTO() { } public ItemDTO(String key, String value, String comment, int lineNum) { this.key = key; this.value = value; this.comment = comment; this.lineNum = lineNum; } public long getId() { return id; } public void setId(long id) { this.id = id; } public String getComment() { return comment; } public String getKey() { return key; } public long getNamespaceId() { return namespaceId; } public String getValue() { return value; } public void setComment(String comment) { this.comment = comment; } public void setKey(String key) { this.key = key; } public void setNamespaceId(long namespaceId) { this.namespaceId = namespaceId; } public void setValue(String value) { this.value = value; } public int getLineNum() { return lineNum; } public void setLineNum(int lineNum) { this.lineNum = lineNum; } public int getType() { return type; } public void setType(int type) { this.type = type; } } ================================================ FILE: apollo-common/src/main/java/com/ctrip/framework/apollo/common/dto/ItemInfoDTO.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.common.dto; public class ItemInfoDTO extends BaseDTO { private String appId; private String clusterName; private String namespaceName; private String key; private String value; public ItemInfoDTO() {} public ItemInfoDTO(String appId, String clusterName, String namespaceName, String key, String value) { this.appId = appId; this.clusterName = clusterName; this.namespaceName = namespaceName; this.key = key; this.value = value; } public String getAppId() { return appId; } public void setAppId(String appId) { this.appId = appId; } public String getClusterName() { return clusterName; } public void setClusterName(String clusterName) { this.clusterName = clusterName; } public String getNamespaceName() { return namespaceName; } public void setNamespaceName(String namespaceName) { this.namespaceName = namespaceName; } public String getKey() { return key; } public void setKey(String key) { this.key = key; } public String getValue() { return value; } public void setValue(String value) { this.value = value; } @Override public String toString() { return "ItemInfoDTO{" + "appId='" + appId + '\'' + ", clusterName='" + clusterName + '\'' + ", namespaceName='" + namespaceName + '\'' + ", key='" + key + '\'' + ", value='" + value + '\'' + '}'; } } ================================================ FILE: apollo-common/src/main/java/com/ctrip/framework/apollo/common/dto/NamespaceDTO.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.common.dto; import com.ctrip.framework.apollo.common.utils.InputValidator; import jakarta.validation.constraints.Pattern; public class NamespaceDTO extends BaseDTO { private long id; private String appId; private String clusterName; @Pattern(regexp = InputValidator.CLUSTER_NAMESPACE_VALIDATOR, message = "Invalid Namespace format: " + InputValidator.INVALID_CLUSTER_NAMESPACE_MESSAGE) private String namespaceName; public long getId() { return id; } public void setId(long id) { this.id = id; } public String getAppId() { return appId; } public String getClusterName() { return clusterName; } public String getNamespaceName() { return namespaceName; } public void setAppId(String appId) { this.appId = appId; } public void setClusterName(String clusterName) { this.clusterName = clusterName; } public void setNamespaceName(String namespaceName) { this.namespaceName = namespaceName; } } ================================================ FILE: apollo-common/src/main/java/com/ctrip/framework/apollo/common/dto/NamespaceLockDTO.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.common.dto; public class NamespaceLockDTO extends BaseDTO { private long namespaceId; public long getNamespaceId() { return namespaceId; } public void setNamespaceId(long namespaceId) { this.namespaceId = namespaceId; } } ================================================ FILE: apollo-common/src/main/java/com/ctrip/framework/apollo/common/dto/PageDTO.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.common.dto; import org.springframework.data.domain.Pageable; import java.util.Collections; import java.util.List; /** * @author Jason Song(song_s@ctrip.com) */ public class PageDTO { private final long total; private final List content; private final int page; private final int size; public PageDTO(List content, Pageable pageable, long total) { this.total = total; this.content = content; this.page = pageable.getPageNumber(); this.size = pageable.getPageSize(); } public long getTotal() { return total; } public List getContent() { return Collections.unmodifiableList(content); } public int getPage() { return page; } public int getSize() { return size; } public boolean hasContent() { return content != null && !content.isEmpty(); } } ================================================ FILE: apollo-common/src/main/java/com/ctrip/framework/apollo/common/dto/ReleaseDTO.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.common.dto; public class ReleaseDTO extends BaseDTO { private long id; private String releaseKey; private String name; private String appId; private String clusterName; private String namespaceName; private String configurations; private String comment; private boolean isAbandoned; public long getId() { return id; } public void setId(long id) { this.id = id; } public String getReleaseKey() { return releaseKey; } public void setReleaseKey(String releaseKey) { this.releaseKey = releaseKey; } public String getAppId() { return appId; } public String getClusterName() { return clusterName; } public String getComment() { return comment; } public String getConfigurations() { return configurations; } public String getName() { return name; } public String getNamespaceName() { return namespaceName; } public void setAppId(String appId) { this.appId = appId; } public void setClusterName(String clusterName) { this.clusterName = clusterName; } public void setComment(String comment) { this.comment = comment; } public void setConfigurations(String configurations) { this.configurations = configurations; } public void setName(String name) { this.name = name; } public void setNamespaceName(String namespaceName) { this.namespaceName = namespaceName; } public boolean isAbandoned() { return isAbandoned; } public void setAbandoned(boolean abandoned) { isAbandoned = abandoned; } } ================================================ FILE: apollo-common/src/main/java/com/ctrip/framework/apollo/common/dto/ReleaseHistoryDTO.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.common.dto; import java.util.Map; public class ReleaseHistoryDTO extends BaseDTO { private long id; private String appId; private String clusterName; private String namespaceName; private String branchName; private long releaseId; private long previousReleaseId; private int operation; private Map operationContext; public ReleaseHistoryDTO() {} public long getId() { return id; } public void setId(long id) { this.id = id; } public String getAppId() { return appId; } public void setAppId(String appId) { this.appId = appId; } public String getClusterName() { return clusterName; } public void setClusterName(String clusterName) { this.clusterName = clusterName; } public String getNamespaceName() { return namespaceName; } public void setNamespaceName(String namespaceName) { this.namespaceName = namespaceName; } public String getBranchName() { return branchName; } public void setBranchName(String branchName) { this.branchName = branchName; } public long getReleaseId() { return releaseId; } public void setReleaseId(long releaseId) { this.releaseId = releaseId; } public long getPreviousReleaseId() { return previousReleaseId; } public void setPreviousReleaseId(long previousReleaseId) { this.previousReleaseId = previousReleaseId; } public int getOperation() { return operation; } public void setOperation(int operation) { this.operation = operation; } public Map getOperationContext() { return operationContext; } public void setOperationContext(Map operationContext) { this.operationContext = operationContext; } } ================================================ FILE: apollo-common/src/main/java/com/ctrip/framework/apollo/common/entity/App.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.common.entity; import com.ctrip.framework.apollo.audit.annotation.ApolloAuditLogDataInfluenceTable; import com.ctrip.framework.apollo.audit.annotation.ApolloAuditLogDataInfluenceTableField; import com.ctrip.framework.apollo.common.utils.InputValidator; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Pattern; import org.hibernate.annotations.SQLDelete; import org.hibernate.annotations.Where; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.Table; @Entity @Table(name = "`App`") @SQLDelete( sql = "Update `App` set IsDeleted = true, DeletedAt = ROUND(UNIX_TIMESTAMP(NOW(4))*1000) where Id = ?") @Where(clause = "`IsDeleted` = false") @ApolloAuditLogDataInfluenceTable(tableName = "App") public class App extends BaseEntity { @NotBlank(message = "Name cannot be blank") @Column(name = "`Name`", nullable = false) @ApolloAuditLogDataInfluenceTableField(fieldName = "Name") private String name; @NotBlank(message = "AppId cannot be blank") @Pattern(regexp = InputValidator.CLUSTER_NAMESPACE_VALIDATOR, message = InputValidator.INVALID_CLUSTER_NAMESPACE_MESSAGE) @Column(name = "`AppId`", nullable = false) @ApolloAuditLogDataInfluenceTableField(fieldName = "AppId") private String appId; @Column(name = "`OrgId`", nullable = false) private String orgId; @Column(name = "`OrgName`", nullable = false) private String orgName; @NotBlank(message = "OwnerName cannot be blank") @Column(name = "`OwnerName`", nullable = false) private String ownerName; @NotBlank(message = "OwnerEmail cannot be blank") @Column(name = "`OwnerEmail`", nullable = false) private String ownerEmail; public String getAppId() { return appId; } public String getName() { return name; } public String getOrgId() { return orgId; } public String getOrgName() { return orgName; } public String getOwnerEmail() { return ownerEmail; } public String getOwnerName() { return ownerName; } public void setAppId(String appId) { this.appId = appId; } public void setName(String name) { this.name = name; } public void setOrgId(String orgId) { this.orgId = orgId; } public void setOrgName(String orgName) { this.orgName = orgName; } public void setOwnerEmail(String ownerEmail) { this.ownerEmail = ownerEmail; } public void setOwnerName(String ownerName) { this.ownerName = ownerName; } @Override public String toString() { return toStringHelper().add("name", name).add("appId", appId).add("orgId", orgId) .add("orgName", orgName).add("ownerName", ownerName).add("ownerEmail", ownerEmail) .toString(); } public static class Builder { public Builder() {} private App app = new App(); public Builder name(String name) { app.setName(name); return this; } public Builder appId(String appId) { app.setAppId(appId); return this; } public Builder orgId(String orgId) { app.setOrgId(orgId); return this; } public Builder orgName(String orgName) { app.setOrgName(orgName); return this; } public Builder ownerName(String ownerName) { app.setOwnerName(ownerName); return this; } public Builder ownerEmail(String ownerEmail) { app.setOwnerEmail(ownerEmail); return this; } public App build() { return app; } } public static Builder builder() { return new Builder(); } } ================================================ FILE: apollo-common/src/main/java/com/ctrip/framework/apollo/common/entity/AppNamespace.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.common.entity; import com.ctrip.framework.apollo.audit.annotation.ApolloAuditLogDataInfluenceTable; import com.ctrip.framework.apollo.audit.annotation.ApolloAuditLogDataInfluenceTableField; import com.ctrip.framework.apollo.common.utils.InputValidator; import com.ctrip.framework.apollo.core.enums.ConfigFileFormat; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Pattern; import org.hibernate.annotations.SQLDelete; import org.hibernate.annotations.Where; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.Table; @Entity @Table(name = "`AppNamespace`") @SQLDelete( sql = "Update `AppNamespace` set IsDeleted = true, DeletedAt = ROUND(UNIX_TIMESTAMP(NOW(4))*1000) where Id = ?") @Where(clause = "`IsDeleted` = false") @ApolloAuditLogDataInfluenceTable(tableName = "AppNamespace") public class AppNamespace extends BaseEntity { @NotBlank(message = "AppNamespace Name cannot be blank") @Pattern(regexp = InputValidator.CLUSTER_NAMESPACE_VALIDATOR, message = "Invalid Namespace format: " + InputValidator.INVALID_CLUSTER_NAMESPACE_MESSAGE + " & " + InputValidator.INVALID_NAMESPACE_NAMESPACE_MESSAGE) @Column(name = "`Name`", nullable = false) @ApolloAuditLogDataInfluenceTableField(fieldName = "Name") private String name; @ApolloAuditLogDataInfluenceTableField(fieldName = "AppId") @NotBlank(message = "AppId cannot be blank") @Column(name = "`AppId`", nullable = false) private String appId; @ApolloAuditLogDataInfluenceTableField(fieldName = "Format") @Column(name = "`Format`", nullable = false) private String format; @ApolloAuditLogDataInfluenceTableField(fieldName = "IsPublic") @Column(name = "`IsPublic`", columnDefinition = "Bit default '0'") private boolean isPublic = false; @Column(name = "`Comment`") private String comment; public String getAppId() { return appId; } public String getComment() { return comment; } public String getName() { return name; } public void setAppId(String appId) { this.appId = appId; } public void setComment(String comment) { this.comment = comment; } public void setName(String name) { this.name = name; } public boolean isPublic() { return isPublic; } public void setPublic(boolean aPublic) { isPublic = aPublic; } public ConfigFileFormat formatAsEnum() { return ConfigFileFormat.fromString(this.format); } public String getFormat() { return format; } public void setFormat(String format) { this.format = format; } @Override public String toString() { return toStringHelper().add("name", name).add("appId", appId).add("comment", comment) .add("format", format).add("isPublic", isPublic).toString(); } } ================================================ FILE: apollo-common/src/main/java/com/ctrip/framework/apollo/common/entity/BaseEntity.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.common.entity; import com.ctrip.framework.apollo.audit.event.ApolloAuditLogDataInfluenceEvent; import com.google.common.base.MoreObjects; import com.google.common.base.MoreObjects.ToStringHelper; import java.util.Collection; import java.util.Collections; import java.util.Date; import jakarta.persistence.Column; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.MappedSuperclass; import jakarta.persistence.PrePersist; import jakarta.persistence.PreRemove; import jakarta.persistence.PreUpdate; import org.springframework.data.domain.DomainEvents; @MappedSuperclass public abstract class BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "`Id`") private long id; @Column(name = "`IsDeleted`", columnDefinition = "Bit default '0'") protected boolean isDeleted = false; @Column(name = "`DeletedAt`", columnDefinition = "Bigint default '0'") protected long deletedAt; @Column(name = "`DataChange_CreatedBy`", nullable = false) private String dataChangeCreatedBy; @Column(name = "`DataChange_CreatedTime`", nullable = false) private Date dataChangeCreatedTime; @Column(name = "`DataChange_LastModifiedBy`") private String dataChangeLastModifiedBy; @Column(name = "`DataChange_LastTime`") private Date dataChangeLastModifiedTime; public long getId() { return id; } public void setId(long id) { this.id = id; } public boolean isDeleted() { return isDeleted; } public void setDeleted(boolean deleted) { isDeleted = deleted; if (deleted && this.deletedAt == 0) { // also set deletedAt value as epoch millisecond this.deletedAt = System.currentTimeMillis(); } else if (!deleted) { this.deletedAt = 0L; } } public long getDeletedAt() { return deletedAt; } public String getDataChangeCreatedBy() { return dataChangeCreatedBy; } public void setDataChangeCreatedBy(String dataChangeCreatedBy) { this.dataChangeCreatedBy = dataChangeCreatedBy; } public Date getDataChangeCreatedTime() { return dataChangeCreatedTime; } public void setDataChangeCreatedTime(Date dataChangeCreatedTime) { this.dataChangeCreatedTime = dataChangeCreatedTime; } public String getDataChangeLastModifiedBy() { return dataChangeLastModifiedBy; } public void setDataChangeLastModifiedBy(String dataChangeLastModifiedBy) { this.dataChangeLastModifiedBy = dataChangeLastModifiedBy; } public Date getDataChangeLastModifiedTime() { return dataChangeLastModifiedTime; } public void setDataChangeLastModifiedTime(Date dataChangeLastModifiedTime) { this.dataChangeLastModifiedTime = dataChangeLastModifiedTime; } @PrePersist protected void prePersist() { if (this.dataChangeCreatedTime == null) { dataChangeCreatedTime = new Date(); } if (this.dataChangeLastModifiedTime == null) { dataChangeLastModifiedTime = new Date(); } } @PreUpdate protected void preUpdate() { this.dataChangeLastModifiedTime = new Date(); } @PreRemove protected void preRemove() { this.dataChangeLastModifiedTime = new Date(); } protected ToStringHelper toStringHelper() { return MoreObjects.toStringHelper(this).omitNullValues().add("id", id) .add("dataChangeCreatedBy", dataChangeCreatedBy) .add("dataChangeCreatedTime", dataChangeCreatedTime) .add("dataChangeLastModifiedBy", dataChangeLastModifiedBy) .add("dataChangeLastModifiedTime", dataChangeLastModifiedTime); } @Override public String toString() { return toStringHelper().toString(); } @DomainEvents public Collection domainEvents() { return Collections.singletonList(new ApolloAuditLogDataInfluenceEvent(this.getClass(), this)); } } ================================================ FILE: apollo-common/src/main/java/com/ctrip/framework/apollo/common/entity/EntityPair.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.common.entity; public class EntityPair { private E firstEntity; private E secondEntity; public EntityPair(E firstEntity, E secondEntity) { this.firstEntity = firstEntity; this.secondEntity = secondEntity; } public E getFirstEntity() { return firstEntity; } public void setFirstEntity(E firstEntity) { this.firstEntity = firstEntity; } public E getSecondEntity() { return secondEntity; } public void setSecondEntity(E secondEntity) { this.secondEntity = secondEntity; } } ================================================ FILE: apollo-common/src/main/java/com/ctrip/framework/apollo/common/exception/AbstractApolloHttpException.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.common.exception; import com.google.common.base.Strings; import org.springframework.http.HttpStatus; public abstract class AbstractApolloHttpException extends RuntimeException { private static final long serialVersionUID = -1713129594004951820L; protected HttpStatus httpStatus; /** * When args not empty, use {@link com.google.common.base.Strings#lenientFormat(String, Object...)} * to replace %s in msgTpl with args to set the error message. Otherwise, use msgTpl * to set the error message. e.g.: *
{@code new NotFoundException("... %s ... %s ... %s", "str", 0, 0.1)}
* If the number of '%s' in `msgTpl` does not match args length, the '%s' string will be printed. */ public AbstractApolloHttpException(String msgTpl, Object... args) { super(args == null || args.length == 0 ? msgTpl : Strings.lenientFormat(msgTpl, args)); } public AbstractApolloHttpException(String msg, Exception e) { super(msg, e); } protected void setHttpStatus(HttpStatus httpStatus) { this.httpStatus = httpStatus; } public HttpStatus getHttpStatus() { return httpStatus; } } ================================================ FILE: apollo-common/src/main/java/com/ctrip/framework/apollo/common/exception/BadRequestException.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.common.exception; import org.springframework.http.HttpStatus; public class BadRequestException extends AbstractApolloHttpException { /** * @see AbstractApolloHttpException#AbstractApolloHttpException(String, Object...) */ public BadRequestException(String msgtpl, Object... args) { super(msgtpl, args); setHttpStatus(HttpStatus.BAD_REQUEST); } public static BadRequestException ownerNameIsBlank() { return new BadRequestException("ownerName can not be blank"); } public static BadRequestException orgIdIsBlank() { return new BadRequestException("orgId can not be blank"); } public static BadRequestException rateLimitIsInvalid() { return new BadRequestException("rate limit must be greater than 1"); } public static BadRequestException itemAlreadyExists(String itemKey) { return new BadRequestException("item already exists for itemKey:%s", itemKey); } public static BadRequestException itemNotExists(long itemId) { return new BadRequestException("item not exists for itemId:%s", itemId); } public static BadRequestException namespaceNotExists() { return new BadRequestException("namespace not exist."); } public static BadRequestException namespaceNotExists(String appId, String clusterName, String namespaceName) { return new BadRequestException( "namespace not exist for appId:%s clusterName:%s namespaceName:%s", appId, clusterName, namespaceName); } public static BadRequestException namespaceAlreadyExists(String namespaceName) { return new BadRequestException("namespace already exists for namespaceName:%s", namespaceName); } public static BadRequestException appNamespaceNotExists(String appId, String namespaceName) { return new BadRequestException("appNamespace not exist for appId:%s namespaceName:%s", appId, namespaceName); } public static BadRequestException appNamespaceAlreadyExists(String appId, String namespaceName) { return new BadRequestException("appNamespace already exists for appId:%s namespaceName:%s", appId, namespaceName); } public static BadRequestException invalidNamespaceFormat(String format) { return new BadRequestException("invalid namespace format:%s", format); } public static BadRequestException invalidNotificationsFormat(String format) { return new BadRequestException("invalid notifications format:%s", format); } public static BadRequestException invalidClusterNameFormat(String format) { return new BadRequestException("invalid clusterName format:%s", format); } public static BadRequestException invalidRoleTypeFormat(String format) { return new BadRequestException("invalid roleType format:%s", format); } public static BadRequestException invalidEnvFormat(String format) { return new BadRequestException("invalid env format:%s", format); } public static BadRequestException namespaceNotMatch() { return new BadRequestException("invalid request, item and namespace do not match!"); } public static BadRequestException appNotExists(String appId) { return new BadRequestException("app not exists for appId:%s", appId); } public static BadRequestException appAlreadyExists(String appId) { return new BadRequestException("app already exists for appId:%s", appId); } public static BadRequestException appIdIsBlank() { return new BadRequestException("appId can not be blank"); } public static BadRequestException appNameIsBlank() { return new BadRequestException("app name can not be blank"); } public static BadRequestException clusterNotExists(String clusterName) { return new BadRequestException("cluster not exists for clusterName:%s", clusterName); } public static BadRequestException clusterAlreadyExists(String clusterName) { return new BadRequestException("cluster already exists for clusterName:%s", clusterName); } public static BadRequestException userNotExists(String userName) { return new BadRequestException("user not exists for userName:%s", userName); } public static BadRequestException userAlreadyExists(String userName) { return new BadRequestException("user already exists for userName:%s", userName); } public static BadRequestException userAlreadyAuthorized(String userName) { return new BadRequestException("%s already authorized", userName); } public static BadRequestException accessKeyNotExists() { return new BadRequestException("accessKey not exist."); } } ================================================ FILE: apollo-common/src/main/java/com/ctrip/framework/apollo/common/exception/BeanUtilsException.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.common.exception; public class BeanUtilsException extends RuntimeException { public BeanUtilsException(Throwable e) { super(e); } } ================================================ FILE: apollo-common/src/main/java/com/ctrip/framework/apollo/common/exception/NotFoundException.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.common.exception; import org.springframework.http.HttpStatus; public class NotFoundException extends AbstractApolloHttpException { /** * @see AbstractApolloHttpException#AbstractApolloHttpException(String, Object...) */ public NotFoundException(String msgTpl, Object... args) { super(msgTpl, args); setHttpStatus(HttpStatus.NOT_FOUND); } public static NotFoundException itemNotFound(long itemId) { return new NotFoundException("item not found for itemId:%s", itemId); } public static NotFoundException itemNotFound(String itemKey) { return new NotFoundException("item not found for itemKey:%s", itemKey); } public static NotFoundException itemNotFound(String appId, String clusterName, String namespaceName, String itemKey) { return new NotFoundException( "item not found for appId:%s clusterName:%s namespaceName:%s itemKey:%s", appId, clusterName, namespaceName, itemKey); } public static NotFoundException itemNotFound(String appId, String clusterName, String namespaceName, long itemId) { return new NotFoundException( "item not found for appId:%s clusterName:%s namespaceName:%s itemId:%s", appId, clusterName, namespaceName, itemId); } public static NotFoundException namespaceNotFound(String appId, String clusterName, String namespaceName) { return new NotFoundException("namespace not found for appId:%s clusterName:%s namespaceName:%s", appId, clusterName, namespaceName); } public static NotFoundException namespaceNotFound(long namespaceId) { return new NotFoundException("namespace not found for namespaceId:%s", namespaceId); } public static NotFoundException releaseNotFound(Object releaseId) { return new NotFoundException("release not found for releaseId:%s", releaseId); } public static NotFoundException clusterNotFound(String appId, String clusterName) { return new NotFoundException("cluster not found for appId:%s clusterName:%s", appId, clusterName); } public static NotFoundException appNotFound(String appId) { return new NotFoundException("app not found for appId:%s", appId); } public static NotFoundException roleNotFound(String roleName) { return new NotFoundException( "role not found for roleName:%s, please check apollo portal DB table 'Role'", roleName); } } ================================================ FILE: apollo-common/src/main/java/com/ctrip/framework/apollo/common/exception/ServiceException.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.common.exception; import org.springframework.http.HttpStatus; public class ServiceException extends AbstractApolloHttpException { /** * @see AbstractApolloHttpException#AbstractApolloHttpException(String, Object...) */ public ServiceException(String msgtpl, Object... args) { super(msgtpl, args); setHttpStatus(HttpStatus.INTERNAL_SERVER_ERROR); } public ServiceException(String str, Exception e) { super(str, e); setHttpStatus(HttpStatus.INTERNAL_SERVER_ERROR); } } ================================================ FILE: apollo-common/src/main/java/com/ctrip/framework/apollo/common/http/MultiResponseEntity.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.common.http; import org.springframework.http.HttpStatus; import java.util.LinkedList; import java.util.List; /** * 一个Response中包含多个ResponseEntity */ public class MultiResponseEntity { private int code; private List> entities = new LinkedList<>(); private MultiResponseEntity(HttpStatus httpCode) { this.code = httpCode.value(); } public static MultiResponseEntity instance(HttpStatus statusCode) { return new MultiResponseEntity<>(statusCode); } public static MultiResponseEntity ok() { return new MultiResponseEntity<>(HttpStatus.OK); } public void addResponseEntity(RichResponseEntity responseEntity) { if (responseEntity == null) { throw new IllegalArgumentException("sub response entity can not be null"); } entities.add(responseEntity); } } ================================================ FILE: apollo-common/src/main/java/com/ctrip/framework/apollo/common/http/RichResponseEntity.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.common.http; import org.springframework.http.HttpStatus; public class RichResponseEntity { private int code; private Object message; private T body; public static RichResponseEntity ok(T body) { RichResponseEntity richResponseEntity = new RichResponseEntity<>(); richResponseEntity.message = HttpStatus.OK.getReasonPhrase(); richResponseEntity.code = HttpStatus.OK.value(); richResponseEntity.body = body; return richResponseEntity; } public static RichResponseEntity error(HttpStatus httpCode, Object message) { RichResponseEntity richResponseEntity = new RichResponseEntity<>(); richResponseEntity.message = message; richResponseEntity.code = httpCode.value(); return richResponseEntity; } public int getCode() { return code; } public Object getMessage() { return message; } public T getBody() { return body; } } ================================================ FILE: apollo-common/src/main/java/com/ctrip/framework/apollo/common/http/SearchResponseEntity.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.common.http; import org.springframework.http.HttpStatus; public class SearchResponseEntity { private T body; private boolean hasMoreData; private Object message; private int code; public static SearchResponseEntity ok(T body) { SearchResponseEntity SearchResponseEntity = new SearchResponseEntity<>(); SearchResponseEntity.message = HttpStatus.OK.getReasonPhrase(); SearchResponseEntity.code = HttpStatus.OK.value(); SearchResponseEntity.body = body; SearchResponseEntity.hasMoreData = false; return SearchResponseEntity; } public static SearchResponseEntity okWithMessage(T body, Object message) { SearchResponseEntity SearchResponseEntity = new SearchResponseEntity<>(); SearchResponseEntity.message = message; SearchResponseEntity.code = HttpStatus.OK.value(); SearchResponseEntity.body = body; SearchResponseEntity.hasMoreData = true; return SearchResponseEntity; } public static SearchResponseEntity error(HttpStatus httpCode, Object message) { SearchResponseEntity SearchResponseEntity = new SearchResponseEntity<>(); SearchResponseEntity.message = message; SearchResponseEntity.code = httpCode.value(); return SearchResponseEntity; } public int getCode() { return code; } public Object getMessage() { return message; } public T getBody() { return body; } public boolean isHasMoreData() { return hasMoreData; } } ================================================ FILE: apollo-common/src/main/java/com/ctrip/framework/apollo/common/jpa/H2Function.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.common.jpa; /** * @author nisiyong */ public class H2Function { public static long unixTimestamp(java.sql.Timestamp timestamp) { return timestamp.getTime() / 1000L; } } ================================================ FILE: apollo-common/src/main/java/com/ctrip/framework/apollo/common/jpa/SqlFunctionsMetadataBuilderContributor.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.common.jpa; import org.hibernate.boot.MetadataBuilder; import org.hibernate.boot.spi.MetadataBuilderContributor; import org.hibernate.dialect.function.StandardSQLFunction; import org.hibernate.type.StandardBasicTypes; /** * @author nisiyong */ public class SqlFunctionsMetadataBuilderContributor implements MetadataBuilderContributor { @Override public void contribute(MetadataBuilder metadataBuilder) { metadataBuilder.applySqlFunction("NOW", new StandardSQLFunction("NOW", StandardBasicTypes.INTEGER)); } } ================================================ FILE: apollo-common/src/main/java/com/ctrip/framework/apollo/common/utils/BeanUtils.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.common.utils; import com.ctrip.framework.apollo.common.exception.BeanUtilsException; import org.springframework.beans.BeanWrapper; import org.springframework.beans.BeanWrapperImpl; import org.springframework.util.CollectionUtils; import java.beans.PropertyDescriptor; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; public class BeanUtils { /** *
   *     List userBeans = userDao.queryUsers();
   *     List userDTOs = BeanUtil.batchTransform(UserDTO.class, userBeans);
   * 
*/ public static List batchTransform(final Class clazz, List srcList) { if (CollectionUtils.isEmpty(srcList)) { return Collections.emptyList(); } List result = new ArrayList<>(srcList.size()); for (Object srcObject : srcList) { result.add(transform(clazz, srcObject)); } return result; } /** * 封装{@link org.springframework.beans.BeanUtils#copyProperties},惯用与直接将转换结果返回 * *
   *      UserBean userBean = new UserBean("username");
   *      return BeanUtil.transform(UserDTO.class, userBean);
   * 
*/ public static T transform(Class clazz, Object src) { if (src == null) { return null; } T instance; try { instance = clazz.newInstance(); } catch (Exception e) { throw new BeanUtilsException(e); } org.springframework.beans.BeanUtils.copyProperties(src, instance, getNullPropertyNames(src)); return instance; } private static String[] getNullPropertyNames(Object source) { final BeanWrapper src = new BeanWrapperImpl(source); PropertyDescriptor[] pds = src.getPropertyDescriptors(); Set emptyNames = new HashSet<>(); for (PropertyDescriptor pd : pds) { Object srcValue = src.getPropertyValue(pd.getName()); if (srcValue == null) { emptyNames.add(pd.getName()); } } String[] result = new String[emptyNames.size()]; return emptyNames.toArray(result); } /** * 用于将一个列表转换为列表中的对象的某个属性映射到列表中的对象 * *
   *      List userList = userService.queryUsers();
   *      Map userIdToUser = BeanUtil.mapByKey("userId", userList);
   * 
* * @param key 属性名 */ @SuppressWarnings("unchecked") public static Map mapByKey(String key, List list) { Map map = new HashMap<>(); if (CollectionUtils.isEmpty(list)) { return map; } try { Class clazz = list.get(0).getClass(); Field field = deepFindField(clazz, key); if (field == null) { throw new IllegalArgumentException("Could not find the key"); } field.setAccessible(true); for (Object o : list) { map.put((K) field.get(o), (V) o); } } catch (Exception e) { throw new BeanUtilsException(e); } return map; } /** * 根据列表里面的属性聚合 * *
   *       List shopList = shopService.queryShops();
   *       Map> city2Shops = BeanUtil.aggByKeyToList("cityId", shopList);
   * 
*/ @SuppressWarnings("unchecked") public static Map> aggByKeyToList(String key, List list) { Map> map = new HashMap<>(); if (CollectionUtils.isEmpty(list)) {// 防止外面传入空list return map; } try { Class clazz = list.get(0).getClass(); Field field = deepFindField(clazz, key); if (field == null) { throw new IllegalArgumentException("Could not find the key"); } field.setAccessible(true); for (Object o : list) { K k = (K) field.get(o); map.computeIfAbsent(k, k1 -> new ArrayList<>()); map.get(k).add((V) o); } } catch (Exception e) { throw new BeanUtilsException(e); } return map; } /** * 用于将一个对象的列表转换为列表中对象的属性集合 * *
   *     List userList = userService.queryUsers();
   *     Set userIds = BeanUtil.toPropertySet("userId", userList);
   * 
*/ @SuppressWarnings("unchecked") public static Set toPropertySet(String key, List list) { Set set = new LinkedHashSet<>(); if (CollectionUtils.isEmpty(list)) {// 防止外面传入空list return set; } try { Class clazz = list.get(0).getClass(); Field field = deepFindField(clazz, key); if (field == null) { throw new IllegalArgumentException("Could not find the key"); } field.setAccessible(true); for (Object o : list) { set.add((K) field.get(o)); } } catch (Exception e) { throw new BeanUtilsException(e); } return set; } private static Field deepFindField(Class clazz, String key) { Field field = null; while (!clazz.getName().equals(Object.class.getName())) { try { field = clazz.getDeclaredField(key); if (field != null) { break; } } catch (Exception e) { clazz = clazz.getSuperclass(); } } return field; } /** * 获取某个对象的某个属性 */ public static Object getProperty(Object obj, String fieldName) { try { Field field = deepFindField(obj.getClass(), fieldName); if (field != null) { field.setAccessible(true); return field.get(obj); } } catch (Exception e) { throw new BeanUtilsException(e); } return null; } /** * 设置某个对象的某个属性 */ public static void setProperty(Object obj, String fieldName, Object value) { try { Field field = deepFindField(obj.getClass(), fieldName); if (field != null) { field.setAccessible(true); field.set(obj, value); } } catch (Exception e) { throw new BeanUtilsException(e); } } /** * * @param source * @param target */ public static void copyProperties(Object source, Object target, String... ignoreProperties) { org.springframework.beans.BeanUtils.copyProperties(source, target, ignoreProperties); } /** * The copy will ignore BaseEntity field * * @param source * @param target */ public static void copyEntityProperties(Object source, Object target) { org.springframework.beans.BeanUtils.copyProperties(source, target, COPY_IGNORED_PROPERTIES); } private static final String[] COPY_IGNORED_PROPERTIES = {"id", "dataChangeCreatedBy", "dataChangeCreatedTime", "dataChangeLastModifiedTime"}; } ================================================ FILE: apollo-common/src/main/java/com/ctrip/framework/apollo/common/utils/ExceptionUtils.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.common.utils; import com.google.common.base.MoreObjects; import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; import org.springframework.web.client.HttpStatusCodeException; import java.lang.reflect.Type; import java.util.Map; public final class ExceptionUtils { private static Gson gson = new Gson(); private static Type mapType = new TypeToken>() {}.getType(); public static String toString(HttpStatusCodeException e) { Map errorAttributes = gson.fromJson(e.getResponseBodyAsString(), mapType); if (errorAttributes != null) { return MoreObjects.toStringHelper(HttpStatusCodeException.class).omitNullValues() .add("status", errorAttributes.get("status")) .add("message", errorAttributes.get("message")) .add("timestamp", errorAttributes.get("timestamp")) .add("exception", errorAttributes.get("exception")) .add("errorCode", errorAttributes.get("errorCode")) .add("stackTrace", errorAttributes.get("stackTrace")).toString(); } return ""; } } ================================================ FILE: apollo-common/src/main/java/com/ctrip/framework/apollo/common/utils/GrayReleaseRuleItemTransformer.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.common.utils; import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; import com.ctrip.framework.apollo.common.dto.GrayReleaseRuleItemDTO; import java.lang.reflect.Type; import java.util.Set; /** * @author Jason Song(song_s@ctrip.com) */ public class GrayReleaseRuleItemTransformer { private static final Gson gson = new Gson(); private static final Type grayReleaseRuleItemsType = new TypeToken>() {}.getType(); public static Set batchTransformFromJSON(String content) { return gson.fromJson(content, grayReleaseRuleItemsType); } public static String batchTransformToJSON(Set ruleItems) { return gson.toJson(ruleItems); } } ================================================ FILE: apollo-common/src/main/java/com/ctrip/framework/apollo/common/utils/InputValidator.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.common.utils; import com.ctrip.framework.apollo.core.utils.StringUtils; import java.util.regex.Pattern; /** * @author Jason Song(song_s@ctrip.com) */ public class InputValidator { public static final String INVALID_CLUSTER_NAMESPACE_MESSAGE = "Only digits, alphabets and symbol - _ . (except single .) are allowed"; public static final String INVALID_NAMESPACE_NAMESPACE_MESSAGE = "not allowed to end with .json, .yml, .yaml, .xml, .properties"; public static final String CLUSTER_NAMESPACE_VALIDATOR = "[0-9a-zA-Z_-]+[0-9a-zA-Z_.-]*"; private static final String APP_NAMESPACE_VALIDATOR = "[a-zA-Z0-9_-]+[a-zA-Z0-9._-]*(? response = SearchResponseEntity.ok(body); assertEquals(HttpStatus.OK.value(), response.getCode()); assertEquals(HttpStatus.OK.getReasonPhrase(), response.getMessage()); assertEquals(body, response.getBody()); assertFalse(response.isHasMoreData()); } @Test public void testOkWithMessage_WithValidBodyAndMessage_ShouldReturnOkResponseWithMessage() { String body = "test body"; String message = "test message"; SearchResponseEntity response = SearchResponseEntity.okWithMessage(body, message); assertEquals(HttpStatus.OK.value(), response.getCode()); assertEquals(message, response.getMessage()); assertEquals(body, response.getBody()); assertTrue(response.isHasMoreData()); } @Test public void testError_WithValidCodeAndMessage_ShouldReturnErrorResponse() { HttpStatus httpCode = HttpStatus.BAD_REQUEST; String message = "error message"; SearchResponseEntity response = SearchResponseEntity.error(httpCode, message); assertEquals(httpCode.value(), response.getCode()); assertEquals(message, response.getMessage()); assertEquals(null, response.getBody()); assertFalse(response.isHasMoreData()); } } ================================================ FILE: apollo-common/src/test/java/com/ctrip/framework/apollo/common/utils/BeanUtilsTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.common.utils; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import java.util.ArrayList; import java.util.List; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; import org.mockito.junit.MockitoJUnitRunner; import com.ctrip.framework.apollo.common.exception.BeanUtilsException; @RunWith(MockitoJUnitRunner.class) public class BeanUtilsTest { @InjectMocks private BeanUtils beanUtils; List someList; List someAnotherList; @Before public void setUp() { someList = new ArrayList<>(); someAnotherList = new ArrayList<>(); } @Test public void testBatchTransformListNotEmpty() { someList.add(77); assertNotNull(BeanUtils.batchTransform(String.class, someList)); } @Test public void testBatchTransformListIsEmpty() { assertNotNull(BeanUtils.batchTransform(String.class, someList)); } @Test(expected = BeanUtilsException.class) public void testBatchTransformBeanUtilsException() { someList.add(77); assertNotNull(BeanUtils.batchTransform(null, someList)); } @Test public void testBatchTransformSrcIsNull() { someList.add(null); assertNotNull(BeanUtils.batchTransform(String.class, someList)); } @Test public void testMapByKeyEmptyList() { assertNotNull(BeanUtils.mapByKey(null, someList)); } class KeyClass { String keys; } @Test public void testMapByKeyNotEmptyList() { someAnotherList.add(new KeyClass()); assertNotNull(BeanUtils.mapByKey("keys", someAnotherList)); } @Test(expected = BeanUtilsException.class) public void testMapByKeyNotEmptyListThrowsEx() { someAnotherList.add(new KeyClass()); assertNotNull(BeanUtils.mapByKey("wrongKey", someAnotherList)); } @Test public void testAggByKeyToListEmpty() { assertNotNull(BeanUtils.aggByKeyToList("keys", someAnotherList)); } @Test public void testAggByKeyToListNotEmpty() { someAnotherList.add(new KeyClass()); assertNotNull(BeanUtils.aggByKeyToList("keys", someAnotherList)); } @Test(expected = BeanUtilsException.class) public void testAggByKeyToListNotEmptyThrowsEx() { someAnotherList.add(new KeyClass()); assertNotNull(BeanUtils.aggByKeyToList("wrongKey", someAnotherList)); } @Test public void testToPropertySetEmpty() { assertNotNull(BeanUtils.toPropertySet("keys", someAnotherList)); } @Test public void testToPropertySetNotEmpty() { someAnotherList.add(new KeyClass()); assertNotNull(BeanUtils.toPropertySet("keys", someAnotherList)); } @Test(expected = BeanUtilsException.class) public void testToPropertySetNotEmptyThrowsEx() { someAnotherList.add(new KeyClass()); assertNotNull(BeanUtils.toPropertySet("wrongKey", someAnotherList)); } @Test public void testGetAndsetProperty() { BeanUtils.setProperty(new KeyClass(), "keys", "value"); assertNull(BeanUtils.getProperty(new KeyClass(), "keys")); } } ================================================ FILE: apollo-common/src/test/java/com/ctrip/framework/apollo/common/utils/InputValidatorTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.common.utils; import static org.junit.Assert.*; import org.junit.Test; public class InputValidatorTest { @Test public void testValidClusterName() throws Exception { checkClusterName("some.cluster-_name.123", true); checkClusterName("some.cluster-_name.123.yml", true); checkClusterName("some.&.name", false); checkClusterName("", false); checkClusterName(null, false); checkClusterName(".", false); } @Test public void testValidAppNamespaceName() throws Exception { checkAppNamespaceName("some.cluster-_name.123", true); checkAppNamespaceName("some.&.name", false); checkAppNamespaceName("", false); checkAppNamespaceName(null, false); checkAppNamespaceName("some.name.json", false); checkAppNamespaceName("some.name.yml", false); checkAppNamespaceName("some.name.yaml", false); checkAppNamespaceName("some.name.xml", false); checkAppNamespaceName("some.name.properties", false); checkAppNamespaceName("..xml", false); } private void checkClusterName(String name, boolean valid) { assertEquals(valid, InputValidator.isValidClusterNamespace(name)); } private void checkAppNamespaceName(String name, boolean valid) { assertEquals(valid, InputValidator.isValidAppNamespace(name)); } } ================================================ FILE: apollo-common/src/test/java/com/ctrip/framework/apollo/common/utils/WebUtilsTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.common.utils; import static org.assertj.core.api.Assertions.assertThat; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.junit.MockitoJUnitRunner; import org.springframework.mock.web.MockHttpServletRequest; /** * @author kl (http://kailing.pub) * @since 2022/8/12 */ @RunWith(MockitoJUnitRunner.class) public class WebUtilsTest { @Test public void testTryToGetClientIp() { MockHttpServletRequest request = new MockHttpServletRequest(); request.addHeader("X-FORWARDED-FOR", "172.1.1.1,172.1.1.2"); request.setRemoteAddr("172.1.1.3"); String ip = WebUtils.tryToGetClientIp(request); assertThat(ip).isEqualTo("172.1.1.1"); request.removeHeader("X-FORWARDED-FOR"); ip = WebUtils.tryToGetClientIp(request); assertThat(ip).isEqualTo("172.1.1.3"); } } ================================================ FILE: apollo-configservice/pom.xml ================================================ com.ctrip.framework.apollo apollo ${revision} ../pom.xml 4.0.0 apollo-configservice Apollo ConfigService ${project.artifactId} com.ctrip.framework.apollo apollo-biz org.springframework.cloud spring-cloud-starter-netflix-eureka-server spring-cloud-starter-netflix-archaius org.springframework.cloud spring-cloud-starter-netflix-ribbon org.springframework.cloud ribbon-eureka com.netflix.ribbon aws-java-sdk-core com.amazonaws aws-java-sdk-ec2 com.amazonaws aws-java-sdk-autoscaling com.amazonaws aws-java-sdk-sts com.amazonaws aws-java-sdk-route53 com.amazonaws org.springframework.security spring-security-crypto com.sun.jersey.contribs jersey-apache-client4 com.alibaba.nacos nacos-api ${nacos-discovery-api.version} jakarta.xml.bind jakarta.xml.bind-api org.glassfish.jaxb jaxb-runtime jakarta.activation jakarta.activation-api org.javassist javassist org.springframework.boot spring-boot-maven-plugin maven-assembly-plugin package single ${project.artifactId}-${project.version}-${package.environment} false src/assembly/assembly-descriptor.xml com.spotify docker-maven-plugin 1.2.2 apolloconfig/${project.artifactId} ${project.version} latest ${project.basedir}/src/main/docker docker-hub ${project.version} / ${project.build.directory} *.zip nacos-discovery com.alibaba.boot nacos-discovery-spring-boot-starter com.alibaba fastjson ================================================ FILE: apollo-configservice/src/assembly/assembly-descriptor.xml ================================================ apollo-assembly zip false src/main/scripts scripts *.sh 0755 unix target/classes / apollo-configservice.conf unix target/classes /config application-github.properties application.properties target / ${project.artifactId}-*.jar 0755 ================================================ FILE: apollo-configservice/src/main/docker/Dockerfile ================================================ # # Copyright 2024 Apollo Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # Dockerfile for apollo-configservice # 1. ./scripts/build.sh # 2. Build with: mvn docker:build -pl apollo-configservice # 3. Run with: docker run -p 8080:8080 -e SPRING_DATASOURCE_URL="jdbc:mysql://fill-in-the-correct-server:3306/ApolloConfigDB?characterEncoding=utf8" -e SPRING_DATASOURCE_USERNAME=FillInCorrectUser -e SPRING_DATASOURCE_PASSWORD=FillInCorrectPassword -d -v /tmp/logs:/opt/logs --name apollo-configservice apolloconfig/apollo-configservice FROM alpine:3.15.5 ARG VERSION ENV VERSION $VERSION COPY apollo-configservice-${VERSION}-github.zip /apollo-configservice/apollo-configservice-${VERSION}-github.zip RUN unzip /apollo-configservice/apollo-configservice-${VERSION}-github.zip -d /apollo-configservice \ && rm -rf /apollo-configservice/apollo-configservice-${VERSION}-github.zip \ && chmod +x /apollo-configservice/scripts/startup.sh FROM eclipse-temurin:17-jre-jammy LABEL maintainer="g632104866@gmail.com;finchcn@gmail.com;ameizi" ENV APOLLO_RUN_MODE "Docker" ENV SERVER_PORT 8080 RUN DEBIAN_FRONTEND=noninteractive apt-get update \ && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends procps curl bash tzdata \ && rm -rf /var/lib/apt/lists/* \ && ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ && echo "Asia/Shanghai" > /etc/timezone COPY --from=0 /apollo-configservice /apollo-configservice EXPOSE $SERVER_PORT CMD ["/apollo-configservice/scripts/startup.sh"] ================================================ FILE: apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/ConfigServerEurekaServerConfigure.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.configservice; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.core.userdetails.User; import org.springframework.security.provisioning.InMemoryUserDetailsManager; import org.springframework.security.web.SecurityFilterChain; import org.springframework.util.StringUtils; /** * Start Eureka Server annotations according to configuration * * @author Zhiqiang Lin(linzhiqiang0514@163.com) */ @Configuration @EnableEurekaServer @ConditionalOnProperty(name = "apollo.eureka.server.enabled", havingValue = "true", matchIfMissing = true) public class ConfigServerEurekaServerConfigure { @Order(99) @Configuration static class EurekaServerSecurityConfigurer { private static final String EUREKA_ROLE = "EUREKA"; @Value("${apollo.eureka.server.security.enabled:false}") private boolean eurekaSecurityEnabled; @Value("${apollo.eureka.server.security.username:}") private String username; @Value("${apollo.eureka.server.security.password:}") private String password; @Bean @Order(99) public SecurityFilterChain eurekaServerSecurityFilterChain(HttpSecurity http) throws Exception { http.securityMatcher("/eureka/**"); http.csrf(csrf -> csrf.disable()); http.httpBasic(Customizer.withDefaults()); if (eurekaSecurityEnabled) { DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider(); authenticationProvider .setUserDetailsService(new InMemoryUserDetailsManager(User.withUsername(username) .password(toDelegatingPassword(password)).roles(EUREKA_ROLE).build())); http.authenticationProvider(authenticationProvider); http.authorizeHttpRequests( authorizeHttpRequests -> authorizeHttpRequests.requestMatchers("/eureka/apps/**", "/eureka/instances/**", "/eureka/peerreplication/**").hasRole(EUREKA_ROLE) .anyRequest().permitAll()); } return http.build(); } private String toDelegatingPassword(String configuredPassword) { if (!StringUtils.hasText(configuredPassword)) { return "{noop}"; } if (configuredPassword.startsWith("{")) { return configuredPassword; } return "{noop}" + configuredPassword; } } } ================================================ FILE: apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/ConfigServiceApplication.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.configservice; import com.ctrip.framework.apollo.biz.ApolloBizConfig; import com.ctrip.framework.apollo.common.ApolloCommonConfig; import com.ctrip.framework.apollo.metaservice.ApolloMetaServiceConfig; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration; import org.springframework.boot.autoconfigure.session.SessionAutoConfiguration; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.EnableAspectJAutoProxy; import org.springframework.context.annotation.PropertySource; import org.springframework.transaction.annotation.EnableTransactionManagement; /** * Spring boot application entry point * * @author Jason Song(song_s@ctrip.com) */ @EnableAspectJAutoProxy @EnableAutoConfiguration( exclude = {UserDetailsServiceAutoConfiguration.class, SessionAutoConfiguration.class}) @Configuration @EnableTransactionManagement @PropertySource(value = {"classpath:configservice.properties"}) @ComponentScan(basePackageClasses = {ApolloCommonConfig.class, ApolloBizConfig.class, ConfigServiceApplication.class, ApolloMetaServiceConfig.class}) public class ConfigServiceApplication { public static void main(String[] args) throws Exception { SpringApplication.run(ConfigServiceApplication.class, args); } } ================================================ FILE: apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/ConfigServiceAssemblyConfiguration.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.configservice; import com.ctrip.framework.apollo.common.datasource.ApolloDataSourceScriptDatabaseInitializer; import com.ctrip.framework.apollo.common.datasource.ApolloDataSourceScriptDatabaseInitializerFactory; import com.ctrip.framework.apollo.common.datasource.ApolloSqlInitializationProperties; import javax.sql.DataSource; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; @Profile("assembly") @Configuration public class ConfigServiceAssemblyConfiguration { @ConfigurationProperties(prefix = "spring.sql.config-init") @Bean public static ApolloSqlInitializationProperties apolloSqlInitializationProperties() { return new ApolloSqlInitializationProperties(); } @Bean public static ApolloDataSourceScriptDatabaseInitializer apolloDataSourceScriptDatabaseInitializer( DataSource dataSource, ApolloSqlInitializationProperties properties) { return ApolloDataSourceScriptDatabaseInitializerFactory.create(dataSource, properties); } } ================================================ FILE: apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/ConfigServiceAutoConfiguration.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.configservice; import com.ctrip.framework.apollo.biz.config.BizConfig; import com.ctrip.framework.apollo.biz.grayReleaseRule.GrayReleaseRulesHolder; import com.ctrip.framework.apollo.biz.message.ReleaseMessageScanner; import com.ctrip.framework.apollo.biz.repository.GrayReleaseRuleRepository; import com.ctrip.framework.apollo.biz.repository.ReleaseMessageRepository; import com.ctrip.framework.apollo.biz.service.ReleaseMessageService; import com.ctrip.framework.apollo.biz.service.ReleaseService; import com.ctrip.framework.apollo.configservice.controller.ConfigFileController; import com.ctrip.framework.apollo.configservice.controller.NotificationController; import com.ctrip.framework.apollo.configservice.controller.NotificationControllerV2; import com.ctrip.framework.apollo.configservice.filter.ClientAuthenticationFilter; import com.ctrip.framework.apollo.configservice.service.ReleaseMessageServiceWithCache; import com.ctrip.framework.apollo.configservice.service.config.ConfigService; import com.ctrip.framework.apollo.configservice.service.config.ConfigServiceWithCache; import com.ctrip.framework.apollo.configservice.service.config.DefaultConfigService; import com.ctrip.framework.apollo.configservice.service.config.DefaultIncrementalSyncService; import com.ctrip.framework.apollo.configservice.service.config.IncrementalSyncService; import com.ctrip.framework.apollo.configservice.util.AccessKeyUtil; import io.micrometer.core.instrument.MeterRegistry; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.crypto.password.NoOpPasswordEncoder; /** * @author Jason Song(song_s@ctrip.com) */ @Configuration public class ConfigServiceAutoConfiguration { private final BizConfig bizConfig; private final ReleaseService releaseService; private final ReleaseMessageService releaseMessageService; private final GrayReleaseRuleRepository grayReleaseRuleRepository; private final MeterRegistry meterRegistry; public ConfigServiceAutoConfiguration(final BizConfig bizConfig, final ReleaseService releaseService, final ReleaseMessageService releaseMessageService, final GrayReleaseRuleRepository grayReleaseRuleRepository, final MeterRegistry meterRegistry) { this.bizConfig = bizConfig; this.releaseService = releaseService; this.releaseMessageService = releaseMessageService; this.grayReleaseRuleRepository = grayReleaseRuleRepository; this.meterRegistry = meterRegistry; } @Bean public GrayReleaseRulesHolder grayReleaseRulesHolder() { return new GrayReleaseRulesHolder(grayReleaseRuleRepository, bizConfig); } @Bean public ConfigService configService() { // enable local cache if (bizConfig.isConfigServiceCacheEnabled()) { return new ConfigServiceWithCache(releaseService, releaseMessageService, grayReleaseRulesHolder(), bizConfig, meterRegistry); } return new DefaultConfigService(releaseService, grayReleaseRulesHolder()); } @Bean public IncrementalSyncService incrementalSyncService() { return new DefaultIncrementalSyncService(); } @Bean public static NoOpPasswordEncoder passwordEncoder() { return (NoOpPasswordEncoder) NoOpPasswordEncoder.getInstance(); } @Bean public FilterRegistrationBean clientAuthenticationFilter( AccessKeyUtil accessKeyUtil) { FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean<>(); filterRegistrationBean.setFilter(new ClientAuthenticationFilter(bizConfig, accessKeyUtil)); filterRegistrationBean.addUrlPatterns("/configs/*"); filterRegistrationBean.addUrlPatterns("/configfiles/*"); filterRegistrationBean.addUrlPatterns("/notifications/v2/*"); return filterRegistrationBean; } @Bean public ReleaseMessageScanner releaseMessageScanner( final NotificationController notificationController, final ConfigFileController configFileController, final NotificationControllerV2 notificationControllerV2, final GrayReleaseRulesHolder grayReleaseRulesHolder, final ReleaseMessageServiceWithCache releaseMessageServiceWithCache, final ConfigService configService, final ReleaseMessageRepository releaseMessageRepository) { ReleaseMessageScanner releaseMessageScanner = new ReleaseMessageScanner(bizConfig, releaseMessageRepository); // 0. handle release message cache releaseMessageScanner.addMessageListener(releaseMessageServiceWithCache); // 1. handle gray release rule releaseMessageScanner.addMessageListener(grayReleaseRulesHolder); // 2. handle server cache releaseMessageScanner.addMessageListener(configService); releaseMessageScanner.addMessageListener(configFileController); // 3. notify clients releaseMessageScanner.addMessageListener(notificationControllerV2); releaseMessageScanner.addMessageListener(notificationController); return releaseMessageScanner; } } ================================================ FILE: apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/ConfigServiceHealthIndicator.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.configservice; import com.ctrip.framework.apollo.biz.service.AppService; import org.springframework.boot.actuate.health.Health; import org.springframework.boot.actuate.health.HealthIndicator; import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Component; @Component public class ConfigServiceHealthIndicator implements HealthIndicator { private final AppService appService; public ConfigServiceHealthIndicator(final AppService appService) { this.appService = appService; } @Override public Health health() { check(); return Health.up().build(); } private void check() { PageRequest pageable = PageRequest.of(0, 1); appService.findAll(pageable); } } ================================================ FILE: apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/ServletInitializer.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.configservice; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; /** * Entry point for traditional web app * * @author Jason Song(song_s@ctrip.com) */ public class ServletInitializer extends SpringBootServletInitializer { @Override protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { return application.sources(ConfigServiceApplication.class); } } ================================================ FILE: apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/controller/ConfigController.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.configservice.controller; import com.ctrip.framework.apollo.biz.config.BizConfig; import com.ctrip.framework.apollo.biz.entity.Release; import com.ctrip.framework.apollo.common.entity.AppNamespace; import com.ctrip.framework.apollo.common.utils.WebUtils; import com.ctrip.framework.apollo.configservice.service.AppNamespaceServiceWithCache; import com.ctrip.framework.apollo.configservice.service.config.ConfigService; import com.ctrip.framework.apollo.configservice.service.config.IncrementalSyncService; import com.ctrip.framework.apollo.configservice.util.InstanceConfigAuditUtil; import com.ctrip.framework.apollo.configservice.util.NamespaceUtil; import com.ctrip.framework.apollo.core.ConfigConsts; import com.ctrip.framework.apollo.core.dto.ApolloConfig; import com.ctrip.framework.apollo.core.dto.ApolloNotificationMessages; import com.ctrip.framework.apollo.core.dto.ConfigurationChange; import com.ctrip.framework.apollo.core.enums.ConfigSyncType; import com.ctrip.framework.apollo.tracer.Tracer; import com.google.common.base.Strings; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; import java.util.regex.Pattern; import org.springframework.util.CollectionUtils; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Arrays; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.stream.Collectors; /** * @author Jason Song(song_s@ctrip.com) */ @RestController @RequestMapping("/configs") public class ConfigController { private final ConfigService configService; private final IncrementalSyncService incrementalSyncService; private final AppNamespaceServiceWithCache appNamespaceService; private final NamespaceUtil namespaceUtil; private final InstanceConfigAuditUtil instanceConfigAuditUtil; private final Gson gson; private final BizConfig bizConfig; private static final Type configurationTypeReference = new TypeToken>() {}.getType(); public ConfigController(final ConfigService configService, final IncrementalSyncService incrementalSyncService, final AppNamespaceServiceWithCache appNamespaceService, final NamespaceUtil namespaceUtil, final InstanceConfigAuditUtil instanceConfigAuditUtil, final Gson gson, final BizConfig bizConfig) { this.configService = configService; this.incrementalSyncService = incrementalSyncService; this.appNamespaceService = appNamespaceService; this.namespaceUtil = namespaceUtil; this.instanceConfigAuditUtil = instanceConfigAuditUtil; this.gson = gson; this.bizConfig = bizConfig; } @GetMapping(value = "/{appId}/{clusterName}/{namespace:.+}") public ApolloConfig queryConfig(@PathVariable String appId, @PathVariable String clusterName, @PathVariable String namespace, @RequestParam(value = "dataCenter", required = false) String dataCenter, @RequestParam(value = "releaseKey", defaultValue = "-1") String clientSideReleaseKey, @RequestParam(value = "ip", required = false) String clientIp, @RequestParam(value = "label", required = false) String clientLabel, @RequestParam(value = "messages", required = false) String messagesAsString, HttpServletRequest request, HttpServletResponse response) throws IOException { String originalNamespace = namespace; // strip out .properties suffix namespace = namespaceUtil.filterNamespaceName(namespace); // fix the character case issue, such as FX.apollo <-> fx.apollo namespace = namespaceUtil.normalizeNamespace(appId, namespace); if (Strings.isNullOrEmpty(clientIp)) { clientIp = WebUtils.tryToGetClientIp(request); } ApolloNotificationMessages clientMessages = transformMessages(messagesAsString); List releases = Lists.newLinkedList(); String appClusterNameLoaded = clusterName; if (!ConfigConsts.NO_APPID_PLACEHOLDER.equalsIgnoreCase(appId)) { Release currentAppRelease = configService.loadConfig(appId, clientIp, clientLabel, appId, clusterName, namespace, dataCenter, clientMessages); if (currentAppRelease != null) { releases.add(currentAppRelease); // we have cluster search process, so the cluster name might be overridden appClusterNameLoaded = currentAppRelease.getClusterName(); } } // if namespace does not belong to this appId, should check if there is a public configuration if (!namespaceBelongsToAppId(appId, namespace)) { Release publicRelease = this.findPublicConfig(appId, clientIp, clientLabel, clusterName, namespace, dataCenter, clientMessages); if (Objects.nonNull(publicRelease)) { releases.add(publicRelease); } } if (releases.isEmpty()) { response.sendError(HttpServletResponse.SC_NOT_FOUND, String.format( "Could not load configurations with appId: %s, clusterName: %s, namespace: %s", appId, clusterName, originalNamespace)); Tracer.logEvent("Apollo.Config.NotFound", assembleKey(appId, clusterName, originalNamespace, dataCenter)); return null; } auditReleases(appId, clusterName, dataCenter, clientIp, releases); String latestMergedReleaseKey = releases.stream().map(Release::getReleaseKey) .collect(Collectors.joining(ConfigConsts.CLUSTER_NAMESPACE_SEPARATOR)); if (latestMergedReleaseKey.equals(clientSideReleaseKey)) { // Client side configuration is the same with server side, return 304 response.setStatus(HttpServletResponse.SC_NOT_MODIFIED); Tracer.logEvent("Apollo.Config.NotModified", assembleKey(appId, appClusterNameLoaded, originalNamespace, dataCenter)); return null; } ApolloConfig apolloConfig = new ApolloConfig(appId, appClusterNameLoaded, originalNamespace, latestMergedReleaseKey); Map latestConfigurations = mergeReleaseConfigurations(releases); try { if (bizConfig.isConfigServiceIncrementalChangeEnabled()) { LinkedHashSet clientSideReleaseKeys = Sets.newLinkedHashSet(Arrays .stream( clientSideReleaseKey.split(Pattern.quote(ConfigConsts.CLUSTER_NAMESPACE_SEPARATOR))) .collect(Collectors.toList())); Map clientSideReleases = configService.findReleasesByReleaseKeys(clientSideReleaseKeys); // find history releases if (!CollectionUtils.isEmpty(clientSideReleases)) { // order by clientSideReleaseKeys List historyReleasesWithOrder = new ArrayList<>(); for (String item : clientSideReleaseKeys) { Release release = clientSideReleases.get(item); if (release != null) { historyReleasesWithOrder.add(release); } } Map clientSideConfigurations = mergeReleaseConfigurations(historyReleasesWithOrder); if (!CollectionUtils.isEmpty(clientSideConfigurations)) { List configurationChanges = incrementalSyncService.getConfigurationChanges(latestMergedReleaseKey, latestConfigurations, clientSideReleaseKey, clientSideConfigurations); apolloConfig.setConfigurationChanges(configurationChanges); apolloConfig.setConfigSyncType(ConfigSyncType.INCREMENTAL_SYNC.getValue()); Tracer.logEvent("Apollo.Config.Found", assembleKey(appId, appClusterNameLoaded, originalNamespace, dataCenter)); return apolloConfig; } } } } catch (Exception e) { // fallback to full sync Tracer.logError("Failed to do incremental sync, fallback to full sync", e); } apolloConfig.setConfigurations(latestConfigurations); Tracer.logEvent("Apollo.Config.Found", assembleKey(appId, appClusterNameLoaded, originalNamespace, dataCenter)); return apolloConfig; } private boolean namespaceBelongsToAppId(String appId, String namespaceName) { // Every app has an 'application' namespace if (Objects.equals(ConfigConsts.NAMESPACE_APPLICATION, namespaceName)) { return true; } // if no appId is present, then no other namespace belongs to it if (ConfigConsts.NO_APPID_PLACEHOLDER.equalsIgnoreCase(appId)) { return false; } AppNamespace appNamespace = appNamespaceService.findByAppIdAndNamespace(appId, namespaceName); return appNamespace != null; } /** * @param clientAppId the application which uses public config * @param namespace the namespace * @param dataCenter the datacenter */ private Release findPublicConfig(String clientAppId, String clientIp, String clientLabel, String clusterName, String namespace, String dataCenter, ApolloNotificationMessages clientMessages) { AppNamespace appNamespace = appNamespaceService.findPublicNamespaceByName(namespace); // check whether the namespace's appId equals to current one if (Objects.isNull(appNamespace) || Objects.equals(clientAppId, appNamespace.getAppId())) { return null; } String publicConfigAppId = appNamespace.getAppId(); return configService.loadConfig(clientAppId, clientIp, clientLabel, publicConfigAppId, clusterName, namespace, dataCenter, clientMessages); } /** * Merge configurations of releases. * Release in lower index override those in higher index */ Map mergeReleaseConfigurations(List releases) { Map result = Maps.newLinkedHashMap(); for (Release release : Lists.reverse(releases)) { result.putAll(gson.fromJson(release.getConfigurations(), configurationTypeReference)); } return result; } private String assembleKey(String appId, String cluster, String namespace, String dataCenter) { List keyParts = Lists.newArrayList(appId, cluster, namespace); if (!Strings.isNullOrEmpty(dataCenter)) { keyParts.add(dataCenter); } return String.join(ConfigConsts.CLUSTER_NAMESPACE_SEPARATOR, keyParts); } private void auditReleases(String appId, String cluster, String dataCenter, String clientIp, List releases) { if (Strings.isNullOrEmpty(clientIp)) { // no need to audit instance config when there is no ip return; } for (Release release : releases) { instanceConfigAuditUtil.audit(appId, cluster, dataCenter, clientIp, release.getAppId(), release.getClusterName(), release.getNamespaceName(), release.getReleaseKey()); } } ApolloNotificationMessages transformMessages(String messagesAsString) { ApolloNotificationMessages notificationMessages = null; if (!Strings.isNullOrEmpty(messagesAsString)) { try { notificationMessages = gson.fromJson(messagesAsString, ApolloNotificationMessages.class); } catch (Throwable ex) { Tracer.logError(ex); } } return notificationMessages; } } ================================================ FILE: apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/controller/ConfigFileController.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.configservice.controller; import com.ctrip.framework.apollo.biz.entity.ReleaseMessage; import com.ctrip.framework.apollo.biz.grayReleaseRule.GrayReleaseRulesHolder; import com.ctrip.framework.apollo.biz.message.ReleaseMessageListener; import com.ctrip.framework.apollo.biz.message.Topics; import com.ctrip.framework.apollo.common.utils.WebUtils; import com.ctrip.framework.apollo.configservice.util.NamespaceUtil; import com.ctrip.framework.apollo.configservice.util.WatchKeysUtil; import com.ctrip.framework.apollo.core.ConfigConsts; import com.ctrip.framework.apollo.core.dto.ApolloConfig; import com.ctrip.framework.apollo.core.enums.ConfigFileFormat; import com.ctrip.framework.apollo.core.utils.PropertiesUtil; import com.ctrip.framework.apollo.tracer.Tracer; import com.google.common.base.Joiner; import com.google.common.base.Strings; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import com.google.common.cache.Weigher; import com.google.common.collect.HashMultimap; import com.google.common.collect.Lists; import com.google.common.collect.Multimap; import com.google.common.collect.Multimaps; import com.google.gson.Gson; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.Properties; import java.util.Set; import java.util.concurrent.TimeUnit; /** * @author Jason Song(song_s@ctrip.com) */ @RestController @RequestMapping("/configfiles") public class ConfigFileController implements ReleaseMessageListener { private static final Logger logger = LoggerFactory.getLogger(ConfigFileController.class); private static final Joiner STRING_JOINER = Joiner.on(ConfigConsts.CLUSTER_NAMESPACE_SEPARATOR); private static final long MAX_CACHE_SIZE = 50 * 1024 * 1024; // 50MB private static final long EXPIRE_AFTER_WRITE = 30; private final HttpHeaders plainTextResponseHeaders; private final HttpHeaders jsonResponseHeaders; private final HttpHeaders yamlResponseHeaders; private final HttpHeaders xmlResponseHeaders; private final ResponseEntity NOT_FOUND_RESPONSE; private Cache localCache; private final Multimap watchedKeys2CacheKey = Multimaps.synchronizedSetMultimap(HashMultimap.create()); private final Multimap cacheKey2WatchedKeys = Multimaps.synchronizedSetMultimap(HashMultimap.create()); private static final Gson GSON = new Gson(); private final ConfigController configController; private final NamespaceUtil namespaceUtil; private final WatchKeysUtil watchKeysUtil; private final GrayReleaseRulesHolder grayReleaseRulesHolder; public ConfigFileController(final ConfigController configController, final NamespaceUtil namespaceUtil, final WatchKeysUtil watchKeysUtil, final GrayReleaseRulesHolder grayReleaseRulesHolder) { localCache = CacheBuilder.newBuilder().expireAfterWrite(EXPIRE_AFTER_WRITE, TimeUnit.MINUTES) .weigher((Weigher) (key, value) -> value == null ? 0 : value.length()) .maximumWeight(MAX_CACHE_SIZE).removalListener(notification -> { String cacheKey = notification.getKey(); logger.debug("removing cache key: {}", cacheKey); if (!cacheKey2WatchedKeys.containsKey(cacheKey)) { return; } // create a new list to avoid ConcurrentModificationException List watchedKeys = new ArrayList<>(cacheKey2WatchedKeys.get(cacheKey)); for (String watchedKey : watchedKeys) { watchedKeys2CacheKey.remove(watchedKey, cacheKey); } cacheKey2WatchedKeys.removeAll(cacheKey); logger.debug("removed cache key: {}", cacheKey); }).build(); plainTextResponseHeaders = new HttpHeaders(); plainTextResponseHeaders.add("Content-Type", "text/plain;charset=UTF-8"); jsonResponseHeaders = new HttpHeaders(); jsonResponseHeaders.add("Content-Type", "application/json;charset=UTF-8"); yamlResponseHeaders = new HttpHeaders(); yamlResponseHeaders.add("Content-Type", "application/yaml;charset=UTF-8"); xmlResponseHeaders = new HttpHeaders(); xmlResponseHeaders.add("Content-Type", "application/xml;charset=UTF-8"); NOT_FOUND_RESPONSE = new ResponseEntity<>(HttpStatus.NOT_FOUND); this.configController = configController; this.namespaceUtil = namespaceUtil; this.watchKeysUtil = watchKeysUtil; this.grayReleaseRulesHolder = grayReleaseRulesHolder; } @GetMapping(value = "/{appId}/{clusterName}/{namespace:.+}") public ResponseEntity queryConfigAsProperties(@PathVariable String appId, @PathVariable String clusterName, @PathVariable String namespace, @RequestParam(value = "dataCenter", required = false) String dataCenter, @RequestParam(value = "ip", required = false) String clientIp, @RequestParam(value = "label", required = false) String clientLabel, HttpServletRequest request, HttpServletResponse response) throws IOException { String result = queryConfig(ConfigFileOutputFormat.PROPERTIES, appId, clusterName, namespace, dataCenter, clientIp, clientLabel, request, response); if (result == null) { return NOT_FOUND_RESPONSE; } return new ResponseEntity<>(result, plainTextResponseHeaders, HttpStatus.OK); } @GetMapping(value = "/json/{appId}/{clusterName}/{namespace:.+}") public ResponseEntity queryConfigAsJson(@PathVariable String appId, @PathVariable String clusterName, @PathVariable String namespace, @RequestParam(value = "dataCenter", required = false) String dataCenter, @RequestParam(value = "ip", required = false) String clientIp, @RequestParam(value = "label", required = false) String clientLabel, HttpServletRequest request, HttpServletResponse response) throws IOException { String result = queryConfig(ConfigFileOutputFormat.JSON, appId, clusterName, namespace, dataCenter, clientIp, clientLabel, request, response); if (result == null) { return NOT_FOUND_RESPONSE; } return new ResponseEntity<>(result, jsonResponseHeaders, HttpStatus.OK); } @GetMapping(value = "/raw/{appId}/{clusterName}/{namespace:.+}") public ResponseEntity queryConfigAsRaw(@PathVariable String appId, @PathVariable String clusterName, @PathVariable String namespace, @RequestParam(value = "dataCenter", required = false) String dataCenter, @RequestParam(value = "ip", required = false) String clientIp, @RequestParam(value = "label", required = false) String clientLabel, HttpServletRequest request, HttpServletResponse response) throws IOException { String result = queryConfig(ConfigFileOutputFormat.RAW, appId, clusterName, namespace, dataCenter, clientIp, clientLabel, request, response); if (result == null) { return NOT_FOUND_RESPONSE; } ConfigFileFormat format = determineNamespaceFormat(namespace); HttpHeaders responseHeaders; switch (format) { case JSON: responseHeaders = jsonResponseHeaders; break; case YML: case YAML: responseHeaders = yamlResponseHeaders; break; case XML: responseHeaders = xmlResponseHeaders; break; default: responseHeaders = plainTextResponseHeaders; break; } return new ResponseEntity<>(result, responseHeaders, HttpStatus.OK); } String queryConfig(ConfigFileOutputFormat outputFormat, String appId, String clusterName, String namespace, String dataCenter, String clientIp, String clientLabel, HttpServletRequest request, HttpServletResponse response) throws IOException { // strip out .properties suffix namespace = namespaceUtil.filterNamespaceName(namespace); // fix the character case issue, such as FX.apollo <-> fx.apollo namespace = namespaceUtil.normalizeNamespace(appId, namespace); if (Strings.isNullOrEmpty(clientIp)) { clientIp = WebUtils.tryToGetClientIp(request); } // 1. check whether this client has gray release rules boolean hasGrayReleaseRule = grayReleaseRulesHolder.hasGrayReleaseRule(appId, clientIp, clientLabel, namespace); String cacheKey = assembleCacheKey(outputFormat, appId, clusterName, namespace, dataCenter); // 2. try to load gray release and return if (hasGrayReleaseRule) { Tracer.logEvent("ConfigFile.Cache.GrayRelease", cacheKey); return loadConfig(outputFormat, appId, clusterName, namespace, dataCenter, clientIp, clientLabel, request, response); } // 3. if not gray release, check weather cache exists, if exists, return String result = localCache.getIfPresent(cacheKey); // 4. if not exists, load from ConfigController if (Strings.isNullOrEmpty(result)) { Tracer.logEvent("ConfigFile.Cache.Miss", cacheKey); result = loadConfig(outputFormat, appId, clusterName, namespace, dataCenter, clientIp, clientLabel, request, response); if (result == null) { return null; } // 5. Double check if this client needs to load gray release, if yes, load from db again // This step is mainly to avoid cache pollution if (grayReleaseRulesHolder.hasGrayReleaseRule(appId, clientIp, clientLabel, namespace)) { Tracer.logEvent("ConfigFile.Cache.GrayReleaseConflict", cacheKey); return loadConfig(outputFormat, appId, clusterName, namespace, dataCenter, clientIp, clientLabel, request, response); } localCache.put(cacheKey, result); logger.debug("adding cache for key: {}", cacheKey); Set watchedKeys = watchKeysUtil.assembleAllWatchKeys(appId, clusterName, namespace, dataCenter); for (String watchedKey : watchedKeys) { watchedKeys2CacheKey.put(watchedKey, cacheKey); } cacheKey2WatchedKeys.putAll(cacheKey, watchedKeys); logger.debug("added cache for key: {}", cacheKey); } else { Tracer.logEvent("ConfigFile.Cache.Hit", cacheKey); } return result; } private String loadConfig(ConfigFileOutputFormat outputFormat, String appId, String clusterName, String namespace, String dataCenter, String clientIp, String clientLabel, HttpServletRequest request, HttpServletResponse response) throws IOException { ApolloConfig apolloConfig = configController.queryConfig(appId, clusterName, namespace, dataCenter, "-1", clientIp, clientLabel, null, request, response); if (apolloConfig == null || apolloConfig.getConfigurations() == null) { return null; } String result = null; switch (outputFormat) { case PROPERTIES: Properties properties = new Properties(); properties.putAll(apolloConfig.getConfigurations()); result = PropertiesUtil.toString(properties); break; case JSON: result = GSON.toJson(apolloConfig.getConfigurations()); break; case RAW: result = getRawConfigContent(apolloConfig); break; } return result; } private String getRawConfigContent(ApolloConfig apolloConfig) throws IOException { ConfigFileFormat format = determineNamespaceFormat(apolloConfig.getNamespaceName()); if (format == ConfigFileFormat.Properties) { Properties properties = new Properties(); properties.putAll(apolloConfig.getConfigurations()); return PropertiesUtil.toString(properties); } return apolloConfig.getConfigurations().get("content"); } String assembleCacheKey(ConfigFileOutputFormat outputFormat, String appId, String clusterName, String namespace, String dataCenter) { List keyParts = Lists.newArrayList(outputFormat.getValue(), appId, clusterName, namespace); if (!Strings.isNullOrEmpty(dataCenter)) { keyParts.add(dataCenter); } return STRING_JOINER.join(keyParts); } ConfigFileFormat determineNamespaceFormat(String namespaceName) { String lowerCase = namespaceName.toLowerCase(); for (ConfigFileFormat format : ConfigFileFormat.values()) { if (lowerCase.endsWith("." + format.getValue())) { return format; } } return ConfigFileFormat.Properties; } @Override public void handleMessage(ReleaseMessage message, String channel) { logger.info("message received - channel: {}, message: {}", channel, message); String content = message.getMessage(); if (!Topics.APOLLO_RELEASE_TOPIC.equals(channel) || Strings.isNullOrEmpty(content)) { return; } if (!watchedKeys2CacheKey.containsKey(content)) { return; } // create a new list to avoid ConcurrentModificationException List cacheKeys = new ArrayList<>(watchedKeys2CacheKey.get(content)); for (String cacheKey : cacheKeys) { logger.debug("invalidate cache key: {}", cacheKey); localCache.invalidate(cacheKey); } } enum ConfigFileOutputFormat { PROPERTIES("properties"), JSON("json"), RAW("raw"); private String value; ConfigFileOutputFormat(String value) { this.value = value; } public String getValue() { return value; } } } ================================================ FILE: apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/controller/NotificationController.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.configservice.controller; import com.ctrip.framework.apollo.biz.entity.ReleaseMessage; import com.ctrip.framework.apollo.biz.message.ReleaseMessageListener; import com.ctrip.framework.apollo.biz.message.Topics; import com.ctrip.framework.apollo.biz.utils.EntityManagerUtil; import com.ctrip.framework.apollo.biz.utils.ReleaseMessageKeyGenerator; import com.ctrip.framework.apollo.configservice.service.ReleaseMessageServiceWithCache; import com.ctrip.framework.apollo.configservice.util.NamespaceUtil; import com.ctrip.framework.apollo.configservice.util.WatchKeysUtil; import com.ctrip.framework.apollo.core.ConfigConsts; import com.ctrip.framework.apollo.core.dto.ApolloConfigNotification; import com.ctrip.framework.apollo.tracer.Tracer; import com.google.common.base.Strings; import com.google.common.collect.HashMultimap; import com.google.common.collect.Lists; import com.google.common.collect.Multimap; import com.google.common.collect.Multimaps; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.util.CollectionUtils; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.context.request.async.DeferredResult; import java.util.List; import java.util.Set; /** * @author Jason Song(song_s@ctrip.com) */ @Deprecated @RestController @RequestMapping("/notifications") public class NotificationController implements ReleaseMessageListener { private static final Logger logger = LoggerFactory.getLogger(NotificationController.class); private static final long TIMEOUT = 30 * 1000;// 30 seconds private final Multimap>> deferredResults = Multimaps.synchronizedSetMultimap(HashMultimap.create()); private static final ResponseEntity NOT_MODIFIED_RESPONSE = new ResponseEntity<>(HttpStatus.NOT_MODIFIED); private final WatchKeysUtil watchKeysUtil; private final ReleaseMessageServiceWithCache releaseMessageService; private final EntityManagerUtil entityManagerUtil; private final NamespaceUtil namespaceUtil; public NotificationController(final WatchKeysUtil watchKeysUtil, final ReleaseMessageServiceWithCache releaseMessageService, final EntityManagerUtil entityManagerUtil, final NamespaceUtil namespaceUtil) { this.watchKeysUtil = watchKeysUtil; this.releaseMessageService = releaseMessageService; this.entityManagerUtil = entityManagerUtil; this.namespaceUtil = namespaceUtil; } /** * For single namespace notification, reserved for older version of apollo clients * * @param appId the appId * @param cluster the cluster * @param namespace the namespace name * @param dataCenter the datacenter * @param notificationId the notification id for the namespace * @param clientIp the client side ip * @return a deferred result */ @GetMapping public DeferredResult> pollNotification( @RequestParam(value = "appId") String appId, @RequestParam(value = "cluster") String cluster, @RequestParam(value = "namespace", defaultValue = ConfigConsts.NAMESPACE_APPLICATION) String namespace, @RequestParam(value = "dataCenter", required = false) String dataCenter, @RequestParam(value = "notificationId", defaultValue = "-1") long notificationId, @RequestParam(value = "ip", required = false) String clientIp) { // strip out .properties suffix namespace = namespaceUtil.filterNamespaceName(namespace); Set watchedKeys = watchKeysUtil.assembleAllWatchKeys(appId, cluster, namespace, dataCenter); DeferredResult> deferredResult = new DeferredResult<>(TIMEOUT, NOT_MODIFIED_RESPONSE); // check whether client is out-dated ReleaseMessage latest = releaseMessageService.findLatestReleaseMessageForMessages(watchedKeys); /** * Manually close the entity manager. * Since for async request, Spring won't do so until the request is finished, * which is unacceptable since we are doing long polling - means the db connection would be hold * for a very long time */ entityManagerUtil.closeEntityManager(); if (latest != null && latest.getId() != notificationId) { deferredResult.setResult(new ResponseEntity<>( new ApolloConfigNotification(namespace, latest.getId()), HttpStatus.OK)); } else { // register all keys for (String key : watchedKeys) { this.deferredResults.put(key, deferredResult); } deferredResult.onTimeout(() -> logWatchedKeys(watchedKeys, "Apollo.LongPoll.TimeOutKeys")); deferredResult.onCompletion(() -> { // unregister all keys for (String key : watchedKeys) { deferredResults.remove(key, deferredResult); } logWatchedKeys(watchedKeys, "Apollo.LongPoll.CompletedKeys"); }); logWatchedKeys(watchedKeys, "Apollo.LongPoll.RegisteredKeys"); logger.debug("Listening {} from appId: {}, cluster: {}, namespace: {}, datacenter: {}", watchedKeys, appId, cluster, namespace, dataCenter); } return deferredResult; } @Override public void handleMessage(ReleaseMessage message, String channel) { logger.info("message received - channel: {}, message: {}", channel, message); String content = message.getMessage(); Tracer.logEvent("Apollo.LongPoll.Messages", content); if (!Topics.APOLLO_RELEASE_TOPIC.equals(channel) || Strings.isNullOrEmpty(content)) { return; } List keys = ReleaseMessageKeyGenerator.messageToList(content); if (CollectionUtils.isEmpty(keys)) { return; } ResponseEntity notification = new ResponseEntity<>( new ApolloConfigNotification(keys.get(2), message.getId()), HttpStatus.OK); if (!deferredResults.containsKey(content)) { return; } // create a new list to avoid ConcurrentModificationException List>> results = Lists.newArrayList(deferredResults.get(content)); logger.debug("Notify {} clients for key {}", results.size(), content); for (DeferredResult> result : results) { result.setResult(notification); } logger.debug("Notification completed"); } private void logWatchedKeys(Set watchedKeys, String eventName) { for (String watchedKey : watchedKeys) { Tracer.logEvent(eventName, watchedKey); } } } ================================================ FILE: apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/controller/NotificationControllerV2.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.configservice.controller; import com.ctrip.framework.apollo.biz.config.BizConfig; import com.ctrip.framework.apollo.biz.entity.ReleaseMessage; import com.ctrip.framework.apollo.biz.message.ReleaseMessageListener; import com.ctrip.framework.apollo.biz.message.Topics; import com.ctrip.framework.apollo.biz.utils.EntityManagerUtil; import com.ctrip.framework.apollo.biz.utils.ReleaseMessageKeyGenerator; import com.ctrip.framework.apollo.common.exception.BadRequestException; import com.ctrip.framework.apollo.configservice.service.ReleaseMessageServiceWithCache; import com.ctrip.framework.apollo.configservice.util.NamespaceUtil; import com.ctrip.framework.apollo.configservice.util.WatchKeysUtil; import com.ctrip.framework.apollo.configservice.wrapper.CaseInsensitiveMultimapWrapper; import com.ctrip.framework.apollo.configservice.wrapper.DeferredResultWrapper; import com.ctrip.framework.apollo.core.ConfigConsts; import com.ctrip.framework.apollo.core.dto.ApolloConfigNotification; import com.ctrip.framework.apollo.core.utils.ApolloThreadFactory; import com.ctrip.framework.apollo.tracer.Tracer; import com.google.common.base.Strings; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Multimap; import com.google.common.collect.Sets; import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.ResponseEntity; import org.springframework.util.CollectionUtils; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.context.request.async.DeferredResult; import java.lang.reflect.Type; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.function.Function; /** * @author Jason Song(song_s@ctrip.com) */ @RestController @RequestMapping("/notifications/v2") public class NotificationControllerV2 implements ReleaseMessageListener { private static final Logger logger = LoggerFactory.getLogger(NotificationControllerV2.class); private final CaseInsensitiveMultimapWrapper deferredResults = new CaseInsensitiveMultimapWrapper<>(Maps.newConcurrentMap(), Sets::newConcurrentHashSet); private static final Type notificationsTypeReference = new TypeToken>() {}.getType(); private final ExecutorService largeNotificationBatchExecutorService; private final WatchKeysUtil watchKeysUtil; private final ReleaseMessageServiceWithCache releaseMessageService; private final EntityManagerUtil entityManagerUtil; private final NamespaceUtil namespaceUtil; private final Gson gson; private final BizConfig bizConfig; public NotificationControllerV2(final WatchKeysUtil watchKeysUtil, final ReleaseMessageServiceWithCache releaseMessageService, final EntityManagerUtil entityManagerUtil, final NamespaceUtil namespaceUtil, final Gson gson, final BizConfig bizConfig) { largeNotificationBatchExecutorService = Executors .newSingleThreadExecutor(ApolloThreadFactory.create("NotificationControllerV2", true)); this.watchKeysUtil = watchKeysUtil; this.releaseMessageService = releaseMessageService; this.entityManagerUtil = entityManagerUtil; this.namespaceUtil = namespaceUtil; this.gson = gson; this.bizConfig = bizConfig; } @GetMapping public DeferredResult>> pollNotification( @RequestParam(value = "appId") String appId, @RequestParam(value = "cluster") String cluster, @RequestParam(value = "notifications") String notificationsAsString, @RequestParam(value = "dataCenter", required = false) String dataCenter, @RequestParam(value = "ip", required = false) String clientIp) { List notifications = null; try { notifications = gson.fromJson(notificationsAsString, notificationsTypeReference); } catch (Throwable ex) { Tracer.logError(ex); } if (CollectionUtils.isEmpty(notifications)) { throw BadRequestException.invalidNotificationsFormat(notificationsAsString); } Map filteredNotifications = filterNotifications(appId, notifications); if (CollectionUtils.isEmpty(filteredNotifications)) { throw BadRequestException.invalidNotificationsFormat(notificationsAsString); } DeferredResultWrapper deferredResultWrapper = new DeferredResultWrapper(bizConfig.longPollingTimeoutInMilli()); Set namespaces = Sets.newHashSetWithExpectedSize(filteredNotifications.size()); Map clientSideNotifications = Maps.newHashMapWithExpectedSize(filteredNotifications.size()); for (Map.Entry notificationEntry : filteredNotifications .entrySet()) { String normalizedNamespace = notificationEntry.getKey(); ApolloConfigNotification notification = notificationEntry.getValue(); namespaces.add(normalizedNamespace); clientSideNotifications.put(normalizedNamespace, notification.getNotificationId()); if (!Objects.equals(notification.getNamespaceName(), normalizedNamespace)) { deferredResultWrapper.recordNamespaceNameNormalizedResult(notification.getNamespaceName(), normalizedNamespace); } } Multimap watchedKeysMap = watchKeysUtil.assembleAllWatchKeys(appId, cluster, namespaces, dataCenter); Set watchedKeys = Sets.newHashSet(watchedKeysMap.values()); /** * 1、set deferredResult before the check, for avoid more waiting * If the check before setting deferredResult,it may receive a notification the next time * when method handleMessage is executed between check and set deferredResult. */ deferredResultWrapper .onTimeout(() -> logWatchedKeys(watchedKeys, "Apollo.LongPoll.TimeOutKeys")); deferredResultWrapper.onCompletion(() -> { // unregister all keys for (String key : watchedKeys) { deferredResults.remove(key, deferredResultWrapper); } logWatchedKeys(watchedKeys, "Apollo.LongPoll.CompletedKeys"); }); // register all keys for (String key : watchedKeys) { this.deferredResults.put(key, deferredResultWrapper); } logWatchedKeys(watchedKeys, "Apollo.LongPoll.RegisteredKeys"); logger.debug("Listening {} from appId: {}, cluster: {}, namespace: {}, datacenter: {}", watchedKeys, appId, cluster, namespaces, dataCenter); /** * 2、check new release */ List latestReleaseMessages = releaseMessageService.findLatestReleaseMessagesGroupByMessages(watchedKeys); /** * Manually close the entity manager. * Since for async request, Spring won't do so until the request is finished, * which is unacceptable since we are doing long polling - means the db connection would be hold * for a very long time */ entityManagerUtil.closeEntityManager(); List newNotifications = getApolloConfigNotifications(namespaces, clientSideNotifications, watchedKeysMap, latestReleaseMessages); if (!CollectionUtils.isEmpty(newNotifications)) { deferredResultWrapper.setResult(newNotifications); } return deferredResultWrapper.getResult(); } private Map filterNotifications(String appId, List notifications) { Map filteredNotifications = Maps.newHashMap(); for (ApolloConfigNotification notification : notifications) { if (Strings.isNullOrEmpty(notification.getNamespaceName())) { continue; } // strip out .properties suffix String originalNamespace = namespaceUtil.filterNamespaceName(notification.getNamespaceName()); notification.setNamespaceName(originalNamespace); // fix the character case issue, such as FX.apollo <-> fx.apollo String normalizedNamespace = namespaceUtil.normalizeNamespace(appId, originalNamespace); // in case client side namespace name has character case issue and has difference notification // ids // such as FX.apollo = 1 but fx.apollo = 2, we should let FX.apollo have the chance to update // its notification id // which means we should record FX.apollo = 1 here and ignore fx.apollo = 2 if (filteredNotifications.containsKey(normalizedNamespace) && filteredNotifications .get(normalizedNamespace).getNotificationId() < notification.getNotificationId()) { continue; } filteredNotifications.put(normalizedNamespace, notification); } return filteredNotifications; } private List getApolloConfigNotifications(Set namespaces, Map clientSideNotifications, Multimap watchedKeysMap, List latestReleaseMessages) { List newNotifications = Lists.newArrayList(); if (!CollectionUtils.isEmpty(latestReleaseMessages)) { Map latestNotifications = Maps.newHashMap(); for (ReleaseMessage releaseMessage : latestReleaseMessages) { latestNotifications.put(releaseMessage.getMessage(), releaseMessage.getId()); } for (String namespace : namespaces) { long clientSideId = clientSideNotifications.get(namespace); long latestId = ConfigConsts.NOTIFICATION_ID_PLACEHOLDER; Collection namespaceWatchedKeys = watchedKeysMap.get(namespace); for (String namespaceWatchedKey : namespaceWatchedKeys) { long namespaceNotificationId = latestNotifications.getOrDefault(namespaceWatchedKey, ConfigConsts.NOTIFICATION_ID_PLACEHOLDER); if (namespaceNotificationId > latestId) { latestId = namespaceNotificationId; } } if (latestId > clientSideId) { ApolloConfigNotification notification = new ApolloConfigNotification(namespace, latestId); namespaceWatchedKeys.stream().filter(latestNotifications::containsKey) .forEach(namespaceWatchedKey -> notification.addMessage(namespaceWatchedKey, latestNotifications.get(namespaceWatchedKey))); newNotifications.add(notification); } } } return newNotifications; } @Override public void handleMessage(ReleaseMessage message, String channel) { logger.info("message received - channel: {}, message: {}", channel, message); String content = message.getMessage(); Tracer.logEvent("Apollo.LongPoll.Messages", content); if (!Topics.APOLLO_RELEASE_TOPIC.equals(channel) || Strings.isNullOrEmpty(content)) { return; } String changedNamespace = retrieveNamespaceFromReleaseMessage.apply(content); if (Strings.isNullOrEmpty(changedNamespace)) { logger.error("message format invalid - {}", content); return; } if (!deferredResults.containsKey(content)) { return; } // create a new list to avoid ConcurrentModificationException List results = Lists.newArrayList(deferredResults.get(content)); ApolloConfigNotification configNotification = new ApolloConfigNotification(changedNamespace, message.getId()); configNotification.addMessage(content, message.getId()); // do async notification if too many clients if (results.size() > bizConfig.releaseMessageNotificationBatch()) { largeNotificationBatchExecutorService.submit(() -> { logger.debug("Async notify {} clients for key {} with batch {}", results.size(), content, bizConfig.releaseMessageNotificationBatch()); for (int i = 0; i < results.size(); i++) { if (i > 0 && i % bizConfig.releaseMessageNotificationBatch() == 0) { try { TimeUnit.MILLISECONDS .sleep(bizConfig.releaseMessageNotificationBatchIntervalInMilli()); } catch (InterruptedException e) { // ignore } } logger.debug("Async notify {}", results.get(i)); results.get(i).setResult(configNotification); } }); return; } logger.debug("Notify {} clients for key {}", results.size(), content); for (DeferredResultWrapper result : results) { result.setResult(configNotification); } logger.debug("Notification completed"); } private static final Function retrieveNamespaceFromReleaseMessage = releaseMessage -> { if (Strings.isNullOrEmpty(releaseMessage)) { return null; } List keys = ReleaseMessageKeyGenerator.messageToList(releaseMessage); if (CollectionUtils.isEmpty(keys)) { return null; } return keys.get(2); }; private void logWatchedKeys(Set watchedKeys, String eventName) { for (String watchedKey : watchedKeys) { Tracer.logEvent(eventName, watchedKey); } } } ================================================ FILE: apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/filter/ClientAuthenticationFilter.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.configservice.filter; import com.ctrip.framework.apollo.biz.config.BizConfig; import com.ctrip.framework.apollo.common.utils.WebUtils; import com.ctrip.framework.apollo.configservice.util.AccessKeyUtil; import com.ctrip.framework.apollo.core.signature.Signature; import com.ctrip.framework.apollo.core.utils.StringUtils; import com.ctrip.framework.apollo.tracer.Tracer; import com.google.common.net.HttpHeaders; import java.io.IOException; import java.util.List; import java.util.Objects; import jakarta.servlet.Filter; import jakarta.servlet.FilterChain; import jakarta.servlet.FilterConfig; import jakarta.servlet.ServletException; import jakarta.servlet.ServletRequest; import jakarta.servlet.ServletResponse; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.util.CollectionUtils; /** * @author nisiyong */ public class ClientAuthenticationFilter implements Filter { private static final Logger logger = LoggerFactory.getLogger(ClientAuthenticationFilter.class); private final BizConfig bizConfig; private final AccessKeyUtil accessKeyUtil; public ClientAuthenticationFilter(BizConfig bizConfig, AccessKeyUtil accessKeyUtil) { this.bizConfig = bizConfig; this.accessKeyUtil = accessKeyUtil; } @Override public void init(FilterConfig filterConfig) { // nothing } @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; String appId = accessKeyUtil.extractAppIdFromRequest(request); if (StringUtils.isBlank(appId)) { response.sendError(HttpServletResponse.SC_BAD_REQUEST, "InvalidAppId"); return; } List availableSecrets = accessKeyUtil.findAvailableSecret(appId); if (!CollectionUtils.isEmpty(availableSecrets)) { if (!doCheck(request, response, appId, availableSecrets, false)) { return; } } else { // pre-check for observable secrets List observableSecrets = accessKeyUtil.findObservableSecrets(appId); if (!CollectionUtils.isEmpty(observableSecrets)) { doCheck(request, response, appId, observableSecrets, true); } } chain.doFilter(request, response); } /** * Performs authentication checks(timestamp and signature) for the request. * * @param preCheck Boolean flag indicating whether this is a pre-check * @return true if authentication checks is successful, false otherwise */ private boolean doCheck(HttpServletRequest req, HttpServletResponse resp, String appId, List secrets, boolean preCheck) throws IOException { String timestamp = req.getHeader(Signature.HTTP_HEADER_TIMESTAMP); String authorization = req.getHeader(HttpHeaders.AUTHORIZATION); String ip = WebUtils.tryToGetClientIp(req); // check timestamp, valid within 1 minute if (!checkTimestamp(timestamp)) { if (preCheck) { preCheckInvalidLogging( String.format("Invalid timestamp in pre-check. " + "appId=%s,clientIp=%s,timestamp=%s", appId, ip, timestamp)); } else { logger.warn("Invalid timestamp. appId={},clientIp={},timestamp={}", appId, ip, timestamp); resp.sendError(HttpServletResponse.SC_UNAUTHORIZED, "RequestTimeTooSkewed"); return false; } } // check signature if (!checkAuthorization(authorization, secrets, timestamp, req.getRequestURI(), req.getQueryString())) { if (preCheck) { preCheckInvalidLogging(String.format( "Invalid authorization in pre-check. " + "appId=%s,clientIp=%s,authorization=%s", appId, ip, authorization)); } else { logger.warn("Invalid authorization. appId={},clientIp={},authorization={}", appId, ip, authorization); resp.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized"); return false; } } return true; } @Override public void destroy() { // nothing } private boolean checkTimestamp(String timestamp) { long requestTimeMillis = 0L; try { requestTimeMillis = Long.parseLong(timestamp); } catch (NumberFormatException e) { // nothing to do } long x = System.currentTimeMillis() - requestTimeMillis; long authTimeDiffToleranceInMillis = bizConfig.accessKeyAuthTimeDiffTolerance() * 1000L; return Math.abs(x) < authTimeDiffToleranceInMillis; } private boolean checkAuthorization(String authorization, List availableSecrets, String timestamp, String path, String query) { String signature = null; if (authorization != null) { String[] split = authorization.split(":"); if (split.length > 1) { signature = split[1]; } } for (String secret : availableSecrets) { String availableSignature = accessKeyUtil.buildSignature(path, query, timestamp, secret); if (Objects.equals(signature, availableSignature)) { return true; } } return false; } protected void preCheckInvalidLogging(String message) { logger.warn(message); Tracer.logEvent("Apollo.AccessKey.PreCheck", message); } } ================================================ FILE: apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/service/AccessKeyServiceWithCache.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.configservice.service; import com.ctrip.framework.apollo.biz.config.BizConfig; import com.ctrip.framework.apollo.biz.entity.AccessKey; import com.ctrip.framework.apollo.biz.repository.AccessKeyRepository; import com.ctrip.framework.apollo.common.constants.AccessKeyMode; import com.ctrip.framework.apollo.core.utils.ApolloThreadFactory; import com.ctrip.framework.apollo.tracer.Tracer; import com.ctrip.framework.apollo.tracer.spi.Transaction; import com.google.common.collect.ListMultimap; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.MultimapBuilder.ListMultimapBuilder; import com.google.common.collect.Multimaps; import com.google.common.collect.Sets; import com.google.common.collect.Sets.SetView; import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Set; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.function.Predicate; import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.InitializingBean; import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; /** * @author nisiyong */ @Service public class AccessKeyServiceWithCache implements InitializingBean, DisposableBean { private static Logger logger = LoggerFactory.getLogger(AccessKeyServiceWithCache.class); private final AccessKeyRepository accessKeyRepository; private final BizConfig bizConfig; private int scanInterval; private TimeUnit scanIntervalTimeUnit; private int rebuildInterval; private TimeUnit rebuildIntervalTimeUnit; private ScheduledExecutorService scheduledExecutorService; private Date lastTimeScanned; private ListMultimap accessKeyCache; private ConcurrentMap accessKeyIdCache; public AccessKeyServiceWithCache(final AccessKeyRepository accessKeyRepository, final BizConfig bizConfig) { this.accessKeyRepository = accessKeyRepository; this.bizConfig = bizConfig; initialize(); } private void initialize() { scheduledExecutorService = new ScheduledThreadPoolExecutor(1, ApolloThreadFactory.create("AccessKeyServiceWithCache", true)); lastTimeScanned = new Date(0L); ListMultimap multimap = ListMultimapBuilder.treeKeys(String.CASE_INSENSITIVE_ORDER).arrayListValues().build(); accessKeyCache = Multimaps.synchronizedListMultimap(multimap); accessKeyIdCache = Maps.newConcurrentMap(); } public List getAvailableSecrets(String appId) { return getSecrets(appId, key -> key.isEnabled() && key.getMode() == AccessKeyMode.FILTER); } public List getObservableSecrets(String appId) { return getSecrets(appId, key -> key.isEnabled() && key.getMode() == AccessKeyMode.OBSERVER); } public List getSecrets(String appId, Predicate filter) { List accessKeys = accessKeyCache.get(appId); if (CollectionUtils.isEmpty(accessKeys)) { return Collections.emptyList(); } return accessKeys.stream().filter(filter).map(AccessKey::getSecret) .collect(Collectors.toList()); } @Override public void afterPropertiesSet() throws Exception { populateDataBaseInterval(); scanNewAndUpdatedAccessKeys(); // block the startup process until load finished scheduledExecutorService.scheduleWithFixedDelay(this::scanNewAndUpdatedAccessKeys, scanInterval, scanInterval, scanIntervalTimeUnit); scheduledExecutorService.scheduleAtFixedRate(this::rebuildAccessKeyCache, rebuildInterval, rebuildInterval, rebuildIntervalTimeUnit); } @Override public void destroy() { if (scheduledExecutorService != null) { scheduledExecutorService.shutdownNow(); } } private void scanNewAndUpdatedAccessKeys() { Transaction transaction = Tracer.newTransaction("Apollo.AccessKeyServiceWithCache", "scanNewAndUpdatedAccessKeys"); try { loadNewAndUpdatedAccessKeys(); transaction.setStatus(Transaction.SUCCESS); } catch (Throwable ex) { transaction.setStatus(ex); logger.error("Load new/updated app access keys failed", ex); } finally { transaction.complete(); } } private void rebuildAccessKeyCache() { Transaction transaction = Tracer.newTransaction("Apollo.AccessKeyServiceWithCache", "rebuildCache"); try { deleteAccessKeyCache(); transaction.setStatus(Transaction.SUCCESS); } catch (Throwable ex) { transaction.setStatus(ex); logger.error("Rebuild cache failed", ex); } finally { transaction.complete(); } } private void loadNewAndUpdatedAccessKeys() { boolean hasMore = true; Date currentTime = new Date(); if (!lastTimeScanned.equals(new Date(0L))) { // prevent time drift lastTimeScanned = new Date(lastTimeScanned.getTime() - 1000); } while (hasMore && !Thread.currentThread().isInterrupted()) { // current batch is 500 List accessKeys = accessKeyRepository .findFirst500ByDataChangeLastModifiedTimeGreaterThanEqualAndDataChangeLastModifiedTimeLessThanOrderByDataChangeLastModifiedTimeAsc( lastTimeScanned, currentTime); int scanned = accessKeys.size(); mergeAccessKeys(accessKeys); if (scanned > 0) { logger.info("Loaded {} new/updated Accesskey from startTime {}", scanned, lastTimeScanned); } hasMore = scanned == 500; // In order to avoid missing some records at the last time, we need to scan records at this // time individually if (hasMore) { lastTimeScanned = accessKeys.get(scanned - 1).getDataChangeLastModifiedTime(); List lastModifiedTimeAccessKeys = accessKeyRepository.findByDataChangeLastModifiedTime(lastTimeScanned); mergeAccessKeys(lastModifiedTimeAccessKeys); logger.info("Loaded {} new/updated Accesskey at lastModifiedTime {}", scanned, lastTimeScanned); lastTimeScanned = new Date(lastTimeScanned.getTime() + 1000); } else { lastTimeScanned = currentTime; } } } private void mergeAccessKeys(List accessKeys) { for (AccessKey accessKey : accessKeys) { AccessKey thatInCache = accessKeyIdCache.get(accessKey.getId()); accessKeyIdCache.put(accessKey.getId(), accessKey); accessKeyCache.put(accessKey.getAppId(), accessKey); if (thatInCache != null && accessKey.getDataChangeLastModifiedTime() .compareTo(thatInCache.getDataChangeLastModifiedTime()) >= 0) { accessKeyCache.remove(accessKey.getAppId(), thatInCache); logger.info("Found Accesskey changes, old: {}, new: {}", thatInCache, accessKey); } } } private void deleteAccessKeyCache() { List ids = Lists.newArrayList(accessKeyIdCache.keySet()); if (CollectionUtils.isEmpty(ids)) { return; } List> partitionIds = Lists.partition(ids, 500); for (List toRebuildIds : partitionIds) { Iterable accessKeys = accessKeyRepository.findAllById(toRebuildIds); Set foundIds = Sets.newHashSet(); for (AccessKey accessKey : accessKeys) { foundIds.add(accessKey.getId()); } // handle deleted SetView deletedIds = Sets.difference(Sets.newHashSet(toRebuildIds), foundIds); handleDeletedAccessKeys(deletedIds); } } private void handleDeletedAccessKeys(Set deletedIds) { if (CollectionUtils.isEmpty(deletedIds)) { return; } for (Long deletedId : deletedIds) { AccessKey deleted = accessKeyIdCache.remove(deletedId); if (deleted == null) { continue; } accessKeyCache.remove(deleted.getAppId(), deleted); logger.info("Found AccessKey deleted, {}", deleted); } } private void populateDataBaseInterval() { scanInterval = bizConfig.accessKeyCacheScanInterval(); scanIntervalTimeUnit = bizConfig.accessKeyCacheScanIntervalTimeUnit(); rebuildInterval = bizConfig.accessKeyCacheRebuildInterval(); rebuildIntervalTimeUnit = bizConfig.accessKeyCacheRebuildIntervalTimeUnit(); } } ================================================ FILE: apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/service/AppNamespaceServiceWithCache.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.configservice.service; import com.ctrip.framework.apollo.biz.config.BizConfig; import com.ctrip.framework.apollo.biz.repository.AppNamespaceRepository; import com.ctrip.framework.apollo.common.entity.AppNamespace; import com.ctrip.framework.apollo.configservice.wrapper.CaseInsensitiveMapWrapper; import com.ctrip.framework.apollo.core.ConfigConsts; import com.ctrip.framework.apollo.core.utils.ApolloThreadFactory; import com.ctrip.framework.apollo.core.utils.StringUtils; import com.ctrip.framework.apollo.tracer.Tracer; import com.ctrip.framework.apollo.tracer.spi.Transaction; import com.google.common.base.Joiner; import com.google.common.base.Preconditions; import com.google.common.base.Strings; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.InitializingBean; import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; import java.util.*; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; /** * @author Jason Song(song_s@ctrip.com) */ @Service public class AppNamespaceServiceWithCache implements InitializingBean, DisposableBean { private static final Logger logger = LoggerFactory.getLogger(AppNamespaceServiceWithCache.class); private static final Joiner STRING_JOINER = Joiner.on(ConfigConsts.CLUSTER_NAMESPACE_SEPARATOR).skipNulls(); private final AppNamespaceRepository appNamespaceRepository; private final BizConfig bizConfig; private int scanInterval; private TimeUnit scanIntervalTimeUnit; private int rebuildInterval; private TimeUnit rebuildIntervalTimeUnit; private ScheduledExecutorService scheduledExecutorService; private long maxIdScanned; // store namespaceName -> AppNamespace private CaseInsensitiveMapWrapper publicAppNamespaceCache; // store appId+namespaceName -> AppNamespace private CaseInsensitiveMapWrapper appNamespaceCache; // store id -> AppNamespace private Map appNamespaceIdCache; public AppNamespaceServiceWithCache(final AppNamespaceRepository appNamespaceRepository, final BizConfig bizConfig) { this.appNamespaceRepository = appNamespaceRepository; this.bizConfig = bizConfig; initialize(); } private void initialize() { maxIdScanned = 0; publicAppNamespaceCache = new CaseInsensitiveMapWrapper<>(Maps.newConcurrentMap()); appNamespaceCache = new CaseInsensitiveMapWrapper<>(Maps.newConcurrentMap()); appNamespaceIdCache = Maps.newConcurrentMap(); scheduledExecutorService = Executors.newScheduledThreadPool(1, ApolloThreadFactory.create("AppNamespaceServiceWithCache", true)); } public AppNamespace findByAppIdAndNamespace(String appId, String namespaceName) { Preconditions.checkArgument(!StringUtils.isContainEmpty(appId, namespaceName), "appId and namespaceName must not be empty"); return appNamespaceCache.get(STRING_JOINER.join(appId, namespaceName)); } public List findByAppIdAndNamespaces(String appId, Set namespaceNames) { Preconditions.checkArgument(!Strings.isNullOrEmpty(appId), "appId must not be null"); if (namespaceNames == null || namespaceNames.isEmpty()) { return Collections.emptyList(); } List result = Lists.newArrayList(); for (String namespaceName : namespaceNames) { AppNamespace appNamespace = appNamespaceCache.get(STRING_JOINER.join(appId, namespaceName)); if (appNamespace != null) { result.add(appNamespace); } } return result; } public AppNamespace findPublicNamespaceByName(String namespaceName) { Preconditions.checkArgument(!Strings.isNullOrEmpty(namespaceName), "namespaceName must not be empty"); return publicAppNamespaceCache.get(namespaceName); } public List findPublicNamespacesByNames(Set namespaceNames) { if (namespaceNames == null || namespaceNames.isEmpty()) { return Collections.emptyList(); } List result = Lists.newArrayList(); for (String namespaceName : namespaceNames) { AppNamespace appNamespace = publicAppNamespaceCache.get(namespaceName); if (appNamespace != null) { result.add(appNamespace); } } return result; } @Override public void afterPropertiesSet() throws Exception { populateDataBaseInterval(); scanNewAppNamespaces(); // block the startup process until load finished scheduledExecutorService.scheduleAtFixedRate(() -> { Transaction transaction = Tracer.newTransaction("Apollo.AppNamespaceServiceWithCache", "rebuildCache"); try { this.updateAndDeleteCache(); transaction.setStatus(Transaction.SUCCESS); } catch (Throwable ex) { transaction.setStatus(ex); logger.error("Rebuild cache failed", ex); } finally { transaction.complete(); } }, rebuildInterval, rebuildInterval, rebuildIntervalTimeUnit); scheduledExecutorService.scheduleWithFixedDelay(this::scanNewAppNamespaces, scanInterval, scanInterval, scanIntervalTimeUnit); } @Override public void destroy() { if (scheduledExecutorService != null) { scheduledExecutorService.shutdownNow(); } } private void scanNewAppNamespaces() { Transaction transaction = Tracer.newTransaction("Apollo.AppNamespaceServiceWithCache", "scanNewAppNamespaces"); try { this.loadNewAppNamespaces(); transaction.setStatus(Transaction.SUCCESS); } catch (Throwable ex) { transaction.setStatus(ex); logger.error("Load new app namespaces failed", ex); } finally { transaction.complete(); } } // for those new app namespaces private void loadNewAppNamespaces() { boolean hasMore = true; while (hasMore && !Thread.currentThread().isInterrupted()) { // current batch is 500 List appNamespaces = appNamespaceRepository.findFirst500ByIdGreaterThanOrderByIdAsc(maxIdScanned); if (CollectionUtils.isEmpty(appNamespaces)) { break; } mergeAppNamespaces(appNamespaces); int scanned = appNamespaces.size(); maxIdScanned = appNamespaces.get(scanned - 1).getId(); hasMore = scanned == 500; logger.info("Loaded {} new app namespaces with startId {}", scanned, maxIdScanned); } } private void mergeAppNamespaces(List appNamespaces) { for (AppNamespace appNamespace : appNamespaces) { appNamespaceCache.put(assembleAppNamespaceKey(appNamespace), appNamespace); appNamespaceIdCache.put(appNamespace.getId(), appNamespace); if (appNamespace.isPublic()) { publicAppNamespaceCache.put(appNamespace.getName(), appNamespace); } } } // for those updated or deleted app namespaces private void updateAndDeleteCache() { List ids = appNamespaceIdCache.keySet().stream().sorted().collect(Collectors.toList()); if (CollectionUtils.isEmpty(ids)) { return; } List> partitionIds = Lists.partition(ids, 500); for (List toRebuild : partitionIds) { Iterable appNamespaces = appNamespaceRepository.findAllById(toRebuild); if (appNamespaces == null) { continue; } // handle updated Set foundIds = handleUpdatedAppNamespaces(appNamespaces); // handle deleted handleDeletedAppNamespaces(Sets.difference(Sets.newHashSet(toRebuild), foundIds)); } } // for those updated app namespaces private Set handleUpdatedAppNamespaces(Iterable appNamespaces) { Set foundIds = Sets.newHashSet(); for (AppNamespace appNamespace : appNamespaces) { foundIds.add(appNamespace.getId()); AppNamespace thatInCache = appNamespaceIdCache.get(appNamespace.getId()); if (thatInCache != null && appNamespace.getDataChangeLastModifiedTime() .after(thatInCache.getDataChangeLastModifiedTime())) { appNamespaceIdCache.put(appNamespace.getId(), appNamespace); String oldKey = assembleAppNamespaceKey(thatInCache); String newKey = assembleAppNamespaceKey(appNamespace); appNamespaceCache.put(newKey, appNamespace); // in case appId or namespaceName changes if (!newKey.equals(oldKey)) { appNamespaceCache.remove(oldKey); } if (appNamespace.isPublic()) { publicAppNamespaceCache.put(appNamespace.getName(), appNamespace); // in case namespaceName changes if (!appNamespace.getName().equals(thatInCache.getName()) && thatInCache.isPublic()) { publicAppNamespaceCache.remove(thatInCache.getName()); } } else if (thatInCache.isPublic()) { // just in case isPublic changes publicAppNamespaceCache.remove(thatInCache.getName()); } logger.info("Found AppNamespace changes, old: {}, new: {}", thatInCache, appNamespace); } } return foundIds; } // for those deleted app namespaces private void handleDeletedAppNamespaces(Set deletedIds) { if (CollectionUtils.isEmpty(deletedIds)) { return; } for (Long deletedId : deletedIds) { AppNamespace deleted = appNamespaceIdCache.remove(deletedId); if (deleted == null) { continue; } String appNamespaceKey = assembleAppNamespaceKey(deleted); AppNamespace appNamespace = appNamespaceCache.get(appNamespaceKey); if (appNamespace != null && Objects.equals(appNamespace.getId(), deletedId)) { appNamespaceCache.remove(appNamespaceKey); } if (deleted.isPublic()) { AppNamespace publicAppNamespace = publicAppNamespaceCache.get(deleted.getName()); // in case there is some dirty data, e.g. public namespace deleted in some app and now // created in another app if (publicAppNamespace == deleted) { publicAppNamespaceCache.remove(deleted.getName()); } } logger.info("Found AppNamespace deleted, {}", deleted); } } private String assembleAppNamespaceKey(AppNamespace appNamespace) { return STRING_JOINER.join(appNamespace.getAppId(), appNamespace.getName()); } private void populateDataBaseInterval() { scanInterval = bizConfig.appNamespaceCacheScanInterval(); scanIntervalTimeUnit = bizConfig.appNamespaceCacheScanIntervalTimeUnit(); rebuildInterval = bizConfig.appNamespaceCacheRebuildInterval(); rebuildIntervalTimeUnit = bizConfig.appNamespaceCacheRebuildIntervalTimeUnit(); } // only for test use private void reset() throws Exception { scheduledExecutorService.shutdownNow(); initialize(); afterPropertiesSet(); } } ================================================ FILE: apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/service/ReleaseMessageServiceWithCache.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.configservice.service; import com.ctrip.framework.apollo.biz.config.BizConfig; import com.ctrip.framework.apollo.biz.entity.ReleaseMessage; import com.ctrip.framework.apollo.biz.message.ReleaseMessageListener; import com.ctrip.framework.apollo.biz.message.Topics; import com.ctrip.framework.apollo.biz.repository.ReleaseMessageRepository; import com.ctrip.framework.apollo.core.utils.ApolloThreadFactory; import com.ctrip.framework.apollo.tracer.Tracer; import com.ctrip.framework.apollo.tracer.spi.Transaction; import com.google.common.base.Strings; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.InitializingBean; import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; import java.util.Collections; import java.util.List; import java.util.Set; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; /** * @author Jason Song(song_s@ctrip.com) */ @Service public class ReleaseMessageServiceWithCache implements ReleaseMessageListener, InitializingBean, DisposableBean { private static final Logger logger = LoggerFactory.getLogger(ReleaseMessageServiceWithCache.class); private final ReleaseMessageRepository releaseMessageRepository; private final BizConfig bizConfig; private int scanInterval; private TimeUnit scanIntervalTimeUnit; private volatile long maxIdScanned; private ConcurrentMap releaseMessageCache; private AtomicBoolean doScan; private ExecutorService executorService; public ReleaseMessageServiceWithCache(final ReleaseMessageRepository releaseMessageRepository, final BizConfig bizConfig) { this.releaseMessageRepository = releaseMessageRepository; this.bizConfig = bizConfig; initialize(); } private void initialize() { releaseMessageCache = Maps.newConcurrentMap(); doScan = new AtomicBoolean(true); executorService = Executors.newSingleThreadExecutor( ApolloThreadFactory.create("ReleaseMessageServiceWithCache", true)); } public ReleaseMessage findLatestReleaseMessageForMessages(Set messages) { if (CollectionUtils.isEmpty(messages)) { return null; } long maxReleaseMessageId = 0; ReleaseMessage result = null; for (String message : messages) { ReleaseMessage releaseMessage = releaseMessageCache.get(message); if (releaseMessage != null && releaseMessage.getId() > maxReleaseMessageId) { maxReleaseMessageId = releaseMessage.getId(); result = releaseMessage; } } return result; } public List findLatestReleaseMessagesGroupByMessages(Set messages) { if (CollectionUtils.isEmpty(messages)) { return Collections.emptyList(); } List releaseMessages = Lists.newArrayList(); for (String message : messages) { ReleaseMessage releaseMessage = releaseMessageCache.get(message); if (releaseMessage != null) { releaseMessages.add(releaseMessage); } } return releaseMessages; } @Override public void handleMessage(ReleaseMessage message, String channel) { // Could stop once the ReleaseMessageScanner starts to work doScan.set(false); logger.info("message received - channel: {}, message: {}", channel, message); String content = message.getMessage(); Tracer.logEvent("Apollo.ReleaseMessageService.UpdateCache", String.valueOf(message.getId())); if (!Topics.APOLLO_RELEASE_TOPIC.equals(channel) || Strings.isNullOrEmpty(content)) { return; } long gap = message.getId() - maxIdScanned; if (gap == 1) { mergeReleaseMessage(message); } else if (gap > 1) { // gap found! loadReleaseMessages(maxIdScanned); } } @Override public void afterPropertiesSet() throws Exception { populateDataBaseInterval(); // block the startup process until load finished // this should happen before ReleaseMessageScanner due to autowire loadReleaseMessages(0); executorService.submit(() -> { while (doScan.get() && !Thread.currentThread().isInterrupted()) { Transaction transaction = Tracer.newTransaction("Apollo.ReleaseMessageServiceWithCache", "scanNewReleaseMessages"); try { loadReleaseMessages(maxIdScanned); transaction.setStatus(Transaction.SUCCESS); } catch (Throwable ex) { transaction.setStatus(ex); logger.error("Scan new release messages failed", ex); } finally { transaction.complete(); } try { scanIntervalTimeUnit.sleep(scanInterval); } catch (InterruptedException e) { // ignore } } }); } @Override public void destroy() { doScan.set(false); if (executorService != null) { executorService.shutdownNow(); } } private synchronized void mergeReleaseMessage(ReleaseMessage releaseMessage) { ReleaseMessage old = releaseMessageCache.get(releaseMessage.getMessage()); if (old == null || releaseMessage.getId() > old.getId()) { releaseMessageCache.put(releaseMessage.getMessage(), releaseMessage); maxIdScanned = releaseMessage.getId(); } } private void loadReleaseMessages(long startId) { boolean hasMore = true; while (hasMore && !Thread.currentThread().isInterrupted()) { // current batch is 500 List releaseMessages = releaseMessageRepository.findFirst500ByIdGreaterThanOrderByIdAsc(startId); if (CollectionUtils.isEmpty(releaseMessages)) { break; } releaseMessages.forEach(this::mergeReleaseMessage); int scanned = releaseMessages.size(); startId = releaseMessages.get(scanned - 1).getId(); hasMore = scanned == 500; logger.info("Loaded {} release messages with startId {}", scanned, startId); } } private void populateDataBaseInterval() { scanInterval = bizConfig.releaseMessageCacheScanInterval(); scanIntervalTimeUnit = bizConfig.releaseMessageCacheScanIntervalTimeUnit(); } // only for test use private void reset() throws Exception { executorService.shutdownNow(); initialize(); afterPropertiesSet(); } } ================================================ FILE: apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/service/config/AbstractConfigService.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.configservice.service.config; import com.ctrip.framework.apollo.biz.entity.Release; import com.ctrip.framework.apollo.biz.grayReleaseRule.GrayReleaseRulesHolder; import com.ctrip.framework.apollo.core.ConfigConsts; import com.ctrip.framework.apollo.core.dto.ApolloNotificationMessages; import com.google.common.base.Strings; import java.util.Objects; /** * @author Jason Song(song_s@ctrip.com) */ public abstract class AbstractConfigService implements ConfigService { private final GrayReleaseRulesHolder grayReleaseRulesHolder; protected AbstractConfigService(final GrayReleaseRulesHolder grayReleaseRulesHolder) { this.grayReleaseRulesHolder = grayReleaseRulesHolder; } @Override public Release loadConfig(String clientAppId, String clientIp, String clientLabel, String configAppId, String configClusterName, String configNamespace, String dataCenter, ApolloNotificationMessages clientMessages) { // load from specified cluster first if (!Objects.equals(ConfigConsts.CLUSTER_NAME_DEFAULT, configClusterName)) { Release clusterRelease = findRelease(clientAppId, clientIp, clientLabel, configAppId, configClusterName, configNamespace, clientMessages); if (Objects.nonNull(clusterRelease)) { return clusterRelease; } } // try to load via data center if (!Strings.isNullOrEmpty(dataCenter) && !Objects.equals(dataCenter, configClusterName)) { Release dataCenterRelease = findRelease(clientAppId, clientIp, clientLabel, configAppId, dataCenter, configNamespace, clientMessages); if (Objects.nonNull(dataCenterRelease)) { return dataCenterRelease; } } // fallback to default release return findRelease(clientAppId, clientIp, clientLabel, configAppId, ConfigConsts.CLUSTER_NAME_DEFAULT, configNamespace, clientMessages); } /** * Find release * * @param clientAppId the client's app id * @param clientIp the client ip * @param clientLabel the client label * @param configAppId the requested config's app id * @param configClusterName the requested config's cluster name * @param configNamespace the requested config's namespace name * @param clientMessages the messages received in client side * @return the release */ private Release findRelease(String clientAppId, String clientIp, String clientLabel, String configAppId, String configClusterName, String configNamespace, ApolloNotificationMessages clientMessages) { Long grayReleaseId = grayReleaseRulesHolder.findReleaseIdFromGrayReleaseRule(clientAppId, clientIp, clientLabel, configAppId, configClusterName, configNamespace); Release release = null; if (grayReleaseId != null) { release = findActiveOne(grayReleaseId, clientMessages); } if (release == null) { release = findLatestActiveRelease(configAppId, configClusterName, configNamespace, clientMessages); } return release; } /** * Find active release by id */ protected abstract Release findActiveOne(long id, ApolloNotificationMessages clientMessages); /** * Find active release by app id, cluster name and namespace name */ protected abstract Release findLatestActiveRelease(String configAppId, String configClusterName, String configNamespaceName, ApolloNotificationMessages clientMessages); } ================================================ FILE: apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/service/config/ConfigService.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.configservice.service.config; import com.ctrip.framework.apollo.biz.entity.Release; import com.ctrip.framework.apollo.biz.message.ReleaseMessageListener; import com.ctrip.framework.apollo.core.dto.ApolloNotificationMessages; import java.util.Map; import java.util.Set; /** * @author Jason Song(song_s@ctrip.com) */ public interface ConfigService extends ReleaseMessageListener { /** * Load config * * @param clientAppId the client's app id * @param clientIp the client ip * @param clientLabel the client label * @param configAppId the requested config's app id * @param configClusterName the requested config's cluster name * @param configNamespace the requested config's namespace name * @param dataCenter the client data center * @param clientMessages the messages received in client side * @return the Release */ Release loadConfig(String clientAppId, String clientIp, String clientLabel, String configAppId, String configClusterName, String configNamespace, String dataCenter, ApolloNotificationMessages clientMessages); /** * @param releaseKeys * @return the ReleaseMap */ Map findReleasesByReleaseKeys(Set releaseKeys); } ================================================ FILE: apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/service/config/ConfigServiceWithCache.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.configservice.service.config; import com.ctrip.framework.apollo.biz.grayReleaseRule.GrayReleaseRulesHolder; import com.ctrip.framework.apollo.biz.config.BizConfig; import com.google.common.base.Strings; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; import com.ctrip.framework.apollo.biz.entity.Release; import com.ctrip.framework.apollo.biz.entity.ReleaseMessage; import com.ctrip.framework.apollo.biz.message.Topics; import com.ctrip.framework.apollo.biz.service.ReleaseMessageService; import com.ctrip.framework.apollo.biz.service.ReleaseService; import com.ctrip.framework.apollo.biz.utils.ReleaseMessageKeyGenerator; import com.ctrip.framework.apollo.core.ConfigConsts; import com.ctrip.framework.apollo.core.dto.ApolloNotificationMessages; import com.ctrip.framework.apollo.tracer.Tracer; import com.ctrip.framework.apollo.tracer.spi.Transaction; import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.binder.cache.GuavaCacheMetrics; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Optional; import java.util.Set; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.List; import java.util.concurrent.TimeUnit; import jakarta.annotation.PostConstruct; import org.springframework.util.CollectionUtils; /** * config service with guava cache * * @author Jason Song(song_s@ctrip.com) */ public class ConfigServiceWithCache extends AbstractConfigService { private static final Logger logger = LoggerFactory.getLogger(ConfigServiceWithCache.class); private static final long DEFAULT_EXPIRED_AFTER_ACCESS_IN_MINUTES = 60;// 1 hour private static final String TRACER_EVENT_CACHE_INVALIDATE = "ConfigCache.Invalidate"; private static final String TRACER_EVENT_CACHE_LOAD = "ConfigCache.LoadFromDB"; private static final String TRACER_EVENT_CACHE_LOAD_ID = "ConfigCache.LoadFromDBById"; private static final String TRACER_EVENT_CACHE_GET = "ConfigCache.Get"; private static final String TRACER_EVENT_CACHE_GET_ID = "ConfigCache.GetById"; private static final String TRACER_EVENT_CACHE_LOAD_RELEASE_KEY = "ConfigCache.LoadFromDBByReleaseKey"; private final ReleaseService releaseService; private final ReleaseMessageService releaseMessageService; private final BizConfig bizConfig; private final MeterRegistry meterRegistry; private LoadingCache configCache; private LoadingCache> configIdCache; private LoadingCache> releaseKeyCache; private ConfigCacheEntry nullConfigCacheEntry; public ConfigServiceWithCache(final ReleaseService releaseService, final ReleaseMessageService releaseMessageService, final GrayReleaseRulesHolder grayReleaseRulesHolder, final BizConfig bizConfig, final MeterRegistry meterRegistry) { super(grayReleaseRulesHolder); this.releaseService = releaseService; this.releaseMessageService = releaseMessageService; this.bizConfig = bizConfig; this.meterRegistry = meterRegistry; nullConfigCacheEntry = new ConfigCacheEntry(ConfigConsts.NOTIFICATION_ID_PLACEHOLDER, null); } @PostConstruct void initialize() { buildConfigCache(); buildConfigIdCache(); buildReleaseKeyCache(); } @Override protected Release findActiveOne(long id, ApolloNotificationMessages clientMessages) { Tracer.logEvent(TRACER_EVENT_CACHE_GET_ID, String.valueOf(id)); return configIdCache.getUnchecked(id).orElse(null); } @Override protected Release findLatestActiveRelease(String appId, String clusterName, String namespaceName, ApolloNotificationMessages clientMessages) { String messageKey = ReleaseMessageKeyGenerator.generate(appId, clusterName, namespaceName); String cacheKey = messageKey; if (bizConfig.isConfigServiceCacheKeyIgnoreCase()) { cacheKey = cacheKey.toLowerCase(); } Tracer.logEvent(TRACER_EVENT_CACHE_GET, cacheKey); ConfigCacheEntry cacheEntry = configCache.getUnchecked(cacheKey); // cache is out-dated if (clientMessages != null && clientMessages.has(messageKey) && clientMessages.get(messageKey) > cacheEntry.getNotificationId()) { // invalidate the cache and try to load from db again invalidate(cacheKey); cacheEntry = configCache.getUnchecked(cacheKey); } return cacheEntry.getRelease(); } private void invalidate(String key) { configCache.invalidate(key); Tracer.logEvent(TRACER_EVENT_CACHE_INVALIDATE, key); } @Override public void handleMessage(ReleaseMessage message, String channel) { logger.info("message received - channel: {}, message: {}", channel, message); if (!Topics.APOLLO_RELEASE_TOPIC.equals(channel) || Strings.isNullOrEmpty(message.getMessage())) { return; } try { String messageKey = message.getMessage(); if (bizConfig.isConfigServiceCacheKeyIgnoreCase()) { messageKey = messageKey.toLowerCase(); } invalidate(messageKey); // warm up the cache configCache.getUnchecked(messageKey); } catch (Throwable ex) { // ignore } } @Override public Map findReleasesByReleaseKeys(Set releaseKeys) { try { ImmutableMap> releaseKeyMap = releaseKeyCache.getAll(releaseKeys); if (CollectionUtils.isEmpty(releaseKeyMap)) { return Collections.emptyMap(); } Map validReleaseKeyIdMap = new HashMap<>(); for (Map.Entry> entry : releaseKeyMap.entrySet()) { entry.getValue().ifPresent(id -> validReleaseKeyIdMap.put(entry.getKey(), id)); } if (validReleaseKeyIdMap.isEmpty()) { return Collections.emptyMap(); } Map> releasesMap = configIdCache.getAll(validReleaseKeyIdMap.values()); if (CollectionUtils.isEmpty(releasesMap)) { return Collections.emptyMap(); } Map releases = new HashMap<>(); for (Map.Entry entry : validReleaseKeyIdMap.entrySet()) { Optional releaseOpt = releasesMap.get(entry.getValue()); releaseOpt.ifPresent(release -> releases.put(entry.getKey(), release)); } return releases.isEmpty() ? Collections.emptyMap() : ImmutableMap.copyOf(releases); } catch (Exception e) { Tracer.logError(e); logger.error("Failed to invoke findReleasesByReleaseKeys {}", releaseKeys, e); } return null; } private void buildConfigCache() { CacheBuilder configCacheBuilder = CacheBuilder.newBuilder() .expireAfterAccess(DEFAULT_EXPIRED_AFTER_ACCESS_IN_MINUTES, TimeUnit.MINUTES); if (bizConfig.isConfigServiceCacheStatsEnabled()) { configCacheBuilder.recordStats(); } configCache = configCacheBuilder.build(new CacheLoader() { @Override public ConfigCacheEntry load(String key) throws Exception { List namespaceInfo = ReleaseMessageKeyGenerator.messageToList(key); if (CollectionUtils.isEmpty(namespaceInfo)) { Tracer.logError( new IllegalArgumentException(String.format("Invalid cache load key %s", key))); return nullConfigCacheEntry; } Transaction transaction = Tracer.newTransaction(TRACER_EVENT_CACHE_LOAD, key); try { ReleaseMessage latestReleaseMessage = releaseMessageService.findLatestReleaseMessageForMessages(Lists.newArrayList(key)); Release latestRelease = releaseService.findLatestActiveRelease(namespaceInfo.get(0), namespaceInfo.get(1), namespaceInfo.get(2)); transaction.setStatus(Transaction.SUCCESS); long notificationId = latestReleaseMessage == null ? ConfigConsts.NOTIFICATION_ID_PLACEHOLDER : latestReleaseMessage.getId(); if (notificationId == ConfigConsts.NOTIFICATION_ID_PLACEHOLDER && latestRelease == null) { return nullConfigCacheEntry; } return new ConfigCacheEntry(notificationId, latestRelease); } catch (Throwable ex) { transaction.setStatus(ex); throw ex; } finally { transaction.complete(); } } }); if (bizConfig.isConfigServiceCacheStatsEnabled()) { GuavaCacheMetrics.monitor(meterRegistry, configCache, "config_cache"); } } private void buildReleaseKeyCache() { CacheBuilder releaseKeyCacheBuilder = CacheBuilder.newBuilder() .expireAfterAccess(DEFAULT_EXPIRED_AFTER_ACCESS_IN_MINUTES, TimeUnit.MINUTES); if (bizConfig.isConfigServiceCacheStatsEnabled()) { releaseKeyCacheBuilder.recordStats(); } releaseKeyCache = releaseKeyCacheBuilder.build(new CacheLoader>() { @Override public Optional load(String key) throws Exception { Transaction transaction = Tracer.newTransaction(TRACER_EVENT_CACHE_LOAD_RELEASE_KEY, String.valueOf(key)); try { Release release = releaseService.findByReleaseKey(key); transaction.setStatus(Transaction.SUCCESS); if (release != null) { return Optional.ofNullable(release.getId()); } return Optional.empty(); } catch (Throwable ex) { transaction.setStatus(ex); throw ex; } finally { transaction.complete(); } } }); if (bizConfig.isConfigServiceCacheStatsEnabled()) { GuavaCacheMetrics.monitor(meterRegistry, releaseKeyCache, "releaseKey_cache"); } } private void buildConfigIdCache() { CacheBuilder configIdCacheBuilder = CacheBuilder.newBuilder() .expireAfterAccess(DEFAULT_EXPIRED_AFTER_ACCESS_IN_MINUTES, TimeUnit.MINUTES); if (bizConfig.isConfigServiceCacheStatsEnabled()) { configIdCacheBuilder.recordStats(); } configIdCache = configIdCacheBuilder.build(new CacheLoader>() { @Override public Optional load(Long key) throws Exception { Transaction transaction = Tracer.newTransaction(TRACER_EVENT_CACHE_LOAD_ID, String.valueOf(key)); try { Release release = releaseService.findActiveOne(key); transaction.setStatus(Transaction.SUCCESS); return Optional.ofNullable(release); } catch (Throwable ex) { transaction.setStatus(ex); throw ex; } finally { transaction.complete(); } } }); if (bizConfig.isConfigServiceCacheStatsEnabled()) { GuavaCacheMetrics.monitor(meterRegistry, configIdCache, "config_id_cache"); } } private static class ConfigCacheEntry { private final long notificationId; private final Release release; public ConfigCacheEntry(long notificationId, Release release) { this.notificationId = notificationId; this.release = release; } public long getNotificationId() { return notificationId; } public Release getRelease() { return release; } } } ================================================ FILE: apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/service/config/DefaultConfigService.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.configservice.service.config; import com.ctrip.framework.apollo.biz.entity.Release; import com.ctrip.framework.apollo.biz.entity.ReleaseMessage; import com.ctrip.framework.apollo.biz.grayReleaseRule.GrayReleaseRulesHolder; import com.ctrip.framework.apollo.biz.service.ReleaseService; import com.ctrip.framework.apollo.core.dto.ApolloNotificationMessages; import com.google.common.collect.ImmutableMap; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; /** * config service with no cache * * @author Jason Song(song_s@ctrip.com) */ public class DefaultConfigService extends AbstractConfigService { private final ReleaseService releaseService; private final GrayReleaseRulesHolder grayReleaseRulesHolder; public DefaultConfigService(final ReleaseService releaseService, final GrayReleaseRulesHolder grayReleaseRulesHolder) { super(grayReleaseRulesHolder); this.releaseService = releaseService; this.grayReleaseRulesHolder = grayReleaseRulesHolder; } @Override protected Release findActiveOne(long id, ApolloNotificationMessages clientMessages) { return releaseService.findActiveOne(id); } @Override protected Release findLatestActiveRelease(String configAppId, String configClusterName, String configNamespace, ApolloNotificationMessages clientMessages) { return releaseService.findLatestActiveRelease(configAppId, configClusterName, configNamespace); } @Override public void handleMessage(ReleaseMessage message, String channel) { // since there is no cache, so do nothing } @Override public Map findReleasesByReleaseKeys(Set releaseKeys) { List releasesMap = releaseService.findByReleaseKeys(releaseKeys); if (releasesMap != null) { return releasesMap.stream() .collect(ImmutableMap.toImmutableMap(Release::getReleaseKey, release -> release)); } return Collections.emptyMap(); } } ================================================ FILE: apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/service/config/DefaultIncrementalSyncService.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.configservice.service.config; import com.ctrip.framework.apollo.core.dto.ConfigurationChange; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.concurrent.TimeUnit; public class DefaultIncrementalSyncService implements IncrementalSyncService { private final Cache> configurationChangeCache; public DefaultIncrementalSyncService() { configurationChangeCache = CacheBuilder.newBuilder().maximumSize(1000).expireAfterWrite(10, TimeUnit.MINUTES).build(); } @Override public List getConfigurationChanges(String latestMergedReleaseKey, Map latestReleaseConfigurations, String clientSideReleaseKey, Map clientSideConfigurations) { ReleaseKeyPair key = new ReleaseKeyPair(clientSideReleaseKey, latestMergedReleaseKey); List cachedChanges = configurationChangeCache.getIfPresent(key); if (cachedChanges != null) { return cachedChanges; } List computed = calcConfigurationChanges(latestReleaseConfigurations, clientSideConfigurations); configurationChangeCache.put(key, computed); return computed; } private List calcConfigurationChanges( Map latestReleaseConfigurations, Map clientSideConfigurations) { if (latestReleaseConfigurations == null) { latestReleaseConfigurations = new HashMap<>(); } if (clientSideConfigurations == null) { clientSideConfigurations = new HashMap<>(); } Set previousKeys = clientSideConfigurations.keySet(); Set currentKeys = latestReleaseConfigurations.keySet(); Set commonKeys = Sets.intersection(previousKeys, currentKeys); Set newKeys = Sets.difference(currentKeys, commonKeys); Set removedKeys = Sets.difference(previousKeys, commonKeys); List changes = Lists.newArrayList(); for (String newKey : newKeys) { changes .add(new ConfigurationChange(newKey, latestReleaseConfigurations.get(newKey), "ADDED")); } for (String removedKey : removedKeys) { changes.add(new ConfigurationChange(removedKey, null, "DELETED")); } for (String commonKey : commonKeys) { String previousValue = clientSideConfigurations.get(commonKey); String currentValue = latestReleaseConfigurations.get(commonKey); if (com.google.common.base.Objects.equal(previousValue, currentValue)) { continue; } changes.add(new ConfigurationChange(commonKey, currentValue, "MODIFIED")); } return changes; } public static class ReleaseKeyPair { private final String clientSideReleaseKey; private final String latestMergedReleaseKey; public ReleaseKeyPair(String clientSideReleaseKey, String latestMergedReleaseKey) { this.clientSideReleaseKey = clientSideReleaseKey; this.latestMergedReleaseKey = latestMergedReleaseKey; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (!(obj instanceof ReleaseKeyPair)) { return false; } ReleaseKeyPair that = (ReleaseKeyPair) obj; return Objects.equals(clientSideReleaseKey, that.clientSideReleaseKey) && Objects.equals(latestMergedReleaseKey, that.latestMergedReleaseKey); } @Override public int hashCode() { return Objects.hash(clientSideReleaseKey, latestMergedReleaseKey); } } } ================================================ FILE: apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/service/config/IncrementalSyncService.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.configservice.service.config; import com.ctrip.framework.apollo.core.dto.ConfigurationChange; import java.util.List; import java.util.Map; public interface IncrementalSyncService { List getConfigurationChanges(String latestMergedReleaseKey, Map latestReleaseConfigurations, String clientSideReleaseKey, Map clientSideConfigurations); } ================================================ FILE: apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/util/AccessKeyUtil.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.configservice.util; import com.ctrip.framework.apollo.configservice.service.AccessKeyServiceWithCache; import com.ctrip.framework.apollo.core.signature.Signature; import com.google.common.base.Strings; import java.util.List; import jakarta.servlet.http.HttpServletRequest; import org.apache.commons.lang.StringUtils; import org.springframework.stereotype.Component; /** * @author nisiyong */ @Component public class AccessKeyUtil { private static final String URL_SEPARATOR = "/"; private static final String URL_CONFIGS_PREFIX = "/configs/"; private static final String URL_CONFIGFILES_JSON_PREFIX = "/configfiles/json/"; private static final String URL_CONFIGFILES_PREFIX = "/configfiles/"; private static final String URL_NOTIFICATIONS_PREFIX = "/notifications/v2"; private final AccessKeyServiceWithCache accessKeyServiceWithCache; public AccessKeyUtil(AccessKeyServiceWithCache accessKeyServiceWithCache) { this.accessKeyServiceWithCache = accessKeyServiceWithCache; } public List findAvailableSecret(String appId) { return accessKeyServiceWithCache.getAvailableSecrets(appId); } public List findObservableSecrets(String appId) { return accessKeyServiceWithCache.getObservableSecrets(appId); } public String extractAppIdFromRequest(HttpServletRequest request) { String appId = null; String servletPath = request.getServletPath(); if (StringUtils.startsWith(servletPath, URL_CONFIGS_PREFIX)) { appId = StringUtils.substringBetween(servletPath, URL_CONFIGS_PREFIX, URL_SEPARATOR); } else if (StringUtils.startsWith(servletPath, URL_CONFIGFILES_JSON_PREFIX)) { appId = StringUtils.substringBetween(servletPath, URL_CONFIGFILES_JSON_PREFIX, URL_SEPARATOR); } else if (StringUtils.startsWith(servletPath, URL_CONFIGFILES_PREFIX)) { appId = StringUtils.substringBetween(servletPath, URL_CONFIGFILES_PREFIX, URL_SEPARATOR); } else if (StringUtils.startsWith(servletPath, URL_NOTIFICATIONS_PREFIX)) { appId = request.getParameter("appId"); } return appId; } public String buildSignature(String path, String query, String timestampString, String secret) { String pathWithQuery = path; if (!Strings.isNullOrEmpty(query)) { pathWithQuery += "?" + query; } return Signature.signature(timestampString, pathWithQuery, secret); } } ================================================ FILE: apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/util/InstanceConfigAuditUtil.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.configservice.util; import com.ctrip.framework.apollo.biz.config.BizConfig; import com.google.common.base.Joiner; import com.google.common.base.Strings; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import com.google.common.collect.Lists; import com.google.common.collect.Queues; import com.ctrip.framework.apollo.biz.entity.Instance; import com.ctrip.framework.apollo.biz.entity.InstanceConfig; import com.ctrip.framework.apollo.biz.service.InstanceService; import com.ctrip.framework.apollo.core.ConfigConsts; import com.ctrip.framework.apollo.core.utils.ApolloThreadFactory; import com.ctrip.framework.apollo.tracer.Tracer; import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.binder.cache.GuavaCacheMetrics; import org.springframework.beans.factory.InitializingBean; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.stereotype.Service; import java.util.Date; import java.util.List; import java.util.Objects; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; /** * @author Jason Song(song_s@ctrip.com) */ @Service public class InstanceConfigAuditUtil implements InitializingBean { private static final Joiner STRING_JOINER = Joiner.on(ConfigConsts.CLUSTER_NAMESPACE_SEPARATOR); private final ExecutorService auditExecutorService; private final AtomicBoolean auditStopped; private BlockingQueue audits; private Cache instanceCache; private Cache instanceConfigReleaseKeyCache; private final InstanceService instanceService; private final BizConfig bizConfig; private final MeterRegistry meterRegistry; public InstanceConfigAuditUtil(final InstanceService instanceService, final BizConfig bizConfig, final MeterRegistry meterRegistry) { this.instanceService = instanceService; this.bizConfig = bizConfig; this.meterRegistry = meterRegistry; audits = Queues.newLinkedBlockingQueue(this.bizConfig.getInstanceConfigAuditMaxSize()); auditExecutorService = Executors .newSingleThreadExecutor(ApolloThreadFactory.create("InstanceConfigAuditUtil", true)); auditStopped = new AtomicBoolean(false); buildInstanceCache(); buildInstanceConfigReleaseKeyCache(); } public boolean audit(String appId, String clusterName, String dataCenter, String ip, String configAppId, String configClusterName, String configNamespace, String releaseKey) { return this.audits.offer(new InstanceConfigAuditModel(appId, clusterName, dataCenter, ip, configAppId, configClusterName, configNamespace, releaseKey)); } void doAudit(InstanceConfigAuditModel auditModel) { String instanceCacheKey = assembleInstanceKey(auditModel.getAppId(), auditModel.getClusterName(), auditModel.getIp(), auditModel.getDataCenter()); Long instanceId = instanceCache.getIfPresent(instanceCacheKey); if (instanceId == null) { instanceId = prepareInstanceId(auditModel); instanceCache.put(instanceCacheKey, instanceId); } // load instance config release key from cache, and check if release key is the same String instanceConfigCacheKey = assembleInstanceConfigKey(instanceId, auditModel.getConfigAppId(), auditModel.getConfigNamespace()); String cacheReleaseKey = instanceConfigReleaseKeyCache.getIfPresent(instanceConfigCacheKey); // if release key is the same, then skip audit if (cacheReleaseKey != null && Objects.equals(cacheReleaseKey, auditModel.getReleaseKey())) { return; } instanceConfigReleaseKeyCache.put(instanceConfigCacheKey, auditModel.getReleaseKey()); // if release key is not the same or cannot find in cache, then do audit InstanceConfig instanceConfig = instanceService.findInstanceConfig(instanceId, auditModel.getConfigAppId(), auditModel.getConfigNamespace()); if (instanceConfig != null) { if (!Objects.equals(instanceConfig.getReleaseKey(), auditModel.getReleaseKey())) { instanceConfig.setConfigClusterName(auditModel.getConfigClusterName()); instanceConfig.setReleaseKey(auditModel.getReleaseKey()); instanceConfig.setReleaseDeliveryTime(auditModel.getOfferTime()); } else if (offerTimeAndLastModifiedTimeCloseEnough(auditModel.getOfferTime(), instanceConfig.getDataChangeLastModifiedTime())) { // when releaseKey is the same, optimize to reduce writes if the record was updated not long // ago return; } // we need to update no matter the release key is the same or not, to ensure the // last modified time is updated each day instanceConfig.setDataChangeLastModifiedTime(auditModel.getOfferTime()); instanceService.updateInstanceConfig(instanceConfig); return; } instanceConfig = new InstanceConfig(); instanceConfig.setInstanceId(instanceId); instanceConfig.setConfigAppId(auditModel.getConfigAppId()); instanceConfig.setConfigClusterName(auditModel.getConfigClusterName()); instanceConfig.setConfigNamespaceName(auditModel.getConfigNamespace()); instanceConfig.setReleaseKey(auditModel.getReleaseKey()); instanceConfig.setReleaseDeliveryTime(auditModel.getOfferTime()); instanceConfig.setDataChangeCreatedTime(auditModel.getOfferTime()); try { instanceService.createInstanceConfig(instanceConfig); } catch (DataIntegrityViolationException ex) { // concurrent insertion, safe to ignore } } private boolean offerTimeAndLastModifiedTimeCloseEnough(Date offerTime, Date lastModifiedTime) { return (offerTime.getTime() - lastModifiedTime.getTime()) < this.bizConfig .getInstanceConfigAuditTimeThresholdInMilli(); } private long prepareInstanceId(InstanceConfigAuditModel auditModel) { Instance instance = instanceService.findInstance(auditModel.getAppId(), auditModel.getClusterName(), auditModel.getDataCenter(), auditModel.getIp()); if (instance != null) { return instance.getId(); } instance = new Instance(); instance.setAppId(auditModel.getAppId()); instance.setClusterName(auditModel.getClusterName()); instance.setDataCenter(auditModel.getDataCenter()); instance.setIp(auditModel.getIp()); try { return instanceService.createInstance(instance).getId(); } catch (DataIntegrityViolationException ex) { // return the one exists return instanceService.findInstance(instance.getAppId(), instance.getClusterName(), instance.getDataCenter(), instance.getIp()).getId(); } } @Override public void afterPropertiesSet() throws Exception { auditExecutorService.submit(() -> { while (!auditStopped.get() && !Thread.currentThread().isInterrupted()) { try { InstanceConfigAuditModel model = audits.take(); doAudit(model); } catch (Throwable ex) { Tracer.logError(ex); } } }); } private void buildInstanceCache() { CacheBuilder instanceCacheBuilder = CacheBuilder.newBuilder() .expireAfterAccess(1, TimeUnit.HOURS).maximumSize(this.bizConfig.getInstanceCacheMaxSize()); if (bizConfig.isConfigServiceCacheStatsEnabled()) { instanceCacheBuilder.recordStats(); } instanceCache = instanceCacheBuilder.build(); if (bizConfig.isConfigServiceCacheStatsEnabled()) { GuavaCacheMetrics.monitor(meterRegistry, instanceCache, "instance_cache"); } } private void buildInstanceConfigReleaseKeyCache() { CacheBuilder instanceConfigReleaseKeyCacheBuilder = CacheBuilder.newBuilder().expireAfterWrite(1, TimeUnit.DAYS) .maximumSize(this.bizConfig.getInstanceConfigCacheMaxSize()); if (bizConfig.isConfigServiceCacheStatsEnabled()) { instanceConfigReleaseKeyCacheBuilder.recordStats(); } instanceConfigReleaseKeyCache = instanceConfigReleaseKeyCacheBuilder.build(); if (bizConfig.isConfigServiceCacheStatsEnabled()) { GuavaCacheMetrics.monitor(meterRegistry, instanceConfigReleaseKeyCache, "instance_config_cache"); } } private String assembleInstanceKey(String appId, String cluster, String ip, String datacenter) { List keyParts = Lists.newArrayList(appId, cluster, ip); if (!Strings.isNullOrEmpty(datacenter)) { keyParts.add(datacenter); } return STRING_JOINER.join(keyParts); } private String assembleInstanceConfigKey(long instanceId, String configAppId, String configNamespace) { return STRING_JOINER.join(instanceId, configAppId, configNamespace); } public static class InstanceConfigAuditModel { private String appId; private String clusterName; private String dataCenter; private String ip; private String configAppId; private String configClusterName; private String configNamespace; private String releaseKey; private Date offerTime; public InstanceConfigAuditModel(String appId, String clusterName, String dataCenter, String clientIp, String configAppId, String configClusterName, String configNamespace, String releaseKey) { this.offerTime = new Date(); this.appId = appId; this.clusterName = clusterName; this.dataCenter = Strings.isNullOrEmpty(dataCenter) ? "" : dataCenter; this.ip = clientIp; this.configAppId = configAppId; this.configClusterName = configClusterName; this.configNamespace = configNamespace; this.releaseKey = releaseKey; } public String getAppId() { return appId; } public String getClusterName() { return clusterName; } public String getDataCenter() { return dataCenter; } public String getIp() { return ip; } public String getConfigAppId() { return configAppId; } public String getConfigNamespace() { return configNamespace; } public String getReleaseKey() { return releaseKey; } public String getConfigClusterName() { return configClusterName; } public Date getOfferTime() { return offerTime; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } InstanceConfigAuditModel model = (InstanceConfigAuditModel) o; return Objects.equals(appId, model.appId) && Objects.equals(clusterName, model.clusterName) && Objects.equals(dataCenter, model.dataCenter) && Objects.equals(ip, model.ip) && Objects.equals(configAppId, model.configAppId) && Objects.equals(configClusterName, model.configClusterName) && Objects.equals(configNamespace, model.configNamespace) && Objects.equals(releaseKey, model.releaseKey); } @Override public int hashCode() { return Objects.hash(appId, clusterName, dataCenter, ip, configAppId, configClusterName, configNamespace, releaseKey); } } } ================================================ FILE: apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/util/NamespaceUtil.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.configservice.util; import com.ctrip.framework.apollo.common.entity.AppNamespace; import com.ctrip.framework.apollo.configservice.service.AppNamespaceServiceWithCache; import org.springframework.stereotype.Component; /** * @author Jason Song(song_s@ctrip.com) */ @Component public class NamespaceUtil { private final AppNamespaceServiceWithCache appNamespaceServiceWithCache; public NamespaceUtil(final AppNamespaceServiceWithCache appNamespaceServiceWithCache) { this.appNamespaceServiceWithCache = appNamespaceServiceWithCache; } public String filterNamespaceName(String namespaceName) { if (namespaceName.toLowerCase().endsWith(".properties")) { int dotIndex = namespaceName.lastIndexOf("."); return namespaceName.substring(0, dotIndex); } return namespaceName; } public String normalizeNamespace(String appId, String namespaceName) { AppNamespace appNamespace = appNamespaceServiceWithCache.findByAppIdAndNamespace(appId, namespaceName); if (appNamespace != null) { return appNamespace.getName(); } appNamespace = appNamespaceServiceWithCache.findPublicNamespaceByName(namespaceName); if (appNamespace != null) { return appNamespace.getName(); } return namespaceName; } } ================================================ FILE: apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/util/WatchKeysUtil.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.configservice.util; import static com.ctrip.framework.apollo.biz.utils.ReleaseMessageKeyGenerator.generate; import com.ctrip.framework.apollo.common.entity.AppNamespace; import com.ctrip.framework.apollo.configservice.service.AppNamespaceServiceWithCache; import com.ctrip.framework.apollo.core.ConfigConsts; import com.google.common.base.Strings; import com.google.common.collect.HashMultimap; import com.google.common.collect.Multimap; import com.google.common.collect.Sets; import org.springframework.stereotype.Component; import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; /** * @author Jason Song(song_s@ctrip.com) */ @Component public class WatchKeysUtil { private final AppNamespaceServiceWithCache appNamespaceService; public WatchKeysUtil(final AppNamespaceServiceWithCache appNamespaceService) { this.appNamespaceService = appNamespaceService; } /** * Assemble watch keys for the given appId, cluster, namespace, dataCenter combination */ public Set assembleAllWatchKeys(String appId, String clusterName, String namespace, String dataCenter) { Multimap watchedKeysMap = assembleAllWatchKeys(appId, clusterName, Sets.newHashSet(namespace), dataCenter); return Sets.newHashSet(watchedKeysMap.get(namespace)); } /** * Assemble watch keys for the given appId, cluster, namespaces, dataCenter combination * * @return a multimap with namespace as the key and watch keys as the value */ public Multimap assembleAllWatchKeys(String appId, String clusterName, Set namespaces, String dataCenter) { Multimap watchedKeysMap = assembleWatchKeys(appId, clusterName, namespaces, dataCenter); // Every app has an 'application' namespace if (!(namespaces.size() == 1 && namespaces.contains(ConfigConsts.NAMESPACE_APPLICATION))) { Set namespacesBelongToAppId = namespacesBelongToAppId(appId, namespaces); Set publicNamespaces = Sets.difference(namespaces, namespacesBelongToAppId); // Listen on more namespaces if it's a public namespace if (!publicNamespaces.isEmpty()) { watchedKeysMap .putAll(findPublicConfigWatchKeys(appId, clusterName, publicNamespaces, dataCenter)); } } return watchedKeysMap; } private Multimap findPublicConfigWatchKeys(String applicationId, String clusterName, Set namespaces, String dataCenter) { Multimap watchedKeysMap = HashMultimap.create(); List appNamespaces = appNamespaceService.findPublicNamespacesByNames(namespaces); for (AppNamespace appNamespace : appNamespaces) { // check whether the namespace's appId equals to current one if (Objects.equals(applicationId, appNamespace.getAppId())) { continue; } String publicConfigAppId = appNamespace.getAppId(); watchedKeysMap.putAll(appNamespace.getName(), assembleWatchKeys(publicConfigAppId, clusterName, appNamespace.getName(), dataCenter)); } return watchedKeysMap; } private Set assembleWatchKeys(String appId, String clusterName, String namespace, String dataCenter) { if (ConfigConsts.NO_APPID_PLACEHOLDER.equalsIgnoreCase(appId)) { return Collections.emptySet(); } Set watchedKeys = Sets.newHashSet(); // watch specified cluster config change if (!Objects.equals(ConfigConsts.CLUSTER_NAME_DEFAULT, clusterName)) { watchedKeys.add(generate(appId, clusterName, namespace)); } // watch data center config change if (!Strings.isNullOrEmpty(dataCenter) && !Objects.equals(dataCenter, clusterName)) { watchedKeys.add(generate(appId, dataCenter, namespace)); } // watch default cluster config change watchedKeys.add(generate(appId, ConfigConsts.CLUSTER_NAME_DEFAULT, namespace)); return watchedKeys; } private Multimap assembleWatchKeys(String appId, String clusterName, Set namespaces, String dataCenter) { Multimap watchedKeysMap = HashMultimap.create(); for (String namespace : namespaces) { watchedKeysMap.putAll(namespace, assembleWatchKeys(appId, clusterName, namespace, dataCenter)); } return watchedKeysMap; } private Set namespacesBelongToAppId(String appId, Set namespaces) { if (ConfigConsts.NO_APPID_PLACEHOLDER.equalsIgnoreCase(appId)) { return Collections.emptySet(); } List appNamespaces = appNamespaceService.findByAppIdAndNamespaces(appId, namespaces); if (appNamespaces == null || appNamespaces.isEmpty()) { return Collections.emptySet(); } return appNamespaces.stream().map(AppNamespace::getName).collect(Collectors.toSet()); } } ================================================ FILE: apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/wrapper/CaseInsensitiveMapWrapper.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.configservice.wrapper; import java.util.Map; /** * @author Jason Song(song_s@ctrip.com) */ public class CaseInsensitiveMapWrapper { private final Map delegate; public CaseInsensitiveMapWrapper(Map delegate) { this.delegate = delegate; } public T get(String key) { return delegate.get(key.toLowerCase()); } public T put(String key, T value) { return delegate.put(key.toLowerCase(), value); } public T remove(String key) { return delegate.remove(key.toLowerCase()); } } ================================================ FILE: apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/wrapper/CaseInsensitiveMultimapWrapper.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.configservice.wrapper; import java.util.Collections; import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.function.Supplier; /** * Multimap with case-insensitive keys. *

* This class wraps a {@code Map>} to provide multimap-like behavior with * case-insensitive keys. *

* *

Thread-safety: This class is thread-safe if and only if the supplied delegate map and * the sets produced by {@code setSupplier} are thread-safe. *

* *

Views: {@link #get(String)} returns an unmodifiable view of the underlying set. * Mutations should be done via {@link #put(String, Object)} and {@link #remove(String, Object)} so * that empty sets can be cleaned up. *

*/ public class CaseInsensitiveMultimapWrapper { private final Map> delegate; private final Supplier> setSupplier; public CaseInsensitiveMultimapWrapper(Map> delegate, Supplier> setSupplier) { this.delegate = delegate; this.setSupplier = setSupplier; } private static String normalizeKey(String key) { return Objects.requireNonNull(key, "key").toLowerCase(Locale.ROOT); } public boolean put(String key, V value) { boolean[] added = {false}; delegate.compute(normalizeKey(key), (k, set) -> { if (set == null) { set = setSupplier.get(); } added[0] = set.add(value); return set; }); return added[0]; } public boolean remove(String key, V value) { boolean[] removed = {false}; delegate.computeIfPresent(normalizeKey(key), (k, set) -> { removed[0] = set.remove(value); return set.isEmpty() ? null : set; }); return removed[0]; } public Set get(String key) { Set set = delegate.get(normalizeKey(key)); return set != null ? Collections.unmodifiableSet(set) : Collections.emptySet(); } public boolean containsKey(String key) { Set set = delegate.get(normalizeKey(key)); return set != null && !set.isEmpty(); } /** * Returns the total number of values across all keys. *

* Note: In concurrent scenarios, the returned value is a best-effort approximation. *

*/ public int size() { int size = 0; for (Set set : delegate.values()) { size += set.size(); } return size; } } ================================================ FILE: apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/wrapper/DeferredResultWrapper.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.configservice.wrapper; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.ctrip.framework.apollo.core.dto.ApolloConfigNotification; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.lang.NonNull; import org.springframework.web.context.request.async.DeferredResult; import java.util.List; import java.util.Map; /** * @author Jason Song(song_s@ctrip.com) */ public class DeferredResultWrapper implements Comparable { private static final ResponseEntity> NOT_MODIFIED_RESPONSE_LIST = new ResponseEntity<>(HttpStatus.NOT_MODIFIED); private Map normalizedNamespaceNameToOriginalNamespaceName; private DeferredResult>> result; public DeferredResultWrapper(long timeoutInMilli) { result = new DeferredResult<>(timeoutInMilli, NOT_MODIFIED_RESPONSE_LIST); } public void recordNamespaceNameNormalizedResult(String originalNamespaceName, String normalizedNamespaceName) { if (normalizedNamespaceNameToOriginalNamespaceName == null) { normalizedNamespaceNameToOriginalNamespaceName = Maps.newHashMap(); } normalizedNamespaceNameToOriginalNamespaceName.put(normalizedNamespaceName, originalNamespaceName); } public void onTimeout(Runnable timeoutCallback) { result.onTimeout(timeoutCallback); } public void onCompletion(Runnable completionCallback) { result.onCompletion(completionCallback); } public void setResult(ApolloConfigNotification notification) { setResult(Lists.newArrayList(notification)); } /** * The namespace name is used as a key in client side, so we have to return the original one instead of the correct one */ public void setResult(List notifications) { if (normalizedNamespaceNameToOriginalNamespaceName != null) { notifications.stream() .filter(notification -> normalizedNamespaceNameToOriginalNamespaceName .containsKey(notification.getNamespaceName())) .forEach(notification -> notification.setNamespaceName( normalizedNamespaceNameToOriginalNamespaceName.get(notification.getNamespaceName()))); } result.setResult(new ResponseEntity<>(notifications, HttpStatus.OK)); } public DeferredResult>> getResult() { return result; } @Override public int compareTo(@NonNull DeferredResultWrapper deferredResultWrapper) { return Integer.compare(this.hashCode(), deferredResultWrapper.hashCode()); } } ================================================ FILE: apollo-configservice/src/main/java/com/ctrip/framework/apollo/metaservice/ApolloMetaServiceConfig.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.metaservice; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Bean; import org.springframework.security.web.firewall.DefaultHttpFirewall; import org.springframework.security.web.firewall.HttpFirewall; @EnableAutoConfiguration @Configuration @ComponentScan(basePackageClasses = ApolloMetaServiceConfig.class) public class ApolloMetaServiceConfig { @Bean public HttpFirewall allowUrlEncodedSlashHttpFirewall() { return new DefaultHttpFirewall(); } } ================================================ FILE: apollo-configservice/src/main/java/com/ctrip/framework/apollo/metaservice/controller/HomePageController.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.metaservice.controller; import com.ctrip.framework.apollo.core.ServiceNameConsts; import com.ctrip.framework.apollo.core.dto.ServiceDTO; import com.ctrip.framework.apollo.metaservice.service.DiscoveryService; import com.google.common.collect.Lists; import java.util.List; import org.springframework.context.annotation.Profile; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; /** * For non-eureka discovery services such as kubernetes and nacos, there is no eureka home page, so we need to add a default one */ @Profile({"kubernetes", "nacos-discovery", "consul-discovery", "zookeeper-discovery", "custom-defined-discovery", "database-discovery",}) @RestController public class HomePageController { private final DiscoveryService discoveryService; public HomePageController(DiscoveryService discoveryService) { this.discoveryService = discoveryService; } @GetMapping("/") public List listAllServices() { List allServices = Lists.newLinkedList(); allServices .addAll(discoveryService.getServiceInstances(ServiceNameConsts.APOLLO_CONFIGSERVICE)); allServices.addAll(discoveryService.getServiceInstances(ServiceNameConsts.APOLLO_ADMINSERVICE)); return allServices; } } ================================================ FILE: apollo-configservice/src/main/java/com/ctrip/framework/apollo/metaservice/controller/ServiceController.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.metaservice.controller; import com.ctrip.framework.apollo.core.ServiceNameConsts; import com.ctrip.framework.apollo.core.dto.ServiceDTO; import com.ctrip.framework.apollo.metaservice.service.DiscoveryService; import java.util.Collections; import java.util.List; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/services") public class ServiceController { private final DiscoveryService discoveryService; public ServiceController(final DiscoveryService discoveryService) { this.discoveryService = discoveryService; } /** * This method always return an empty list as meta service is not used at all */ @Deprecated @RequestMapping("/meta") public List getMetaService() { return Collections.emptyList(); } @RequestMapping("/config") public List getConfigService( @RequestParam(value = "appId", defaultValue = "") String appId, @RequestParam(value = "ip", required = false) String clientIp) { return discoveryService.getServiceInstances(ServiceNameConsts.APOLLO_CONFIGSERVICE); } @RequestMapping("/admin") public List getAdminService() { return discoveryService.getServiceInstances(ServiceNameConsts.APOLLO_ADMINSERVICE); } } ================================================ FILE: apollo-configservice/src/main/java/com/ctrip/framework/apollo/metaservice/service/DatabaseDiscoveryService.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.metaservice.service; import com.ctrip.framework.apollo.biz.registry.DatabaseDiscoveryClient; import com.ctrip.framework.apollo.biz.registry.ServiceInstance; import com.ctrip.framework.apollo.core.dto.ServiceDTO; import java.util.ArrayList; import java.util.List; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; /** * use database as a registry */ @Service @Profile("database-discovery") public class DatabaseDiscoveryService implements DiscoveryService { private final DatabaseDiscoveryClient discoveryClient; public DatabaseDiscoveryService(DatabaseDiscoveryClient discoveryClient) { this.discoveryClient = discoveryClient; } @Override public List getServiceInstances(String serviceId) { List serviceInstanceList = this.discoveryClient.getInstances(serviceId); return convert(serviceInstanceList); } static List convert(List list) { List serviceDTOList = new ArrayList<>(list.size()); for (ServiceInstance serviceInstance : list) { ServiceDTO serviceDTO = convert(serviceInstance); serviceDTOList.add(serviceDTO); } return serviceDTOList; } static ServiceDTO convert(ServiceInstance serviceInstance) { ServiceDTO serviceDTO = new ServiceDTO(); serviceDTO.setAppName(serviceInstance.getServiceName()); String homePageUrl = serviceInstance.getUri().toString(); serviceDTO.setInstanceId(homePageUrl); serviceDTO.setHomepageUrl(homePageUrl); return serviceDTO; } } ================================================ FILE: apollo-configservice/src/main/java/com/ctrip/framework/apollo/metaservice/service/DefaultDiscoveryService.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.metaservice.service; import com.ctrip.framework.apollo.common.condition.ConditionalOnMissingProfile; import com.ctrip.framework.apollo.core.dto.ServiceDTO; import com.ctrip.framework.apollo.tracer.Tracer; import com.netflix.appinfo.InstanceInfo; import com.netflix.discovery.EurekaClient; import com.netflix.discovery.shared.Application; import java.util.Collections; import java.util.List; import java.util.function.Function; import java.util.stream.Collectors; import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; /** * Default discovery service for Eureka */ @Service @ConditionalOnMissingProfile({"kubernetes", "nacos-discovery", "consul-discovery", "zookeeper-discovery", "custom-defined-discovery", "database-discovery",}) public class DefaultDiscoveryService implements DiscoveryService { private final EurekaClient eurekaClient; public DefaultDiscoveryService(final EurekaClient eurekaClient) { this.eurekaClient = eurekaClient; } @Override public List getServiceInstances(String serviceId) { Application application = eurekaClient.getApplication(serviceId); if (application == null || CollectionUtils.isEmpty(application.getInstances())) { Tracer.logEvent("Apollo.Discovery.NotFound", serviceId); return Collections.emptyList(); } return application.getInstances().stream().map(instanceInfoToServiceDTOFunc) .collect(Collectors.toList()); } private static final Function instanceInfoToServiceDTOFunc = instance -> { ServiceDTO service = new ServiceDTO(); service.setAppName(instance.getAppName()); service.setInstanceId(instance.getInstanceId()); service.setHomepageUrl(instance.getHomePageUrl()); return service; }; } ================================================ FILE: apollo-configservice/src/main/java/com/ctrip/framework/apollo/metaservice/service/DiscoveryService.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.metaservice.service; import com.ctrip.framework.apollo.core.dto.ServiceDTO; import java.util.List; public interface DiscoveryService { /** * @param serviceId the service id * @return the service instance list for the specified service id, or an empty list if no service * instance available */ List getServiceInstances(String serviceId); } ================================================ FILE: apollo-configservice/src/main/java/com/ctrip/framework/apollo/metaservice/service/KubernetesDiscoveryService.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.metaservice.service; import com.ctrip.framework.apollo.biz.config.BizConfig; import com.ctrip.framework.apollo.core.ServiceNameConsts; import com.ctrip.framework.apollo.core.dto.ServiceDTO; import com.google.common.base.Splitter; import com.google.common.base.Strings; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; import java.util.Collections; import java.util.List; import java.util.Map; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; /** * This is a simple implementation that skips any service discovery and just return what is configured * *
    *
  • getServiceInstances("apollo-configservice") returns ${apollo.config-service.url}
  • *
  • getServiceInstances("apollo-adminservice") returns ${apollo.admin-service.url}
  • *
*/ @Service @Profile({"kubernetes", "custom-defined-discovery"}) public class KubernetesDiscoveryService implements DiscoveryService { private static final Splitter COMMA_SPLITTER = Splitter.on(",").omitEmptyStrings().trimResults(); private static final Map SERVICE_ID_TO_CONFIG_NAME = ImmutableMap.of(ServiceNameConsts.APOLLO_CONFIGSERVICE, "apollo.config-service.url", ServiceNameConsts.APOLLO_ADMINSERVICE, "apollo.admin-service.url"); private final BizConfig bizConfig; public KubernetesDiscoveryService(final BizConfig bizConfig) { this.bizConfig = bizConfig; } @Override public List getServiceInstances(String serviceId) { String configName = SERVICE_ID_TO_CONFIG_NAME.get(serviceId); if (configName == null) { return Collections.emptyList(); } return assembleServiceDTO(serviceId, bizConfig.getValue(configName)); } private List assembleServiceDTO(String serviceId, String directUrl) { if (Strings.isNullOrEmpty(directUrl)) { return Collections.emptyList(); } List serviceDTOList = Lists.newLinkedList(); COMMA_SPLITTER.split(directUrl).forEach(url -> { ServiceDTO service = new ServiceDTO(); service.setAppName(serviceId); service.setInstanceId(String.format("%s:%s", serviceId, url)); service.setHomepageUrl(url); serviceDTOList.add(service); }); return serviceDTOList; } } ================================================ FILE: apollo-configservice/src/main/java/com/ctrip/framework/apollo/metaservice/service/NacosDiscoveryService.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.metaservice.service; import com.alibaba.nacos.api.annotation.NacosInjected; import com.alibaba.nacos.api.exception.NacosException; import com.alibaba.nacos.api.naming.NamingService; import com.alibaba.nacos.api.naming.pojo.Instance; import com.ctrip.framework.apollo.core.dto.ServiceDTO; import com.google.common.collect.Lists; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; import java.util.Collections; import java.util.List; /** * @author : kl * Service discovery nacos implementation **/ @Service @Profile({"nacos-discovery"}) public class NacosDiscoveryService implements DiscoveryService { private final static Logger logger = LoggerFactory.getLogger(NacosDiscoveryService.class); private NamingService namingService; @NacosInjected public void setNamingService(NamingService namingService) { this.namingService = namingService; } @Override public List getServiceInstances(String serviceId) { try { List instances = namingService.selectInstances(serviceId, true); List serviceDTOList = Lists.newLinkedList(); instances.forEach(instance -> { ServiceDTO serviceDTO = this.toServiceDTO(instance, serviceId); serviceDTOList.add(serviceDTO); }); return serviceDTOList; } catch (NacosException ex) { logger.error(ex.getMessage(), ex); } return Collections.emptyList(); } private ServiceDTO toServiceDTO(Instance instance, String appName) { ServiceDTO service = new ServiceDTO(); service.setAppName(appName); service.setInstanceId(instance.getInstanceId()); String homePageUrl = "http://" + instance.getIp() + ":" + instance.getPort() + "/"; service.setHomepageUrl(homePageUrl); return service; } } ================================================ FILE: apollo-configservice/src/main/java/com/ctrip/framework/apollo/metaservice/service/SpringCloudInnerDiscoveryService.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.metaservice.service; import com.ctrip.framework.apollo.core.dto.ServiceDTO; import com.google.common.collect.Lists; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; import java.util.List; /** * @author : kl * Service discovery consul implementation **/ @Service @Profile({"consul-discovery", "zookeeper-discovery"}) public class SpringCloudInnerDiscoveryService implements DiscoveryService { private final DiscoveryClient discoveryClient; public SpringCloudInnerDiscoveryService(DiscoveryClient discoveryClient) { this.discoveryClient = discoveryClient; } @Override public List getServiceInstances(String serviceId) { List instances = discoveryClient.getInstances(serviceId); List serviceDTOList = Lists.newLinkedList(); if (!CollectionUtils.isEmpty(instances)) { instances.forEach(instance -> { ServiceDTO serviceDTO = this.toServiceDTO(instance, serviceId); serviceDTOList.add(serviceDTO); }); } return serviceDTOList; } private ServiceDTO toServiceDTO(ServiceInstance instance, String appName) { ServiceDTO service = new ServiceDTO(); service.setAppName(appName); service.setInstanceId(instance.getInstanceId()); String homePageUrl = "http://" + instance.getHost() + ":" + instance.getPort() + "/"; service.setHomepageUrl(homePageUrl); return service; } } ================================================ FILE: apollo-configservice/src/main/resources/apollo-configservice.conf ================================================ MODE=service PID_FOLDER=. # console appender log file folder LOG_FOLDER=/opt/logs/ # console appender log file name LOG_FILENAME=apollo-configservice.console.log # write application logs only to file appender export LOG_APPENDERS=FILE ================================================ FILE: apollo-configservice/src/main/resources/application-consul-discovery.properties ================================================ # # Copyright 2024 Apollo Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # apollo.eureka.server.enabled=false eureka.client.enabled=false #consul enabled spring.cloud.consul.enabled=true spring.cloud.consul.discovery.enabled=true spring.cloud.consul.service-registry.enabled=true spring.cloud.consul.discovery.heartbeat.enabled=true spring.cloud.consul.discovery.instance-id=apollo-configservice ================================================ FILE: apollo-configservice/src/main/resources/application-custom-defined-discovery.properties ================================================ # # Copyright 2024 Apollo Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # apollo.eureka.server.enabled=false eureka.client.enabled=false spring.cloud.discovery.enabled=false ================================================ FILE: apollo-configservice/src/main/resources/application-database-discovery.properties ================================================ # # Copyright 2024 Apollo Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # apollo.eureka.server.enabled=false eureka.client.enabled=false spring.cloud.discovery.enabled=false apollo.service.registry.enabled=true apollo.service.registry.cluster=default apollo.service.registry.heartbeat-interval-in-second=10 apollo.service.discovery.enabled=true # health check by heartbeat, heartbeat time before 61s ago will be seemed as unhealthy apollo.service.discovery.health-check-interval-in-second = 61 ================================================ FILE: apollo-configservice/src/main/resources/application-github.properties ================================================ # # Copyright 2024 Apollo Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # DataSource spring.datasource.url = ${spring_datasource_url} spring.datasource.username = ${spring_datasource_username} spring.datasource.password = ${spring_datasource_password} ================================================ FILE: apollo-configservice/src/main/resources/application-kubernetes.properties ================================================ # # Copyright 2024 Apollo Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # apollo.eureka.server.enabled=false eureka.client.enabled=false spring.cloud.discovery.enabled=false ================================================ FILE: apollo-configservice/src/main/resources/application-nacos-discovery.properties ================================================ # # Copyright 2024 Apollo Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # apollo.eureka.server.enabled=false eureka.client.enabled=false spring.cloud.discovery.enabled=false #nacos enabled nacos.discovery.register.enabled=true nacos.discovery.auto-register=true nacos.discovery.register.service-name=apollo-configservice ================================================ FILE: apollo-configservice/src/main/resources/application-zookeeper-discovery.properties ================================================ # # Copyright 2024 Apollo Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # apollo.eureka.server.enabled=false eureka.client.enabled=false #zookeeper enabled spring.cloud.zookeeper.enabled=true spring.cloud.zookeeper.discovery.enabled=true spring.cloud.zookeeper.discovery.register=true spring.cloud.zookeeper.discovery.instance-id=${spring.cloud.client.ip-address}:${server.port} ================================================ FILE: apollo-configservice/src/main/resources/application.properties ================================================ # # Copyright 2024 Apollo Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # You may uncomment the following config to activate different spring profiles #spring.profiles.active=github,consul-discovery #spring.profiles.active=github,zookeeper-discovery #spring.profiles.active=github,custom-defined-discovery #spring.profiles.active=github,database-discovery # You may change the following config to activate different database profiles like h2/postgres spring.profiles.group.github = mysql ================================================ FILE: apollo-configservice/src/main/resources/application.yml ================================================ # # Copyright 2024 Apollo Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # spring: application: name: apollo-configservice profiles: active: ${apollo_profile} cloud: consul: enabled: false zookeeper: enabled: false jpa: properties: hibernate: metadata_builder_contributor: com.ctrip.framework.apollo.common.jpa.SqlFunctionsMetadataBuilderContributor lifecycle: timeout-per-shutdown-phase: ${GRACEFUL_SHUTDOWN_TIMEOUT:10s} server: port: 8080 shutdown: graceful logging: file: name: /opt/logs/apollo-configservice.log eureka: instance: hostname: ${hostname:localhost} prefer-ip-address: true status-page-url-path: /info health-check-url-path: /health server: peer-eureka-nodes-update-interval-ms: 60000 enable-self-preservation: false client: service-url: # This setting will be overridden by eureka.service.url setting from ApolloConfigDB.ServerConfig or System Property # see com.ctrip.framework.apollo.biz.eureka.ApolloEurekaClientConfig defaultZone: http://${eureka.instance.hostname}:8080/eureka/ healthcheck: enabled: true eureka-service-url-poll-interval-seconds: 60 fetch-registry: false register-with-eureka: false management: health: status: order: DOWN, OUT_OF_SERVICE, UNKNOWN, UP ================================================ FILE: apollo-configservice/src/main/resources/configservice.properties ================================================ # # Copyright 2024 Apollo Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # #Used for apollo-assembly spring.application.name= apollo-configservice server.port= 8080 logging.file.name= /opt/logs/apollo-configservice.log spring.jmx.default-domain = apollo-configservice ================================================ FILE: apollo-configservice/src/main/resources/jpa/configdb.init.h2.sql ================================================ -- -- Copyright 2024 Apollo Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. -- You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. -- INSERT INTO "ServerConfig" ("Key", "Cluster", "Value", "Comment", "DataChange_CreatedBy", "DataChange_CreatedTime") VALUES ('eureka.service.url', 'default', 'http://localhost:8080/eureka/', 'Eureka服务Url,多个service以英文逗号分隔', 'default', '1970-01-01 00:00:00'), ('namespace.lock.switch', 'default', 'false', '一次发布只能有一个人修改开关', 'default', '1970-01-01 00:00:00'), ('item.key.length.limit', 'default', '128', 'item key 最大长度限制', 'default', '1970-01-01 00:00:00'), ('item.value.length.limit', 'default', '20000', 'item value最大长度限制', 'default', '1970-01-01 00:00:00'), ('config-service.cache.enabled', 'default', 'false', 'ConfigService是否开启缓存,开启后能提高性能,但是会增大内存消耗!', 'default', '1970-01-01 00:00:00'), ('config-service.incremental.change.enabled', 'default', 'false', 'ConfigService是否开启客户端同步增量配置,开启后能提高性能,但是会增大内存消耗!', 'default', '1970-01-01 00:00:00'); CREATE ALIAS IF NOT EXISTS UNIX_TIMESTAMP FOR "com.ctrip.framework.apollo.common.jpa.H2Function.unixTimestamp"; ================================================ FILE: apollo-configservice/src/main/resources/logback.xml ================================================ propertyContains("LOG_APPENDERS", "FILE") && !propertyContains("LOG_APPENDERS", "CONSOLE") propertyContains("LOG_APPENDERS", "CONSOLE") && !propertyContains("LOG_APPENDERS", "FILE") ================================================ FILE: apollo-configservice/src/main/scripts/shutdown.sh ================================================ #!/bin/bash # # Copyright 2024 Apollo Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # SERVICE_NAME=apollo-configservice export APP_NAME=$SERVICE_NAME if [[ -z "$JAVA_HOME" && -d /usr/java/latest/ ]]; then export JAVA_HOME=/usr/java/latest/ fi cd `dirname $0`/.. if [[ ! -f $SERVICE_NAME".jar" && -d current ]]; then cd current fi if [[ -f $SERVICE_NAME".jar" ]]; then chmod a+x $SERVICE_NAME".jar" ./$SERVICE_NAME".jar" stop fi ================================================ FILE: apollo-configservice/src/main/scripts/startup.sh ================================================ #!/bin/bash # # Copyright 2024 Apollo Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # SERVICE_NAME=apollo-configservice ## Adjust log dir if necessary LOG_DIR=${LOG_DIR:=/opt/logs} ## Adjust server port if necessary SERVER_PORT=${SERVER_PORT:=8080} ## Adjust context path if necessary CONTEXT_PATH=${CONTEXT_PATH:=/} ## Create log directory if not existed because JDK 8+ won't do that mkdir -p $LOG_DIR # Create directory of -XX:HeapDumpPath mkdir -p $LOG_DIR/HeapDumpOnOutOfMemoryError/ ## Adjust memory settings if necessary #export JAVA_OPTS="-Xms6144m -Xmx6144m -Xss256k -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=384m -XX:NewSize=4096m -XX:MaxNewSize=4096m -XX:SurvivorRatio=8" ## Only uncomment the following when you are using server jvm #export JAVA_OPTS="$JAVA_OPTS -server -XX:-ReduceInitialCardMarks" ########### The following is the same for configservice, adminservice, portal ########### export JAVA_OPTS="$JAVA_OPTS -XX:ParallelGCThreads=4 -XX:MaxTenuringThreshold=9 -XX:+DisableExplicitGC -XX:+ScavengeBeforeFullGC -XX:SoftRefLRUPolicyMSPerMB=0 -XX:+ExplicitGCInvokesConcurrent -XX:+HeapDumpOnOutOfMemoryError -XX:-OmitStackTraceInFastThrow -Duser.timezone=Asia/Shanghai -Dclient.encoding.override=UTF-8 -Dfile.encoding=UTF-8 -Djava.security.egd=file:/dev/./urandom" # DS_URL, DS_USERNAME, DS_PASSWORD are deprecated, please use SPRING_DATASOURCE_URL, SPRING_DATASOURCE_USERNAME, SPRING_DATASOURCE_PASSWORD instead # DataSource URL USERNAME PASSWORD if [ "$DS_URL"x != x ] then export SPRING_DATASOURCE_URL=$DS_URL export SPRING_DATASOURCE_USERNAME=$DS_USERNAME export SPRING_DATASOURCE_PASSWORD=$DS_PASSWORD fi export JAVA_OPTS="$JAVA_OPTS -Dserver.port=$SERVER_PORT -Dlogging.file.name=$LOG_DIR/$SERVICE_NAME.log -XX:HeapDumpPath=$LOG_DIR/HeapDumpOnOutOfMemoryError/" export APP_NAME=$SERVICE_NAME PATH_TO_JAR=$SERVICE_NAME".jar" CONTEXT_PATH=$(echo "$CONTEXT_PATH" | sed 's/^\/*//; s/\/*$//') SERVER_URL="http://localhost:${SERVER_PORT}${CONTEXT_PATH:+/$CONTEXT_PATH}" function getPid() { pgrep -f $SERVICE_NAME } function checkPidAlive() { for i in `ls -t $APP_NAME/$APP_NAME.pid 2>/dev/null` do read pid < $i result=$(ps -p "$pid") if [ "$?" -eq 0 ]; then return 0 else printf "\npid - $pid just quit unexpectedly, please check logs under $LOG_DIR and /tmp for more information!\n" exit 1; fi done printf "\nNo pid file found, startup may failed. Please check logs under $LOG_DIR and /tmp for more information!\n" exit 1; } function existProcessUsePort() { if [ "$(curl -X GET --silent --connect-timeout 1 --max-time 2 --head $SERVER_URL | grep "HTTP")" != "" ]; then true else false fi } function isServiceRunning() { if [ "$(curl -X GET --silent --connect-timeout 1 --max-time 2 $SERVER_URL/health | grep "UP")" != "" ]; then true else false fi } if [ "$(uname)" == "Darwin" ]; then windows="0" elif [ "$(expr substr $(uname -s) 1 5)" == "Linux" ]; then windows="0" elif [ "$(expr substr $(uname -s) 1 5)" == "MINGW" ]; then windows="1" else windows="0" fi # for Windows if [ "$windows" == "1" ] && [[ -n "$JAVA_HOME" ]] && [[ -x "$JAVA_HOME/bin/java" ]]; then tmp_java_home=`cygpath -sw "$JAVA_HOME"` export JAVA_HOME=`cygpath -u $tmp_java_home` echo "Windows new JAVA_HOME is: $JAVA_HOME" fi # Find Java if [[ -n "$JAVA_HOME" ]] && [[ -x "$JAVA_HOME/bin/java" ]]; then javaexe="$JAVA_HOME/bin/java" elif type -p java > /dev/null 2>&1; then javaexe=$(type -p java) elif [[ -x "/usr/bin/java" ]]; then javaexe="/usr/bin/java" else echo "Unable to find Java" exit 1 fi if [[ "$javaexe" ]]; then version=$("$javaexe" -version 2>&1 | awk -F '"' '/version/ {print $2}') version=$(echo "$version" | awk -F. '{printf("%03d%03d",$1,$2);}') # now version is of format 009003 (9.3.x) if [ $version -ge 011000 ]; then JAVA_OPTS="$JAVA_OPTS -Xlog:gc*:$LOG_DIR/$SERVICE_NAME.gc.log:time,level,tags -Xlog:safepoint -Xlog:gc+heap=trace" elif [ $version -ge 010000 ]; then JAVA_OPTS="$JAVA_OPTS -Xlog:gc*:$LOG_DIR/$SERVICE_NAME.gc.log:time,level,tags -Xlog:safepoint -Xlog:gc+heap=trace" elif [ $version -ge 009000 ]; then JAVA_OPTS="$JAVA_OPTS -Xlog:gc*:$LOG_DIR/$SERVICE_NAME.gc.log:time,level,tags -Xlog:safepoint -Xlog:gc+heap=trace" else JAVA_OPTS="$JAVA_OPTS -XX:+UseParNewGC" JAVA_OPTS="$JAVA_OPTS -Xloggc:$LOG_DIR/$SERVICE_NAME.gc.log -XX:+PrintGCDetails" JAVA_OPTS="$JAVA_OPTS -XX:+UseConcMarkSweepGC -XX:+UseCMSCompactAtFullCollection -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=60 -XX:+CMSClassUnloadingEnabled -XX:+CMSParallelRemarkEnabled -XX:CMSFullGCsBeforeCompaction=9 -XX:+CMSClassUnloadingEnabled -XX:+PrintGCDateStamps -XX:+PrintGCApplicationConcurrentTime -XX:+PrintHeapAtGC -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=5M" fi fi cd `dirname $0`/.. for i in `ls $SERVICE_NAME-*.jar 2>/dev/null` do if [[ ! $i == *"-sources.jar" ]] then PATH_TO_JAR=$i break fi done if [[ ! -f $PATH_TO_JAR && -d current ]]; then cd current for i in `ls $SERVICE_NAME-*.jar 2>/dev/null` do if [[ ! $i == *"-sources.jar" ]] then PATH_TO_JAR=$i break fi done fi # For Docker environment, start in foreground mode if [[ -n "$APOLLO_RUN_MODE" ]] && [[ "$APOLLO_RUN_MODE" == "Docker" ]]; then exec $javaexe -Dsun.misc.URLClassPath.disableJarChecking=true $JAVA_OPTS -jar $PATH_TO_JAR else # before running check there is another process use port or not if existProcessUsePort; then if isServiceRunning; then echo "$(date) ==== $SERVICE_NAME is running already with port $SERVER_PORT, pid $(getPid)" exit 0 else echo "$(date) ==== $SERVICE_NAME failed to start. The port $SERVER_PORT already be in use by another process" echo "maybe you can figure out which process use port $SERVER_PORT by following ways:" echo "1. access http://change-to-this-machine-ip:$SERVER_PORT by browser" echo "2. run command 'curl $SERVER_URL'" echo "3. run command 'sudo netstat -tunlp | grep :$SERVER_PORT'" echo "4. run command 'sudo lsof -nP -iTCP:$SERVER_PORT -sTCP:LISTEN'" exit 1 fi fi printf "$(date) ==== $SERVICE_NAME Starting ==== \n" if [[ -f $SERVICE_NAME".jar" ]]; then rm -rf $SERVICE_NAME".jar" fi ln $PATH_TO_JAR $SERVICE_NAME".jar" chmod a+x $SERVICE_NAME".jar" ./$SERVICE_NAME".jar" start rc=$?; if [[ $rc != 0 ]]; then echo "$(date) Failed to start $SERVICE_NAME.jar, return code: $rc" exit $rc; fi declare -i counter=0 declare -i max_counter=48 # 48*5=240s declare -i total_time=0 printf "Waiting for server startup" until [[ (( counter -ge max_counter )) ]]; do printf "." sleep 5 counter+=1 total_time=$((counter*5)) checkPidAlive if isServiceRunning; then printf "\n$(date) Server started in $total_time seconds!\n" exit 0; fi done printf "\n$(date) Server failed to start in $total_time seconds!\n" exit 1; fi ================================================ FILE: apollo-configservice/src/test/java/com/ctrip/framework/apollo/ConfigServiceTestConfiguration.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo; import com.ctrip.framework.apollo.biz.auth.WebSecurityConfig; import com.ctrip.framework.apollo.configservice.ConfigServiceApplication; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.ComponentScan.Filter; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.FilterType; @Configuration @ComponentScan(excludeFilters = {@Filter(type = FilterType.ASSIGNABLE_TYPE, value = {LocalConfigServiceApplication.class, ConfigServiceApplication.class, WebSecurityConfig.class})}) @EnableAutoConfiguration public class ConfigServiceTestConfiguration { } ================================================ FILE: apollo-configservice/src/test/java/com/ctrip/framework/apollo/LocalConfigServiceApplication.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer; @SpringBootApplication @EnableEurekaServer public class LocalConfigServiceApplication { public static void main(String[] args) { new SpringApplicationBuilder(LocalConfigServiceApplication.class).run(args); } } ================================================ FILE: apollo-configservice/src/test/java/com/ctrip/framework/apollo/configservice/GracefulShutdownConfigurationTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.configservice; import com.ctrip.framework.apollo.ConfigServiceTestConfiguration; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; /** * Configuration validation test for graceful shutdown feature. * * This test verifies that the graceful shutdown configuration is properly loaded * from application.yml by checking ServerProperties and the web server lifecycle. * * Note: This test does NOT verify the actual behavior of graceful shutdown * (e.g., waiting for in-flight requests). Full behavioral testing requires: * - Integration tests with real HTTP requests during shutdown * - Manual testing in staging/production environments * - Monitoring of shutdown metrics and logs */ @RunWith(SpringJUnit4ClassRunner.class) @SpringBootTest(classes = ConfigServiceTestConfiguration.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class GracefulShutdownConfigurationTest { @Autowired private ServletWebServerApplicationContext webServerAppContext; @Autowired private ServerProperties serverProperties; @Test public void testGracefulShutdownIsConfigured() { assertNotNull("WebServer should be available", webServerAppContext); assertTrue("Server should be running", webServerAppContext.getWebServer().getPort() > 0); // Verify graceful shutdown is enabled in application.yml assertEquals("Graceful shutdown should be enabled in application.yml", "graceful", serverProperties.getShutdown().name().toLowerCase()); // Verify the lifecycle processor exists (indicates graceful shutdown is enabled) assertNotNull("Lifecycle processor should be present for graceful shutdown", webServerAppContext.getBean("lifecycleProcessor")); } } ================================================ FILE: apollo-configservice/src/test/java/com/ctrip/framework/apollo/configservice/controller/ConfigControllerTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.configservice.controller; import com.ctrip.framework.apollo.biz.config.BizConfig; import com.ctrip.framework.apollo.biz.entity.Release; import com.ctrip.framework.apollo.common.entity.AppNamespace; import com.ctrip.framework.apollo.configservice.service.AppNamespaceServiceWithCache; import com.ctrip.framework.apollo.configservice.service.config.ConfigService; import com.ctrip.framework.apollo.configservice.service.config.IncrementalSyncService; import com.ctrip.framework.apollo.configservice.util.InstanceConfigAuditUtil; import com.ctrip.framework.apollo.configservice.util.NamespaceUtil; import com.ctrip.framework.apollo.core.ConfigConsts; import com.ctrip.framework.apollo.core.dto.ApolloConfig; import com.ctrip.framework.apollo.core.dto.ApolloNotificationMessages; import com.ctrip.framework.apollo.core.dto.ConfigurationChange; import com.ctrip.framework.apollo.core.enums.ConfigSyncType; import com.google.common.base.Joiner; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import com.google.gson.Gson; import com.google.gson.JsonSyntaxException; import com.google.gson.reflect.TypeToken; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.List; import java.util.Map; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import static org.mockito.Mockito.*; /** * @author Jason Song(song_s@ctrip.com) */ @RunWith(MockitoJUnitRunner.class) public class ConfigControllerTest { private ConfigController configController; @Mock private ConfigService configService; @Mock private IncrementalSyncService incrementalSyncService; @Mock private AppNamespaceServiceWithCache appNamespaceService; @Mock private BizConfig bizConfig; private String someAppId; private String someClusterName; private String defaultClusterName; private String defaultNamespaceName; private String somePublicNamespaceName; private String someDataCenter; private String someClientIp; private String someClientLabel; private String someMessagesAsString; @Mock private ApolloNotificationMessages someNotificationMessages; @Mock private Release someRelease; @Mock private Release somePublicRelease; @Mock private Release anotherPublicRelease; @Mock private Release anotherRelease; @Mock private NamespaceUtil namespaceUtil; @Mock private InstanceConfigAuditUtil instanceConfigAuditUtil; @Mock private HttpServletRequest someRequest; private Gson gson = new Gson(); @Before public void setUp() throws Exception { configController = spy(new ConfigController(configService, incrementalSyncService, appNamespaceService, namespaceUtil, instanceConfigAuditUtil, gson, bizConfig)); someAppId = "1"; someClusterName = "someClusterName"; defaultClusterName = ConfigConsts.CLUSTER_NAME_DEFAULT; defaultNamespaceName = ConfigConsts.NAMESPACE_APPLICATION; somePublicNamespaceName = "somePublicNamespace"; someDataCenter = "someDC"; someClientIp = "someClientIp"; someClientLabel = "someClientLabel"; String someValidConfiguration = "{\"apollo.bar\": \"foo\"}"; String somePublicConfiguration = "{\"apollo.public.bar\": \"foo\"}"; when(someRelease.getAppId()).thenReturn(someAppId); when(someRelease.getClusterName()).thenReturn(someClusterName); when(someRelease.getConfigurations()).thenReturn(someValidConfiguration); when(somePublicRelease.getConfigurations()).thenReturn(somePublicConfiguration); when(namespaceUtil.filterNamespaceName(defaultNamespaceName)).thenReturn(defaultNamespaceName); when(namespaceUtil.filterNamespaceName(somePublicNamespaceName)) .thenReturn(somePublicNamespaceName); when(namespaceUtil.normalizeNamespace(someAppId, defaultNamespaceName)) .thenReturn(defaultNamespaceName); when(namespaceUtil.normalizeNamespace(someAppId, somePublicNamespaceName)) .thenReturn(somePublicNamespaceName); someMessagesAsString = "someValidJson"; when(configController.transformMessages(someMessagesAsString)) .thenReturn(someNotificationMessages); } @Test public void testQueryConfig() throws Exception { String someClientSideReleaseKey = "1"; String someServerSideNewReleaseKey = "2"; HttpServletResponse someResponse = mock(HttpServletResponse.class); when(configService.loadConfig(someAppId, someClientIp, someClientLabel, someAppId, someClusterName, defaultNamespaceName, someDataCenter, someNotificationMessages)) .thenReturn(someRelease); when(someRelease.getReleaseKey()).thenReturn(someServerSideNewReleaseKey); when(someRelease.getNamespaceName()).thenReturn(defaultNamespaceName); ApolloConfig result = configController.queryConfig(someAppId, someClusterName, defaultNamespaceName, someDataCenter, someClientSideReleaseKey, someClientIp, someClientLabel, someMessagesAsString, someRequest, someResponse); verify(configService, times(1)).loadConfig(someAppId, someClientIp, someClientLabel, someAppId, someClusterName, defaultNamespaceName, someDataCenter, someNotificationMessages); assertEquals(someAppId, result.getAppId()); assertEquals(someClusterName, result.getCluster()); assertEquals(defaultNamespaceName, result.getNamespaceName()); assertEquals(someServerSideNewReleaseKey, result.getReleaseKey()); verify(instanceConfigAuditUtil, times(1)).audit(someAppId, someClusterName, someDataCenter, someClientIp, someAppId, someClusterName, defaultNamespaceName, someServerSideNewReleaseKey); } @Test public void testQueryConfigFile() throws Exception { String someClientSideReleaseKey = "1"; String someServerSideNewReleaseKey = "2"; HttpServletResponse someResponse = mock(HttpServletResponse.class); String someNamespaceName = String.format("%s.%s", defaultClusterName, "properties"); when(configService.loadConfig(someAppId, someClientIp, someClientLabel, someAppId, someClusterName, defaultNamespaceName, someDataCenter, someNotificationMessages)) .thenReturn(someRelease); when(someRelease.getReleaseKey()).thenReturn(someServerSideNewReleaseKey); when(namespaceUtil.filterNamespaceName(someNamespaceName)).thenReturn(defaultNamespaceName); when(namespaceUtil.normalizeNamespace(someAppId, defaultNamespaceName)) .thenReturn(defaultNamespaceName); ApolloConfig result = configController.queryConfig(someAppId, someClusterName, someNamespaceName, someDataCenter, someClientSideReleaseKey, someClientIp, someClientLabel, someMessagesAsString, someRequest, someResponse); verify(configService, times(1)).loadConfig(someAppId, someClientIp, someClientLabel, someAppId, someClusterName, defaultNamespaceName, someDataCenter, someNotificationMessages); assertEquals(someAppId, result.getAppId()); assertEquals(someClusterName, result.getCluster()); assertEquals(someNamespaceName, result.getNamespaceName()); assertEquals(someServerSideNewReleaseKey, result.getReleaseKey()); } @Test public void testQueryConfigFileWithPrivateNamespace() throws Exception { String someClientSideReleaseKey = "1"; String someServerSideNewReleaseKey = "2"; String somePrivateNamespace = "datasource"; HttpServletResponse someResponse = mock(HttpServletResponse.class); String somePrivateNamespaceName = String.format("%s.%s", somePrivateNamespace, "xml"); AppNamespace appNamespace = mock(AppNamespace.class); when(configService.loadConfig(someAppId, someClientIp, someClientLabel, someAppId, someClusterName, somePrivateNamespace, someDataCenter, someNotificationMessages)) .thenReturn(someRelease); when(someRelease.getReleaseKey()).thenReturn(someServerSideNewReleaseKey); when(namespaceUtil.filterNamespaceName(somePrivateNamespaceName)) .thenReturn(somePrivateNamespace); when(namespaceUtil.normalizeNamespace(someAppId, somePrivateNamespace)) .thenReturn(somePrivateNamespace); when(appNamespaceService.findByAppIdAndNamespace(someAppId, somePrivateNamespace)) .thenReturn(appNamespace); ApolloConfig result = configController.queryConfig(someAppId, someClusterName, somePrivateNamespaceName, someDataCenter, someClientSideReleaseKey, someClientIp, someClientLabel, someMessagesAsString, someRequest, someResponse); assertEquals(someAppId, result.getAppId()); assertEquals(someClusterName, result.getCluster()); assertEquals(somePrivateNamespaceName, result.getNamespaceName()); assertEquals(someServerSideNewReleaseKey, result.getReleaseKey()); } @Test public void testQueryConfigWithReleaseNotFound() throws Exception { String someClientSideReleaseKey = "1"; HttpServletResponse someResponse = mock(HttpServletResponse.class); when(configService.loadConfig(someAppId, someClientIp, someClientLabel, someAppId, someClusterName, defaultNamespaceName, someDataCenter, someNotificationMessages)) .thenReturn(null); ApolloConfig result = configController.queryConfig(someAppId, someClusterName, defaultNamespaceName, someDataCenter, someClientSideReleaseKey, someClientIp, someClientLabel, someMessagesAsString, someRequest, someResponse); assertNull(result); verify(someResponse, times(1)).sendError(eq(HttpServletResponse.SC_NOT_FOUND), anyString()); } @Test public void testQueryConfigWithApolloConfigNotModified() throws Exception { String someClientSideReleaseKey = "1"; String someServerSideReleaseKey = someClientSideReleaseKey; HttpServletResponse someResponse = mock(HttpServletResponse.class); when(configService.loadConfig(someAppId, someClientIp, someClientLabel, someAppId, someClusterName, defaultNamespaceName, someDataCenter, someNotificationMessages)) .thenReturn(someRelease); when(someRelease.getReleaseKey()).thenReturn(someServerSideReleaseKey); ApolloConfig result = configController.queryConfig(someAppId, someClusterName, defaultNamespaceName, someDataCenter, someClientSideReleaseKey, someClientIp, someClientLabel, someMessagesAsString, someRequest, someResponse); assertNull(result); verify(someResponse, times(1)).setStatus(HttpServletResponse.SC_NOT_MODIFIED); } @Test public void testQueryConfigWithAppOwnNamespace() throws Exception { String someClientSideReleaseKey = "1"; String someServerSideReleaseKey = "2"; String someAppOwnNamespaceName = "someAppOwn"; HttpServletResponse someResponse = mock(HttpServletResponse.class); AppNamespace someAppOwnNamespace = assemblePublicAppNamespace(someAppId, someAppOwnNamespaceName); when(configService.loadConfig(someAppId, someClientIp, someClientLabel, someAppId, someClusterName, someAppOwnNamespaceName, someDataCenter, someNotificationMessages)) .thenReturn(someRelease); when(appNamespaceService.findPublicNamespaceByName(someAppOwnNamespaceName)) .thenReturn(someAppOwnNamespace); when(someRelease.getReleaseKey()).thenReturn(someServerSideReleaseKey); when(namespaceUtil.filterNamespaceName(someAppOwnNamespaceName)) .thenReturn(someAppOwnNamespaceName); when(namespaceUtil.normalizeNamespace(someAppId, someAppOwnNamespaceName)) .thenReturn(someAppOwnNamespaceName); ApolloConfig result = configController.queryConfig(someAppId, someClusterName, someAppOwnNamespaceName, someDataCenter, someClientSideReleaseKey, someClientIp, someClientLabel, someMessagesAsString, someRequest, someResponse); assertEquals(someServerSideReleaseKey, result.getReleaseKey()); assertEquals(someAppId, result.getAppId()); assertEquals(someClusterName, result.getCluster()); assertEquals(someAppOwnNamespaceName, result.getNamespaceName()); assertEquals("foo", result.getConfigurations().get("apollo.bar")); } @Test public void testQueryConfigWithPubicNamespaceAndNoAppOverride() throws Exception { String someClientSideReleaseKey = "1"; String someServerSideReleaseKey = "2"; HttpServletResponse someResponse = mock(HttpServletResponse.class); String somePublicAppId = "somePublicAppId"; String somePublicClusterName = "somePublicClusterName"; AppNamespace somePublicAppNamespace = assemblePublicAppNamespace(somePublicAppId, somePublicNamespaceName); when(configService.loadConfig(someAppId, someClientIp, someClientLabel, someAppId, someClusterName, somePublicNamespaceName, someDataCenter, someNotificationMessages)) .thenReturn(null); when(appNamespaceService.findPublicNamespaceByName(somePublicNamespaceName)) .thenReturn(somePublicAppNamespace); when(configService.loadConfig(someAppId, someClientIp, someClientLabel, somePublicAppId, someClusterName, somePublicNamespaceName, someDataCenter, someNotificationMessages)) .thenReturn(somePublicRelease); when(somePublicRelease.getReleaseKey()).thenReturn(someServerSideReleaseKey); when(somePublicRelease.getAppId()).thenReturn(somePublicAppId); when(somePublicRelease.getClusterName()).thenReturn(somePublicClusterName); when(somePublicRelease.getNamespaceName()).thenReturn(somePublicNamespaceName); ApolloConfig result = configController.queryConfig(someAppId, someClusterName, somePublicNamespaceName, someDataCenter, someClientSideReleaseKey, someClientIp, someClientLabel, someMessagesAsString, someRequest, someResponse); assertEquals(someServerSideReleaseKey, result.getReleaseKey()); assertEquals(someAppId, result.getAppId()); assertEquals(someClusterName, result.getCluster()); assertEquals(somePublicNamespaceName, result.getNamespaceName()); assertEquals("foo", result.getConfigurations().get("apollo.public.bar")); verify(instanceConfigAuditUtil, times(1)).audit(someAppId, someClusterName, someDataCenter, someClientIp, somePublicAppId, somePublicClusterName, somePublicNamespaceName, someServerSideReleaseKey); } @Test public void testQueryConfigFileWithPublicNamespaceAndNoAppOverride() throws Exception { String someClientSideReleaseKey = "1"; String someServerSideReleaseKey = "2"; HttpServletResponse someResponse = mock(HttpServletResponse.class); String somePublicAppId = "somePublicAppId"; String someNamespace = String.format("%s.%s", somePublicNamespaceName, "properties"); AppNamespace somePublicAppNamespace = assemblePublicAppNamespace(somePublicAppId, somePublicNamespaceName); when(configService.loadConfig(someAppId, someClientIp, someClientLabel, someAppId, someClusterName, somePublicNamespaceName, someDataCenter, someNotificationMessages)) .thenReturn(null); when(appNamespaceService.findPublicNamespaceByName(somePublicNamespaceName)) .thenReturn(somePublicAppNamespace); when(configService.loadConfig(someAppId, someClientIp, someClientLabel, somePublicAppId, someClusterName, somePublicNamespaceName, someDataCenter, someNotificationMessages)) .thenReturn(somePublicRelease); when(somePublicRelease.getReleaseKey()).thenReturn(someServerSideReleaseKey); when(namespaceUtil.filterNamespaceName(someNamespace)).thenReturn(somePublicNamespaceName); when(namespaceUtil.normalizeNamespace(someAppId, somePublicNamespaceName)) .thenReturn(somePublicNamespaceName); when(appNamespaceService.findByAppIdAndNamespace(someAppId, somePublicNamespaceName)) .thenReturn(null); ApolloConfig result = configController.queryConfig(someAppId, someClusterName, someNamespace, someDataCenter, someClientSideReleaseKey, someClientIp, someClientLabel, someMessagesAsString, someRequest, someResponse); assertEquals(someServerSideReleaseKey, result.getReleaseKey()); assertEquals(someAppId, result.getAppId()); assertEquals(someClusterName, result.getCluster()); assertEquals(someNamespace, result.getNamespaceName()); assertEquals("foo", result.getConfigurations().get("apollo.public.bar")); } @Test public void testQueryConfigWithPublicNamespaceAndAppOverride() throws Exception { String someAppSideReleaseKey = "1"; String somePublicAppSideReleaseKey = "2"; HttpServletResponse someResponse = mock(HttpServletResponse.class); String somePublicAppId = "somePublicAppId"; AppNamespace somePublicAppNamespace = assemblePublicAppNamespace(somePublicAppId, somePublicNamespaceName); when(someRelease.getConfigurations()).thenReturn("{\"apollo.public.foo\": \"foo-override\"}"); when(somePublicRelease.getConfigurations()) .thenReturn("{\"apollo.public.foo\": \"foo\", \"apollo.public.bar\": \"bar\"}"); when(configService.loadConfig(someAppId, someClientIp, someClientLabel, someAppId, someClusterName, somePublicNamespaceName, someDataCenter, someNotificationMessages)) .thenReturn(someRelease); when(someRelease.getReleaseKey()).thenReturn(someAppSideReleaseKey); when(someRelease.getNamespaceName()).thenReturn(somePublicNamespaceName); when(appNamespaceService.findPublicNamespaceByName(somePublicNamespaceName)) .thenReturn(somePublicAppNamespace); when(configService.loadConfig(someAppId, someClientIp, someClientLabel, somePublicAppId, someClusterName, somePublicNamespaceName, someDataCenter, someNotificationMessages)) .thenReturn(somePublicRelease); when(somePublicRelease.getReleaseKey()).thenReturn(somePublicAppSideReleaseKey); when(somePublicRelease.getAppId()).thenReturn(somePublicAppId); when(somePublicRelease.getClusterName()).thenReturn(someDataCenter); when(somePublicRelease.getNamespaceName()).thenReturn(somePublicNamespaceName); ApolloConfig result = configController.queryConfig(someAppId, someClusterName, somePublicNamespaceName, someDataCenter, someAppSideReleaseKey, someClientIp, someClientLabel, someMessagesAsString, someRequest, someResponse); assertEquals(Joiner.on(ConfigConsts.CLUSTER_NAMESPACE_SEPARATOR).join(someAppSideReleaseKey, somePublicAppSideReleaseKey), result.getReleaseKey()); assertEquals(someAppId, result.getAppId()); assertEquals(someClusterName, result.getCluster()); assertEquals(somePublicNamespaceName, result.getNamespaceName()); assertEquals("foo-override", result.getConfigurations().get("apollo.public.foo")); assertEquals("bar", result.getConfigurations().get("apollo.public.bar")); verify(instanceConfigAuditUtil, times(1)).audit(someAppId, someClusterName, someDataCenter, someClientIp, someAppId, someClusterName, somePublicNamespaceName, someAppSideReleaseKey); verify(instanceConfigAuditUtil, times(1)).audit(someAppId, someClusterName, someDataCenter, someClientIp, somePublicAppId, someDataCenter, somePublicNamespaceName, somePublicAppSideReleaseKey); } @Test public void testMergeConfigurations() throws Exception { Gson gson = new Gson(); String key1 = "key1"; String value1 = "value1"; String anotherValue1 = "anotherValue1"; String key2 = "key2"; String value2 = "value2"; Map config = ImmutableMap.of(key1, anotherValue1); Map anotherConfig = ImmutableMap.of(key1, value1, key2, value2); Release releaseWithHighPriority = new Release(); releaseWithHighPriority.setConfigurations(gson.toJson(config)); Release releaseWithLowPriority = new Release(); releaseWithLowPriority.setConfigurations(gson.toJson(anotherConfig)); Map result = configController.mergeReleaseConfigurations( Lists.newArrayList(releaseWithHighPriority, releaseWithLowPriority)); assertEquals(2, result.keySet().size()); assertEquals(anotherValue1, result.get(key1)); assertEquals(value2, result.get(key2)); } @Test(expected = JsonSyntaxException.class) public void testTransformConfigurationToMapFailed() throws Exception { String someInvalidConfiguration = "xxx"; Release someRelease = new Release(); someRelease.setConfigurations(someInvalidConfiguration); configController.mergeReleaseConfigurations(Lists.newArrayList(someRelease)); } @Test public void testQueryConfigForNoAppIdPlaceHolder() throws Exception { String someClientSideReleaseKey = "1"; HttpServletResponse someResponse = mock(HttpServletResponse.class); String appId = ConfigConsts.NO_APPID_PLACEHOLDER; ApolloConfig result = configController.queryConfig(appId, someClusterName, defaultNamespaceName, someDataCenter, someClientSideReleaseKey, someClientIp, someClientLabel, someMessagesAsString, someRequest, someResponse); verify(configService, never()).loadConfig(appId, someClientIp, someAppId, someClientLabel, someClusterName, defaultNamespaceName, someDataCenter, someNotificationMessages); verify(appNamespaceService, never()).findPublicNamespaceByName(defaultNamespaceName); assertNull(result); verify(someResponse, times(1)).sendError(eq(HttpServletResponse.SC_NOT_FOUND), anyString()); } @Test public void testQueryConfigForNoAppIdPlaceHolderWithPublicNamespace() throws Exception { String someClientSideReleaseKey = "1"; String someServerSideReleaseKey = "2"; HttpServletResponse someResponse = mock(HttpServletResponse.class); String somePublicAppId = "somePublicAppId"; AppNamespace somePublicAppNamespace = assemblePublicAppNamespace(somePublicAppId, somePublicNamespaceName); String appId = ConfigConsts.NO_APPID_PLACEHOLDER; when(appNamespaceService.findPublicNamespaceByName(somePublicNamespaceName)) .thenReturn(somePublicAppNamespace); when(configService.loadConfig(appId, someClientIp, someClientLabel, somePublicAppId, someClusterName, somePublicNamespaceName, someDataCenter, someNotificationMessages)) .thenReturn(somePublicRelease); when(somePublicRelease.getReleaseKey()).thenReturn(someServerSideReleaseKey); when(namespaceUtil.normalizeNamespace(appId, somePublicNamespaceName)) .thenReturn(somePublicNamespaceName); ApolloConfig result = configController.queryConfig(appId, someClusterName, somePublicNamespaceName, someDataCenter, someClientSideReleaseKey, someClientIp, someClientLabel, someMessagesAsString, someRequest, someResponse); verify(configService, never()).loadConfig(appId, someClientIp, someClientLabel, appId, someClusterName, somePublicNamespaceName, someDataCenter, someNotificationMessages); assertEquals(someServerSideReleaseKey, result.getReleaseKey()); assertEquals(appId, result.getAppId()); assertEquals(someClusterName, result.getCluster()); assertEquals(somePublicNamespaceName, result.getNamespaceName()); assertEquals("foo", result.getConfigurations().get("apollo.public.bar")); } @Test public void testTransformMessages() throws Exception { String someKey = "someKey"; long someNotificationId = 1; String anotherKey = "anotherKey"; long anotherNotificationId = 2; ApolloNotificationMessages notificationMessages = new ApolloNotificationMessages(); notificationMessages.put(someKey, someNotificationId); notificationMessages.put(anotherKey, anotherNotificationId); String someMessagesAsString = gson.toJson(notificationMessages); ApolloNotificationMessages result = configController.transformMessages(someMessagesAsString); assertEquals(notificationMessages.getDetails(), result.getDetails()); } @Test public void testTransformInvalidMessages() throws Exception { String someInvalidMessages = "someInvalidMessages"; assertNull(configController.transformMessages(someInvalidMessages)); } private AppNamespace assemblePublicAppNamespace(String appId, String namespace) { return assembleAppNamespace(appId, namespace, true); } private AppNamespace assembleAppNamespace(String appId, String namespace, boolean isPublic) { AppNamespace appNamespace = new AppNamespace(); appNamespace.setAppId(appId); appNamespace.setName(namespace); appNamespace.setPublic(isPublic); return appNamespace; } @Test public void testQueryConfigWithIncrementalSync() throws Exception { when(bizConfig.isConfigServiceIncrementalChangeEnabled()) .thenReturn(true); String clientSideReleaseKey = "1"; String someConfigurations = "{\"apollo.public.foo\": \"foo\"}"; HttpServletResponse someResponse = mock(HttpServletResponse.class); ImmutableMap someReleaseMap = mock(ImmutableMap.class); String someServerSideNewReleaseKey = "2"; String anotherConfigurations = "{\"apollo.public.foo\": \"foo\", \"apollo.public.bar\": \"bar\"}"; when(configService.findReleasesByReleaseKeys(Sets.newHashSet(clientSideReleaseKey))).thenReturn( someReleaseMap); when(someReleaseMap.get(clientSideReleaseKey)).thenReturn(someRelease); when(someRelease.getConfigurations()).thenReturn(someConfigurations); when(configService.loadConfig(someAppId, someClientIp, someClientLabel, someAppId, someClusterName, defaultNamespaceName, someDataCenter, someNotificationMessages)).thenReturn(anotherRelease); when(anotherRelease.getNamespaceName()).thenReturn(defaultNamespaceName); when(anotherRelease.getConfigurations()).thenReturn(anotherConfigurations); when(anotherRelease.getReleaseKey()).thenReturn(someServerSideNewReleaseKey); List configurationChanges = new ArrayList<>(); configurationChanges.add(new ConfigurationChange("apollo.public.bar", "bar", "ADDED")); when(incrementalSyncService.getConfigurationChanges(someServerSideNewReleaseKey, gson.fromJson(anotherConfigurations, configurationTypeReference), clientSideReleaseKey, gson.fromJson(someConfigurations, configurationTypeReference))) .thenReturn(configurationChanges); ApolloConfig anotherResult = configController.queryConfig(someAppId, someClusterName, defaultNamespaceName, someDataCenter, clientSideReleaseKey, someClientIp, someClientLabel, someMessagesAsString, someRequest, someResponse); assertEquals(ConfigSyncType.INCREMENTAL_SYNC.getValue(), anotherResult.getConfigSyncType()); assertEquals(configurationChanges, anotherResult.getConfigurationChanges()); } @Test public void testQueryConfigWithIncrementalSyncNotFound() throws Exception { when(bizConfig.isConfigServiceIncrementalChangeEnabled()) .thenReturn(true); String someClientSideReleaseKey = "1"; String someServerSideNewReleaseKey = "2"; HttpServletResponse someResponse = mock(HttpServletResponse.class); when(configService.loadConfig(someAppId, someClientIp, someClientLabel, someAppId, someClusterName, defaultNamespaceName, someDataCenter, someNotificationMessages)).thenReturn(someRelease); when(configService.findReleasesByReleaseKeys( Sets.newHashSet(someClientSideReleaseKey))).thenReturn(null); when(someRelease.getReleaseKey()).thenReturn(someServerSideNewReleaseKey); when(someRelease.getNamespaceName()).thenReturn(defaultNamespaceName); String configurations = "{\"apollo.public.foo\": \"foo\"}"; when(someRelease.getConfigurations()).thenReturn(configurations); ApolloConfig result = configController.queryConfig(someAppId, someClusterName, defaultNamespaceName, someDataCenter, someClientSideReleaseKey, someClientIp, someClientLabel, someMessagesAsString, someRequest, someResponse); assertEquals(1, result.getConfigurations().size()); assertEquals("foo", result.getConfigurations().get("apollo.public.foo")); } @Test public void testQueryConfigWithIncrementalSyncPublicNamespaceAndAppOverride() throws Exception { when(bizConfig.isConfigServiceIncrementalChangeEnabled()) .thenReturn(true); String someAppClientSideReleaseKey = "1"; String somePublicAppClientSideReleaseKey = "2"; String someConfigurations = "{\"apollo.public.foo.client\": \"foo.override\"}"; String somePublicConfigurations = "{\"apollo.public.foo.client\": \"foo\"}"; ImmutableMap someReleaseMap = mock(ImmutableMap.class); Release somePublicRelease = mock(Release.class); when(configService.findReleasesByReleaseKeys(Sets.newHashSet(someAppClientSideReleaseKey, somePublicAppClientSideReleaseKey))).thenReturn(someReleaseMap); when(someReleaseMap.get(someAppClientSideReleaseKey)).thenReturn(someRelease); when(someReleaseMap.get(somePublicAppClientSideReleaseKey)).thenReturn(somePublicRelease); when(someRelease.getConfigurations()).thenReturn(someConfigurations); when(somePublicRelease.getConfigurations()).thenReturn(somePublicConfigurations); String someAppServerSideReleaseKey = "3"; String somePublicAppServerSideReleaseKey = "4"; HttpServletResponse someResponse = mock(HttpServletResponse.class); String somePublicAppId = "somePublicAppId"; AppNamespace somePublicAppNamespace = assemblePublicAppNamespace(somePublicAppId, somePublicNamespaceName); when(anotherRelease.getConfigurations()).thenReturn( "{\"apollo.public.foo\": \"foo-override\"}"); when(anotherPublicRelease.getConfigurations()) .thenReturn("{\"apollo.public.foo\": \"foo\", \"apollo.public.bar\": \"bar\"}"); when(configService.loadConfig(someAppId, someClientIp, someClientLabel, someAppId, someClusterName, somePublicNamespaceName, someDataCenter, someNotificationMessages)).thenReturn(anotherRelease); when(anotherRelease.getReleaseKey()).thenReturn(someAppServerSideReleaseKey); when(anotherRelease.getNamespaceName()).thenReturn(somePublicNamespaceName); when(appNamespaceService.findPublicNamespaceByName(somePublicNamespaceName)) .thenReturn(somePublicAppNamespace); when(configService.loadConfig(someAppId, someClientIp, someClientLabel, somePublicAppId, someClusterName, somePublicNamespaceName, someDataCenter, someNotificationMessages)).thenReturn(anotherPublicRelease); when(anotherPublicRelease.getReleaseKey()).thenReturn(somePublicAppServerSideReleaseKey); when(anotherPublicRelease.getAppId()).thenReturn(somePublicAppId); when(anotherPublicRelease.getClusterName()).thenReturn(someDataCenter); when(anotherPublicRelease.getNamespaceName()).thenReturn(somePublicNamespaceName); String mergeServerSideConfigurations = "{\"apollo.public.bar\": \"bar\",\"apollo.public.foo\": \"foo-override\"}"; String mergeServerSideReleaseKey = Joiner.on(ConfigConsts.CLUSTER_NAMESPACE_SEPARATOR) .join(someAppServerSideReleaseKey, somePublicAppServerSideReleaseKey); String mergeClientSideConfigurations = "{\"apollo.public.foo.client\": \"foo.override\"}"; String mergeClientSideReleaseKey = Joiner.on(ConfigConsts.CLUSTER_NAMESPACE_SEPARATOR) .join(someAppClientSideReleaseKey, somePublicAppClientSideReleaseKey); List configurationChanges = new ArrayList<>(); configurationChanges.add(new ConfigurationChange("apollo.public.bar", "bar", "ADDED")); configurationChanges.add(new ConfigurationChange("apollo.public.foo", "foo-override", "ADDED")); configurationChanges.add(new ConfigurationChange("apollo.public.foo.client", null, "DELETED")); when(incrementalSyncService.getConfigurationChanges(mergeServerSideReleaseKey, gson.fromJson(mergeServerSideConfigurations, configurationTypeReference), mergeClientSideReleaseKey, gson.fromJson(mergeClientSideConfigurations, configurationTypeReference))) .thenReturn(configurationChanges); ApolloConfig result = configController.queryConfig(someAppId, someClusterName, somePublicNamespaceName, someDataCenter, mergeClientSideReleaseKey, someClientIp, someClientLabel, someMessagesAsString, someRequest, someResponse); assertEquals(mergeServerSideReleaseKey, result.getReleaseKey()); assertEquals(ConfigSyncType.INCREMENTAL_SYNC.getValue(), result.getConfigSyncType()); assertEquals(configurationChanges, result.getConfigurationChanges()); } private static final Type configurationTypeReference = new TypeToken>() {}.getType(); } ================================================ FILE: apollo-configservice/src/test/java/com/ctrip/framework/apollo/configservice/controller/ConfigFileControllerTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.configservice.controller; import com.ctrip.framework.apollo.biz.entity.ReleaseMessage; import com.ctrip.framework.apollo.biz.grayReleaseRule.GrayReleaseRulesHolder; import com.ctrip.framework.apollo.biz.message.Topics; import com.ctrip.framework.apollo.configservice.util.NamespaceUtil; import com.ctrip.framework.apollo.configservice.util.WatchKeysUtil; import com.ctrip.framework.apollo.core.dto.ApolloConfig; import com.google.common.cache.Cache; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; import com.google.common.collect.Multimap; import com.google.common.collect.Sets; import com.google.common.reflect.TypeToken; import com.google.gson.Gson; import java.lang.reflect.Type; import java.util.Map; import java.util.Set; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.test.util.ReflectionTestUtils; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.startsWith; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; /** * @author Jason Song(song_s@ctrip.com) */ @RunWith(MockitoJUnitRunner.class) public class ConfigFileControllerTest { @Mock private ConfigController configController; @Mock private WatchKeysUtil watchKeysUtil; @Mock private NamespaceUtil namespaceUtil; @Mock private GrayReleaseRulesHolder grayReleaseRulesHolder; private ConfigFileController configFileController; private String someAppId; private String someClusterName; private String someNamespace; private String someDataCenter; private String someClientIp; private String someClientLabel; @Mock private HttpServletResponse someResponse; @Mock private HttpServletRequest someRequest; Multimap watchedKeys2CacheKey; Multimap cacheKey2WatchedKeys; private static final Gson GSON = new Gson(); @Before public void setUp() throws Exception { configFileController = new ConfigFileController(configController, namespaceUtil, watchKeysUtil, grayReleaseRulesHolder); someAppId = "someAppId"; someClusterName = "someClusterName"; someNamespace = "someNamespace"; someDataCenter = "someDataCenter"; someClientIp = "10.1.1.1"; someClientLabel = "myLabel"; when(namespaceUtil.filterNamespaceName(startsWith(someNamespace))).thenReturn(someNamespace); when(namespaceUtil.normalizeNamespace(someAppId, someNamespace)).thenReturn(someNamespace); when(grayReleaseRulesHolder.hasGrayReleaseRule(anyString(), anyString(), anyString(), anyString())).thenReturn(false); watchedKeys2CacheKey = (Multimap) ReflectionTestUtils .getField(configFileController, "watchedKeys2CacheKey"); cacheKey2WatchedKeys = (Multimap) ReflectionTestUtils .getField(configFileController, "cacheKey2WatchedKeys"); } @Test public void testQueryConfigAsProperties() throws Exception { String someKey = "someKey"; String someValue = "someValue"; String anotherKey = "anotherKey"; String anotherValue = "anotherValue"; String someWatchKey = "someWatchKey"; String anotherWatchKey = "anotherWatchKey"; Set watchKeys = Sets.newHashSet(someWatchKey, anotherWatchKey); String cacheKey = configFileController.assembleCacheKey( ConfigFileController.ConfigFileOutputFormat.PROPERTIES, someAppId, someClusterName, someNamespace, someDataCenter); Map configurations = ImmutableMap.of(someKey, someValue, anotherKey, anotherValue); ApolloConfig someApolloConfig = mock(ApolloConfig.class); when(someApolloConfig.getConfigurations()).thenReturn(configurations); when(configController.queryConfig(someAppId, someClusterName, someNamespace, someDataCenter, "-1", someClientIp, someClientLabel, null, someRequest, someResponse)) .thenReturn(someApolloConfig); when(watchKeysUtil.assembleAllWatchKeys(someAppId, someClusterName, someNamespace, someDataCenter)).thenReturn(watchKeys); ResponseEntity response = configFileController.queryConfigAsProperties(someAppId, someClusterName, someNamespace, someDataCenter, someClientIp, someClientLabel, someRequest, someResponse); assertEquals(2, watchedKeys2CacheKey.size()); assertEquals(2, cacheKey2WatchedKeys.size()); assertTrue(watchedKeys2CacheKey.containsEntry(someWatchKey, cacheKey)); assertTrue(watchedKeys2CacheKey.containsEntry(anotherWatchKey, cacheKey)); assertTrue(cacheKey2WatchedKeys.containsEntry(cacheKey, someWatchKey)); assertTrue(cacheKey2WatchedKeys.containsEntry(cacheKey, anotherWatchKey)); assertEquals(HttpStatus.OK, response.getStatusCode()); assertTrue(response.getBody().contains(String.format("%s=%s", someKey, someValue))); assertTrue(response.getBody().contains(String.format("%s=%s", anotherKey, anotherValue))); ResponseEntity anotherResponse = configFileController.queryConfigAsProperties(someAppId, someClusterName, someNamespace, someDataCenter, someClientIp, someClientLabel, someRequest, someResponse); assertEquals(response, anotherResponse); verify(configController, times(1)).queryConfig(someAppId, someClusterName, someNamespace, someDataCenter, "-1", someClientIp, someClientLabel, null, someRequest, someResponse); } @Test public void testQueryConfigAsJson() throws Exception { String someKey = "someKey"; String someValue = "someValue"; Type responseType = new TypeToken>() {}.getType(); String someWatchKey = "someWatchKey"; Set watchKeys = Sets.newHashSet(someWatchKey); Map configurations = ImmutableMap.of(someKey, someValue); ApolloConfig someApolloConfig = mock(ApolloConfig.class); when(configController.queryConfig(someAppId, someClusterName, someNamespace, someDataCenter, "-1", someClientIp, someClientLabel, null, someRequest, someResponse)) .thenReturn(someApolloConfig); when(someApolloConfig.getConfigurations()).thenReturn(configurations); when(watchKeysUtil.assembleAllWatchKeys(someAppId, someClusterName, someNamespace, someDataCenter)).thenReturn(watchKeys); ResponseEntity response = configFileController.queryConfigAsJson(someAppId, someClusterName, someNamespace, someDataCenter, someClientIp, someClientLabel, someRequest, someResponse); assertEquals(HttpStatus.OK, response.getStatusCode()); assertEquals(configurations, GSON.fromJson(response.getBody(), responseType)); } @Test public void testQueryConfigAsRaw() throws Exception { String someKey = "someKey"; String someValue = "someValue"; String someWatchKey = "someWatchKey"; Set watchKeys = Sets.newHashSet(someWatchKey); ApolloConfig someApolloConfig = mock(ApolloConfig.class); when(configController.queryConfig(someAppId, someClusterName, someNamespace, someDataCenter, "-1", someClientIp, someClientLabel, null, someRequest, someResponse)) .thenReturn(someApolloConfig); when(someApolloConfig.getNamespaceName()).thenReturn(someNamespace + ".json"); String jsonContent = GSON.toJson(ImmutableMap.of(someKey, someValue)); when(someApolloConfig.getConfigurations()).thenReturn(ImmutableMap.of("content", jsonContent)); when(watchKeysUtil.assembleAllWatchKeys(someAppId, someClusterName, someNamespace, someDataCenter)).thenReturn(watchKeys); ResponseEntity response = configFileController.queryConfigAsRaw(someAppId, someClusterName, someNamespace + ".json", someDataCenter, someClientIp, someClientLabel, someRequest, someResponse); assertEquals(HttpStatus.OK, response.getStatusCode()); assertEquals("application/json;charset=UTF-8", response.getHeaders().getContentType().toString()); assertEquals(jsonContent, response.getBody()); } @Test public void testQueryConfigWithGrayRelease() throws Exception { String someKey = "someKey"; String someValue = "someValue"; Type responseType = new TypeToken>() {}.getType(); Map configurations = ImmutableMap.of(someKey, someValue); when(grayReleaseRulesHolder.hasGrayReleaseRule(someAppId, someClientIp, someClientLabel, someNamespace)).thenReturn(true); ApolloConfig someApolloConfig = mock(ApolloConfig.class); when(someApolloConfig.getConfigurations()).thenReturn(configurations); when(configController.queryConfig(someAppId, someClusterName, someNamespace, someDataCenter, "-1", someClientIp, someClientLabel, null, someRequest, someResponse)) .thenReturn(someApolloConfig); ResponseEntity response = configFileController.queryConfigAsJson(someAppId, someClusterName, someNamespace, someDataCenter, someClientIp, someClientLabel, someRequest, someResponse); ResponseEntity anotherResponse = configFileController.queryConfigAsJson(someAppId, someClusterName, someNamespace, someDataCenter, someClientIp, someClientLabel, someRequest, someResponse); verify(configController, times(2)).queryConfig(someAppId, someClusterName, someNamespace, someDataCenter, "-1", someClientIp, someClientLabel, null, someRequest, someResponse); assertEquals(HttpStatus.OK, response.getStatusCode()); assertEquals(configurations, GSON.fromJson(response.getBody(), responseType)); assertTrue(watchedKeys2CacheKey.isEmpty()); assertTrue(cacheKey2WatchedKeys.isEmpty()); } @Test public void testHandleMessage() throws Exception { String someWatchKey = "someWatchKey"; String anotherWatchKey = "anotherWatchKey"; String someCacheKey = "someCacheKey"; String anotherCacheKey = "anotherCacheKey"; String someValue = "someValue"; ReleaseMessage someReleaseMessage = mock(ReleaseMessage.class); when(someReleaseMessage.getMessage()).thenReturn(someWatchKey); Cache cache = (Cache) ReflectionTestUtils.getField(configFileController, "localCache"); cache.put(someCacheKey, someValue); cache.put(anotherCacheKey, someValue); watchedKeys2CacheKey.putAll(someWatchKey, Lists.newArrayList(someCacheKey, anotherCacheKey)); watchedKeys2CacheKey.putAll(anotherWatchKey, Lists.newArrayList(someCacheKey, anotherCacheKey)); cacheKey2WatchedKeys.putAll(someCacheKey, Lists.newArrayList(someWatchKey, anotherWatchKey)); cacheKey2WatchedKeys.putAll(anotherCacheKey, Lists.newArrayList(someWatchKey, anotherWatchKey)); configFileController.handleMessage(someReleaseMessage, Topics.APOLLO_RELEASE_TOPIC); assertTrue(watchedKeys2CacheKey.isEmpty()); assertTrue(cacheKey2WatchedKeys.isEmpty()); } } ================================================ FILE: apollo-configservice/src/test/java/com/ctrip/framework/apollo/configservice/controller/NotificationControllerTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.configservice.controller; import com.ctrip.framework.apollo.biz.entity.ReleaseMessage; import com.ctrip.framework.apollo.biz.message.Topics; import com.ctrip.framework.apollo.biz.utils.EntityManagerUtil; import com.ctrip.framework.apollo.configservice.service.ReleaseMessageServiceWithCache; import com.ctrip.framework.apollo.configservice.util.NamespaceUtil; import com.ctrip.framework.apollo.configservice.util.WatchKeysUtil; import com.ctrip.framework.apollo.core.ConfigConsts; import com.ctrip.framework.apollo.core.dto.ApolloConfigNotification; import com.google.common.base.Joiner; import com.google.common.collect.Multimap; import com.google.common.collect.Sets; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.web.context.request.async.DeferredResult; import java.util.Set; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; /** * @author Jason Song(song_s@ctrip.com) */ @RunWith(MockitoJUnitRunner.class) public class NotificationControllerTest { private NotificationController controller; private String someAppId; private String someCluster; private String defaultNamespace; private String someDataCenter; private long someNotificationId; private String someClientIp; @Mock private ReleaseMessageServiceWithCache releaseMessageService; @Mock private EntityManagerUtil entityManagerUtil; @Mock private NamespaceUtil namespaceUtil; @Mock private WatchKeysUtil watchKeysUtil; private Multimap>> deferredResults; @Before public void setUp() throws Exception { controller = new NotificationController(watchKeysUtil, releaseMessageService, entityManagerUtil, namespaceUtil); someAppId = "someAppId"; someCluster = "someCluster"; defaultNamespace = ConfigConsts.NAMESPACE_APPLICATION; someDataCenter = "someDC"; someNotificationId = 1; someClientIp = "someClientIp"; when(namespaceUtil.filterNamespaceName(defaultNamespace)).thenReturn(defaultNamespace); deferredResults = (Multimap>>) ReflectionTestUtils .getField(controller, "deferredResults"); } @Test public void testPollNotificationWithDefaultNamespace() throws Exception { String someWatchKey = "someKey"; String anotherWatchKey = "anotherKey"; Set watchKeys = Sets.newHashSet(someWatchKey, anotherWatchKey); when(watchKeysUtil.assembleAllWatchKeys(someAppId, someCluster, defaultNamespace, someDataCenter)).thenReturn(watchKeys); DeferredResult> deferredResult = controller.pollNotification(someAppId, someCluster, defaultNamespace, someDataCenter, someNotificationId, someClientIp); assertEquals(watchKeys.size(), deferredResults.size()); for (String watchKey : watchKeys) { assertTrue(deferredResults.get(watchKey).contains(deferredResult)); } } @Test public void testPollNotificationWithDefaultNamespaceAsFile() throws Exception { String namespace = String.format("%s.%s", defaultNamespace, "properties"); when(namespaceUtil.filterNamespaceName(namespace)).thenReturn(defaultNamespace); String someWatchKey = "someKey"; String anotherWatchKey = "anotherKey"; Set watchKeys = Sets.newHashSet(someWatchKey, anotherWatchKey); when(watchKeysUtil.assembleAllWatchKeys(someAppId, someCluster, defaultNamespace, someDataCenter)).thenReturn(watchKeys); DeferredResult> deferredResult = controller.pollNotification(someAppId, someCluster, namespace, someDataCenter, someNotificationId, someClientIp); assertEquals(watchKeys.size(), deferredResults.size()); for (String watchKey : watchKeys) { assertTrue(deferredResults.get(watchKey).contains(deferredResult)); } } @Test public void testPollNotificationWithSomeNamespaceAsFile() throws Exception { String namespace = "someNamespace.xml"; when(namespaceUtil.filterNamespaceName(namespace)).thenReturn(namespace); String someWatchKey = "someKey"; Set watchKeys = Sets.newHashSet(someWatchKey); when(watchKeysUtil.assembleAllWatchKeys(someAppId, someCluster, namespace, someDataCenter)) .thenReturn(watchKeys); DeferredResult> deferredResult = controller.pollNotification(someAppId, someCluster, namespace, someDataCenter, someNotificationId, someClientIp); assertEquals(watchKeys.size(), deferredResults.size()); for (String watchKey : watchKeys) { assertTrue(deferredResults.get(watchKey).contains(deferredResult)); } } @Test public void testPollNotificationWithDefaultNamespaceWithNotificationIdOutDated() throws Exception { long notificationId = someNotificationId + 1; ReleaseMessage someReleaseMessage = mock(ReleaseMessage.class); String someWatchKey = "someKey"; Set watchKeys = Sets.newHashSet(someWatchKey); when(watchKeysUtil.assembleAllWatchKeys(someAppId, someCluster, defaultNamespace, someDataCenter)).thenReturn(watchKeys); when(someReleaseMessage.getId()).thenReturn(notificationId); when(releaseMessageService.findLatestReleaseMessageForMessages(watchKeys)) .thenReturn(someReleaseMessage); DeferredResult> deferredResult = controller.pollNotification(someAppId, someCluster, defaultNamespace, someDataCenter, someNotificationId, someClientIp); ResponseEntity result = (ResponseEntity) deferredResult.getResult(); assertEquals(HttpStatus.OK, result.getStatusCode()); assertEquals(defaultNamespace, result.getBody().getNamespaceName()); assertEquals(notificationId, result.getBody().getNotificationId()); } @Test public void testPollNotificationWithDefaultNamespaceAndHandleMessage() throws Exception { String someWatchKey = "someKey"; String anotherWatchKey = Joiner.on(ConfigConsts.CLUSTER_NAMESPACE_SEPARATOR).join(someAppId, someCluster, defaultNamespace); Set watchKeys = Sets.newHashSet(someWatchKey, anotherWatchKey); when(watchKeysUtil.assembleAllWatchKeys(someAppId, someCluster, defaultNamespace, someDataCenter)).thenReturn(watchKeys); DeferredResult> deferredResult = controller.pollNotification(someAppId, someCluster, defaultNamespace, someDataCenter, someNotificationId, someClientIp); long someId = 1; ReleaseMessage someReleaseMessage = new ReleaseMessage(anotherWatchKey); someReleaseMessage.setId(someId); controller.handleMessage(someReleaseMessage, Topics.APOLLO_RELEASE_TOPIC); ResponseEntity response = (ResponseEntity) deferredResult.getResult(); ApolloConfigNotification notification = response.getBody(); assertEquals(HttpStatus.OK, response.getStatusCode()); assertEquals(defaultNamespace, notification.getNamespaceName()); assertEquals(someId, notification.getNotificationId()); } } ================================================ FILE: apollo-configservice/src/test/java/com/ctrip/framework/apollo/configservice/controller/NotificationControllerV2Test.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.configservice.controller; import com.ctrip.framework.apollo.biz.config.BizConfig; import com.ctrip.framework.apollo.biz.entity.ReleaseMessage; import com.ctrip.framework.apollo.biz.message.Topics; import com.ctrip.framework.apollo.biz.utils.EntityManagerUtil; import com.ctrip.framework.apollo.configservice.service.ReleaseMessageServiceWithCache; import com.ctrip.framework.apollo.configservice.util.NamespaceUtil; import com.ctrip.framework.apollo.configservice.util.WatchKeysUtil; import com.ctrip.framework.apollo.configservice.wrapper.CaseInsensitiveMultimapWrapper; import com.ctrip.framework.apollo.configservice.wrapper.DeferredResultWrapper; import com.ctrip.framework.apollo.core.ConfigConsts; import com.ctrip.framework.apollo.core.dto.ApolloConfigNotification; import com.ctrip.framework.apollo.core.dto.ApolloNotificationMessages; import com.google.common.base.Joiner; import com.google.common.collect.HashMultimap; import com.google.common.collect.Lists; import com.google.common.collect.Multimap; import com.google.common.collect.Sets; import com.google.gson.Gson; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.web.context.request.async.DeferredResult; import java.util.Collection; import java.util.List; import java.util.Objects; import java.util.concurrent.TimeUnit; import static org.awaitility.Awaitility.await; import static org.junit.Assert.*; import static org.mockito.Mockito.*; /** * @author Jason Song(song_s@ctrip.com) */ @RunWith(MockitoJUnitRunner.class) public class NotificationControllerV2Test { private NotificationControllerV2 controller; private String someAppId; private String someCluster; private String defaultCluster; private String defaultNamespace; private String somePublicNamespace; private String someDataCenter; private long someNotificationId; private String someClientIp; @Mock private ReleaseMessageServiceWithCache releaseMessageService; @Mock private EntityManagerUtil entityManagerUtil; @Mock private NamespaceUtil namespaceUtil; @Mock private WatchKeysUtil watchKeysUtil; @Mock private BizConfig bizConfig; private Gson gson; private CaseInsensitiveMultimapWrapper deferredResults; @Before public void setUp() throws Exception { gson = new Gson(); controller = new NotificationControllerV2(watchKeysUtil, releaseMessageService, entityManagerUtil, namespaceUtil, gson, bizConfig); when(bizConfig.releaseMessageNotificationBatch()).thenReturn(100); when(bizConfig.releaseMessageNotificationBatchIntervalInMilli()).thenReturn(5); someAppId = "someAppId"; someCluster = "someCluster"; defaultCluster = ConfigConsts.CLUSTER_NAME_DEFAULT; defaultNamespace = ConfigConsts.NAMESPACE_APPLICATION; somePublicNamespace = "somePublicNamespace"; someDataCenter = "someDC"; someNotificationId = 1; someClientIp = "someClientIp"; when(namespaceUtil.filterNamespaceName(defaultNamespace)).thenReturn(defaultNamespace); when(namespaceUtil.filterNamespaceName(somePublicNamespace)).thenReturn(somePublicNamespace); when(namespaceUtil.normalizeNamespace(someAppId, defaultNamespace)) .thenReturn(defaultNamespace); when(namespaceUtil.normalizeNamespace(someAppId, somePublicNamespace)) .thenReturn(somePublicNamespace); deferredResults = (CaseInsensitiveMultimapWrapper) ReflectionTestUtils .getField(controller, "deferredResults"); } @Test public void testPollNotificationWithDefaultNamespace() throws Exception { String someWatchKey = "someKey"; String anotherWatchKey = "anotherKey"; Multimap watchKeysMap = assembleMultiMap(defaultNamespace, Lists.newArrayList(someWatchKey, anotherWatchKey)); String notificationAsString = transformApolloConfigNotificationsToString(defaultNamespace, someNotificationId); when(watchKeysUtil.assembleAllWatchKeys(someAppId, someCluster, Sets.newHashSet(defaultNamespace), someDataCenter)).thenReturn(watchKeysMap); DeferredResult>> deferredResult = controller.pollNotification(someAppId, someCluster, notificationAsString, someDataCenter, someClientIp); assertEquals(watchKeysMap.size(), deferredResults.size()); assertWatchKeys(watchKeysMap, deferredResult); } @Test public void testPollNotificationWithDefaultNamespaceAsFile() throws Exception { String namespace = String.format("%s.%s", defaultNamespace, "properties"); when(namespaceUtil.filterNamespaceName(namespace)).thenReturn(defaultNamespace); String someWatchKey = "someKey"; String anotherWatchKey = "anotherKey"; Multimap watchKeysMap = assembleMultiMap(defaultNamespace, Lists.newArrayList(someWatchKey, anotherWatchKey)); String notificationAsString = transformApolloConfigNotificationsToString(namespace, someNotificationId); when(watchKeysUtil.assembleAllWatchKeys(someAppId, someCluster, Sets.newHashSet(defaultNamespace), someDataCenter)).thenReturn(watchKeysMap); DeferredResult>> deferredResult = controller.pollNotification(someAppId, someCluster, notificationAsString, someDataCenter, someClientIp); assertEquals(watchKeysMap.size(), deferredResults.size()); assertWatchKeys(watchKeysMap, deferredResult); } @Test public void testPollNotificationWithMultipleNamespaces() throws Exception { String defaultNamespaceAsFile = defaultNamespace + ".properties"; String somePublicNamespaceAsFile = somePublicNamespace + ".xml"; when(namespaceUtil.filterNamespaceName(defaultNamespaceAsFile)).thenReturn(defaultNamespace); when(namespaceUtil.filterNamespaceName(somePublicNamespaceAsFile)) .thenReturn(somePublicNamespaceAsFile); when(namespaceUtil.normalizeNamespace(someAppId, somePublicNamespaceAsFile)) .thenReturn(somePublicNamespaceAsFile); String someWatchKey = "someKey"; String anotherWatchKey = "anotherKey"; String somePublicWatchKey = "somePublicWatchKey"; String somePublicFileWatchKey = "somePublicFileWatchKey"; Multimap watchKeysMap = assembleMultiMap(defaultNamespace, Lists.newArrayList(someWatchKey, anotherWatchKey)); watchKeysMap .putAll(assembleMultiMap(somePublicNamespace, Lists.newArrayList(somePublicWatchKey))); watchKeysMap.putAll( assembleMultiMap(somePublicNamespaceAsFile, Lists.newArrayList(somePublicFileWatchKey))); String notificationAsString = transformApolloConfigNotificationsToString(defaultNamespaceAsFile, someNotificationId, somePublicNamespace, someNotificationId, somePublicNamespaceAsFile, someNotificationId); when(watchKeysUtil.assembleAllWatchKeys(someAppId, someCluster, Sets.newHashSet(defaultNamespace, somePublicNamespace, somePublicNamespaceAsFile), someDataCenter)).thenReturn(watchKeysMap); DeferredResult>> deferredResult = controller.pollNotification(someAppId, someCluster, notificationAsString, someDataCenter, someClientIp); assertEquals(watchKeysMap.size(), deferredResults.size()); assertWatchKeys(watchKeysMap, deferredResult); verify(watchKeysUtil, times(1)).assembleAllWatchKeys(someAppId, someCluster, Sets.newHashSet(defaultNamespace, somePublicNamespace, somePublicNamespaceAsFile), someDataCenter); } @Test public void testPollNotificationWithMultipleNamespaceWithNotificationIdOutDated() throws Exception { String someWatchKey = "someKey"; String anotherWatchKey = Joiner.on(ConfigConsts.CLUSTER_NAMESPACE_SEPARATOR).join(someAppId, someCluster, somePublicNamespace); String yetAnotherWatchKey = Joiner.on(ConfigConsts.CLUSTER_NAMESPACE_SEPARATOR).join(someAppId, defaultCluster, somePublicNamespace); long notificationId = someNotificationId + 1; long yetAnotherNotificationId = someNotificationId; Multimap watchKeysMap = assembleMultiMap(defaultNamespace, Lists.newArrayList(someWatchKey)); watchKeysMap.putAll(assembleMultiMap(somePublicNamespace, Lists.newArrayList(anotherWatchKey, yetAnotherWatchKey))); when(watchKeysUtil.assembleAllWatchKeys(someAppId, someCluster, Sets.newHashSet(defaultNamespace, somePublicNamespace), someDataCenter)) .thenReturn(watchKeysMap); ReleaseMessage someReleaseMessage = mock(ReleaseMessage.class); when(someReleaseMessage.getId()).thenReturn(notificationId); when(someReleaseMessage.getMessage()).thenReturn(anotherWatchKey); ReleaseMessage yetAnotherReleaseMessage = mock(ReleaseMessage.class); when(yetAnotherReleaseMessage.getId()).thenReturn(yetAnotherNotificationId); when(yetAnotherReleaseMessage.getMessage()).thenReturn(yetAnotherWatchKey); when(releaseMessageService .findLatestReleaseMessagesGroupByMessages(Sets.newHashSet(watchKeysMap.values()))) .thenReturn(Lists.newArrayList(someReleaseMessage, yetAnotherReleaseMessage)); String notificationAsString = transformApolloConfigNotificationsToString(defaultNamespace, someNotificationId, somePublicNamespace, someNotificationId); DeferredResult>> deferredResult = controller.pollNotification(someAppId, someCluster, notificationAsString, someDataCenter, someClientIp); ResponseEntity> result = (ResponseEntity>) deferredResult.getResult(); assertEquals(HttpStatus.OK, result.getStatusCode()); assertEquals(1, result.getBody().size()); assertEquals(somePublicNamespace, result.getBody().get(0).getNamespaceName()); assertEquals(notificationId, result.getBody().get(0).getNotificationId()); ApolloNotificationMessages notificationMessages = result.getBody().get(0).getMessages(); assertEquals(2, notificationMessages.getDetails().size()); assertEquals(notificationId, notificationMessages.get(anotherWatchKey).longValue()); assertEquals(yetAnotherNotificationId, notificationMessages.get(yetAnotherWatchKey).longValue()); } @Test public void testPollNotificationWithMultipleNamespacesAndHandleMessage() throws Exception { String someWatchKey = "someKey"; String anotherWatchKey = Joiner.on(ConfigConsts.CLUSTER_NAMESPACE_SEPARATOR).join(someAppId, someCluster, somePublicNamespace); Multimap watchKeysMap = assembleMultiMap(defaultNamespace, Lists.newArrayList(someWatchKey)); watchKeysMap.putAll(assembleMultiMap(somePublicNamespace, Lists.newArrayList(anotherWatchKey))); when(watchKeysUtil.assembleAllWatchKeys(someAppId, someCluster, Sets.newHashSet(defaultNamespace, somePublicNamespace), someDataCenter)) .thenReturn(watchKeysMap); String notificationAsString = transformApolloConfigNotificationsToString(defaultNamespace, someNotificationId, somePublicNamespace, someNotificationId); DeferredResult>> deferredResult = controller.pollNotification(someAppId, someCluster, notificationAsString, someDataCenter, someClientIp); assertEquals(watchKeysMap.size(), deferredResults.size()); long someId = 1; ReleaseMessage someReleaseMessage = new ReleaseMessage(anotherWatchKey); someReleaseMessage.setId(someId); controller.handleMessage(someReleaseMessage, Topics.APOLLO_RELEASE_TOPIC); ResponseEntity> response = (ResponseEntity>) deferredResult.getResult(); assertEquals(1, response.getBody().size()); ApolloConfigNotification notification = response.getBody().get(0); assertEquals(HttpStatus.OK, response.getStatusCode()); assertEquals(somePublicNamespace, notification.getNamespaceName()); assertEquals(someId, notification.getNotificationId()); ApolloNotificationMessages notificationMessages = response.getBody().get(0).getMessages(); assertEquals(1, notificationMessages.getDetails().size()); assertEquals(someId, notificationMessages.get(anotherWatchKey).longValue()); } @Test public void testPollNotificationWithHandleMessageInBatch() throws Exception { String someWatchKey = Joiner.on(ConfigConsts.CLUSTER_NAMESPACE_SEPARATOR).join(someAppId, someCluster, defaultNamespace); int someBatch = 1; int someBatchInterval = 10; Multimap watchKeysMap = assembleMultiMap(defaultNamespace, Lists.newArrayList(someWatchKey)); String notificationAsString = transformApolloConfigNotificationsToString(defaultNamespace, someNotificationId); when(watchKeysUtil.assembleAllWatchKeys(someAppId, someCluster, Sets.newHashSet(defaultNamespace), someDataCenter)).thenReturn(watchKeysMap); when(bizConfig.releaseMessageNotificationBatch()).thenReturn(someBatch); when(bizConfig.releaseMessageNotificationBatchIntervalInMilli()).thenReturn(someBatchInterval); DeferredResult>> deferredResult = controller.pollNotification(someAppId, someCluster, notificationAsString, someDataCenter, someClientIp); DeferredResult>> anotherDeferredResult = controller.pollNotification(someAppId, someCluster, notificationAsString, someDataCenter, someClientIp); long someId = 1; ReleaseMessage someReleaseMessage = new ReleaseMessage(someWatchKey); someReleaseMessage.setId(someId); controller.handleMessage(someReleaseMessage, Topics.APOLLO_RELEASE_TOPIC); // in batch mode, at most one of them should have result assertFalse(deferredResult.hasResult() && anotherDeferredResult.hasResult()); // now both of them should have result await().atMost(someBatchInterval * 500, TimeUnit.MILLISECONDS).untilAsserted( () -> assertTrue(deferredResult.hasResult() && anotherDeferredResult.hasResult())); } @Test public void testPollNotificationWithIncorrectCase() throws Exception { String appIdWithIncorrectCase = someAppId.toUpperCase(); String namespaceWithIncorrectCase = defaultNamespace.toUpperCase(); String someMessage = Joiner.on(ConfigConsts.CLUSTER_NAMESPACE_SEPARATOR).join(someAppId, someCluster, defaultNamespace); String someWatchKey = Joiner.on(ConfigConsts.CLUSTER_NAMESPACE_SEPARATOR) .join(appIdWithIncorrectCase, someCluster, defaultNamespace); Multimap watchKeysMap = assembleMultiMap(defaultNamespace, Lists.newArrayList(someWatchKey)); String notificationAsString = transformApolloConfigNotificationsToString( defaultNamespace.toUpperCase(), someNotificationId); when(namespaceUtil.filterNamespaceName(namespaceWithIncorrectCase)) .thenReturn(namespaceWithIncorrectCase); when(namespaceUtil.normalizeNamespace(appIdWithIncorrectCase, namespaceWithIncorrectCase)) .thenReturn(defaultNamespace); when(watchKeysUtil.assembleAllWatchKeys(appIdWithIncorrectCase, someCluster, Sets.newHashSet(defaultNamespace), someDataCenter)).thenReturn(watchKeysMap); DeferredResult>> deferredResult = controller.pollNotification(appIdWithIncorrectCase, someCluster, notificationAsString, someDataCenter, someClientIp); long someId = 1; ReleaseMessage someReleaseMessage = new ReleaseMessage(someMessage); someReleaseMessage.setId(someId); controller.handleMessage(someReleaseMessage, Topics.APOLLO_RELEASE_TOPIC); assertTrue(deferredResult.hasResult()); ResponseEntity> response = (ResponseEntity>) deferredResult.getResult(); assertEquals(1, response.getBody().size()); ApolloConfigNotification notification = response.getBody().get(0); assertEquals(HttpStatus.OK, response.getStatusCode()); assertEquals(namespaceWithIncorrectCase, notification.getNamespaceName()); assertEquals(someId, notification.getNotificationId()); ApolloNotificationMessages notificationMessages = notification.getMessages(); assertEquals(1, notificationMessages.getDetails().size()); assertEquals(someId, notificationMessages.get(someMessage).longValue()); } private String transformApolloConfigNotificationsToString(String namespace, long notificationId) { List notifications = Lists.newArrayList(assembleApolloConfigNotification(namespace, notificationId)); return gson.toJson(notifications); } private String transformApolloConfigNotificationsToString(String namespace, long notificationId, String anotherNamespace, long anotherNotificationId) { List notifications = Lists.newArrayList(assembleApolloConfigNotification(namespace, notificationId), assembleApolloConfigNotification(anotherNamespace, anotherNotificationId)); return gson.toJson(notifications); } private String transformApolloConfigNotificationsToString(String namespace, long notificationId, String anotherNamespace, long anotherNotificationId, String yetAnotherNamespace, long yetAnotherNotificationId) { List notifications = Lists.newArrayList(assembleApolloConfigNotification(namespace, notificationId), assembleApolloConfigNotification(anotherNamespace, anotherNotificationId), assembleApolloConfigNotification(yetAnotherNamespace, yetAnotherNotificationId)); return gson.toJson(notifications); } private ApolloConfigNotification assembleApolloConfigNotification(String namespace, long notificationId) { return new ApolloConfigNotification(namespace, notificationId); } private Multimap assembleMultiMap(String key, Iterable values) { Multimap multimap = HashMultimap.create(); multimap.putAll(key, values); return multimap; } private void assertWatchKeys(Multimap watchKeysMap, DeferredResult deferredResult) { for (String watchKey : watchKeysMap.values()) { Collection deferredResultWrappers = deferredResults.get(watchKey); boolean found = false; for (DeferredResultWrapper wrapper : deferredResultWrappers) { if (Objects.equals(wrapper.getResult(), deferredResult)) { found = true; } } assertTrue(found); } } } ================================================ FILE: apollo-configservice/src/test/java/com/ctrip/framework/apollo/configservice/controller/TestWebSecurityConfig.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.configservice.controller; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.web.SecurityFilterChain; @Configuration @Order(0) public class TestWebSecurityConfig { @Bean @Order(0) public SecurityFilterChain testSecurityFilterChain(HttpSecurity http) throws Exception { http.securityMatcher("/", "/console/**"); http.httpBasic(Customizer.withDefaults()); http.csrf(csrf -> csrf.disable()); http.authorizeHttpRequests(authorizeHttpRequests -> authorizeHttpRequests.requestMatchers("/") .permitAll().requestMatchers("/console/**").permitAll()); http.headers(headers -> headers.frameOptions(frameOptions -> frameOptions.disable())); return http.build(); } } ================================================ FILE: apollo-configservice/src/test/java/com/ctrip/framework/apollo/configservice/filter/ClientAuthenticationFilterTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.configservice.filter; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import com.ctrip.framework.apollo.biz.config.BizConfig; import com.ctrip.framework.apollo.configservice.util.AccessKeyUtil; import com.ctrip.framework.apollo.core.signature.Signature; import com.google.common.collect.Lists; import java.util.Collections; import java.util.List; import jakarta.servlet.FilterChain; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import org.springframework.http.HttpHeaders; /** * @author nisiyong */ @RunWith(MockitoJUnitRunner.class) public class ClientAuthenticationFilterTest { private ClientAuthenticationFilter clientAuthenticationFilter; @Mock private BizConfig bizConfig; @Mock private AccessKeyUtil accessKeyUtil; @Mock private HttpServletRequest request; @Mock private HttpServletResponse response; @Mock private FilterChain filterChain; @Before public void setUp() { clientAuthenticationFilter = spy(new ClientAuthenticationFilter(bizConfig, accessKeyUtil)); } @Test public void testInvalidAppId() throws Exception { when(accessKeyUtil.extractAppIdFromRequest(any())).thenReturn(null); clientAuthenticationFilter.doFilter(request, response, filterChain); verify(response).sendError(HttpServletResponse.SC_BAD_REQUEST, "InvalidAppId"); verify(filterChain, never()).doFilter(request, response); } @Test public void testRequestTimeTooSkewed() throws Exception { String appId = "someAppId"; List secrets = Lists.newArrayList("someSecret"); String oneMinAgoTimestamp = Long.toString(System.currentTimeMillis() - 61 * 1000); when(accessKeyUtil.extractAppIdFromRequest(any())).thenReturn(appId); when(accessKeyUtil.findAvailableSecret(appId)).thenReturn(secrets); when(request.getHeader(Signature.HTTP_HEADER_TIMESTAMP)).thenReturn(oneMinAgoTimestamp); clientAuthenticationFilter.doFilter(request, response, filterChain); verify(response).sendError(HttpServletResponse.SC_UNAUTHORIZED, "RequestTimeTooSkewed"); verify(filterChain, never()).doFilter(request, response); } @Test public void testRequestTimeOneMinFasterThenCurrentTime() throws Exception { String appId = "someAppId"; List secrets = Lists.newArrayList("someSecret"); String oneMinAfterTimestamp = Long.toString(System.currentTimeMillis() + 61 * 1000); when(accessKeyUtil.extractAppIdFromRequest(any())).thenReturn(appId); when(accessKeyUtil.findAvailableSecret(appId)).thenReturn(secrets); when(request.getHeader(Signature.HTTP_HEADER_TIMESTAMP)).thenReturn(oneMinAfterTimestamp); clientAuthenticationFilter.doFilter(request, response, filterChain); verify(response).sendError(HttpServletResponse.SC_UNAUTHORIZED, "RequestTimeTooSkewed"); verify(filterChain, never()).doFilter(request, response); } @Test public void testUnauthorized() throws Exception { String appId = "someAppId"; String availableSignature = "someSignature"; List secrets = Lists.newArrayList("someSecret"); String oneMinAgoTimestamp = Long.toString(System.currentTimeMillis()); String errorAuthorization = "Apollo someAppId:wrongSignature"; when(accessKeyUtil.extractAppIdFromRequest(any())).thenReturn(appId); when(accessKeyUtil.findAvailableSecret(appId)).thenReturn(secrets); when(accessKeyUtil.buildSignature(any(), any(), any(), any())).thenReturn(availableSignature); when(request.getHeader(Signature.HTTP_HEADER_TIMESTAMP)).thenReturn(oneMinAgoTimestamp); when(request.getHeader(HttpHeaders.AUTHORIZATION)).thenReturn(errorAuthorization); when(bizConfig.accessKeyAuthTimeDiffTolerance()).thenReturn(60); clientAuthenticationFilter.doFilter(request, response, filterChain); verify(response).sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized"); verify(filterChain, never()).doFilter(request, response); } @Test public void testAuthorizedSuccessfully() throws Exception { String appId = "someAppId"; String availableSignature = "someSignature"; List secrets = Lists.newArrayList("someSecret"); String oneMinAgoTimestamp = Long.toString(System.currentTimeMillis()); String correctAuthorization = "Apollo someAppId:someSignature"; when(accessKeyUtil.extractAppIdFromRequest(any())).thenReturn(appId); when(accessKeyUtil.findAvailableSecret(appId)).thenReturn(secrets); when(accessKeyUtil.buildSignature(any(), any(), any(), any())).thenReturn(availableSignature); when(request.getHeader(Signature.HTTP_HEADER_TIMESTAMP)).thenReturn(oneMinAgoTimestamp); when(request.getHeader(HttpHeaders.AUTHORIZATION)).thenReturn(correctAuthorization); when(bizConfig.accessKeyAuthTimeDiffTolerance()).thenReturn(60); clientAuthenticationFilter.doFilter(request, response, filterChain); verifySuccessAndDoFilter(); } @Test public void testPreCheckInvalid() throws Exception { String appId = "someAppId"; String availableSignature = "someSignature"; List secrets = Lists.newArrayList("someSecret"); String oneMinAgoTimestamp = Long.toString(System.currentTimeMillis() - 61 * 1000); String errorAuthorization = "Apollo someAppId:wrongSignature"; when(accessKeyUtil.extractAppIdFromRequest(any())).thenReturn(appId); when(accessKeyUtil.findAvailableSecret(appId)).thenReturn(Collections.emptyList()); when(accessKeyUtil.findObservableSecrets(appId)).thenReturn(secrets); when(accessKeyUtil.buildSignature(any(), any(), any(), any())).thenReturn(availableSignature); when(request.getHeader(Signature.HTTP_HEADER_TIMESTAMP)).thenReturn(oneMinAgoTimestamp); when(request.getHeader(HttpHeaders.AUTHORIZATION)).thenReturn(errorAuthorization); when(bizConfig.accessKeyAuthTimeDiffTolerance()).thenReturn(60); clientAuthenticationFilter.doFilter(request, response, filterChain); verifySuccessAndDoFilter(); verify(clientAuthenticationFilter, times(2)).preCheckInvalidLogging(anyString()); } @Test public void testPreCheckSuccessfully() throws Exception { String appId = "someAppId"; String availableSignature = "someSignature"; List secrets = Lists.newArrayList("someSecret"); String oneMinAgoTimestamp = Long.toString(System.currentTimeMillis()); String correctAuthorization = "Apollo someAppId:someSignature"; when(accessKeyUtil.extractAppIdFromRequest(any())).thenReturn(appId); when(accessKeyUtil.findAvailableSecret(appId)).thenReturn(Collections.emptyList()); when(accessKeyUtil.findObservableSecrets(appId)).thenReturn(secrets); when(accessKeyUtil.buildSignature(any(), any(), any(), any())).thenReturn(availableSignature); when(request.getHeader(Signature.HTTP_HEADER_TIMESTAMP)).thenReturn(oneMinAgoTimestamp); when(request.getHeader(HttpHeaders.AUTHORIZATION)).thenReturn(correctAuthorization); when(bizConfig.accessKeyAuthTimeDiffTolerance()).thenReturn(60); clientAuthenticationFilter.doFilter(request, response, filterChain); verifySuccessAndDoFilter(); verify(clientAuthenticationFilter, never()).preCheckInvalidLogging(anyString()); } private void verifySuccessAndDoFilter() throws Exception { verify(response, never()).sendError(HttpServletResponse.SC_BAD_REQUEST, "InvalidAppId"); verify(response, never()).sendError(HttpServletResponse.SC_UNAUTHORIZED, "RequestTimeTooSkewed"); verify(response, never()).sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized"); verify(filterChain, times(1)).doFilter(request, response); } } ================================================ FILE: apollo-configservice/src/test/java/com/ctrip/framework/apollo/configservice/integration/AbstractBaseIntegrationTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.configservice.integration; import com.ctrip.framework.apollo.biz.service.BizDBPropertySource; import com.ctrip.framework.apollo.core.ConfigConsts; import com.google.common.base.Joiner; import com.google.gson.Gson; import com.ctrip.framework.apollo.ConfigServiceTestConfiguration; import com.ctrip.framework.apollo.biz.config.BizConfig; import com.ctrip.framework.apollo.biz.entity.Namespace; import com.ctrip.framework.apollo.biz.entity.Release; import com.ctrip.framework.apollo.biz.entity.ReleaseMessage; import com.ctrip.framework.apollo.biz.repository.ReleaseMessageRepository; import com.ctrip.framework.apollo.biz.repository.ReleaseRepository; import com.ctrip.framework.apollo.biz.utils.ReleaseKeyGenerator; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.boot.test.web.client.TestRestTemplate; import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.web.client.DefaultResponseErrorHandler; import org.springframework.web.client.RestTemplate; import org.springframework.web.util.UriTemplateHandler; import java.net.URI; import java.time.Duration; import java.util.Arrays; import java.util.Date; import java.util.LinkedHashMap; import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import jakarta.annotation.PostConstruct; /** * @author Jason Song(song_s@ctrip.com) */ @RunWith(SpringJUnit4ClassRunner.class) @SpringBootTest(classes = AbstractBaseIntegrationTest.TestConfiguration.class, webEnvironment = WebEnvironment.RANDOM_PORT) public abstract class AbstractBaseIntegrationTest { @Autowired private ReleaseMessageRepository releaseMessageRepository; @Autowired private ReleaseRepository releaseRepository; private static final Gson GSON = new Gson(); protected RestTemplate restTemplate = (new TestRestTemplate(new RestTemplateBuilder().setConnectTimeout(Duration.ofSeconds(5)))) .getRestTemplate(); @PostConstruct private void postConstruct() { restTemplate.setErrorHandler(new DefaultResponseErrorHandler()); restTemplate .setUriTemplateHandler(new BaseUrlUriTemplateHandler(restTemplate.getUriTemplateHandler())); } @Value("${local.server.port}") int port; protected String getHostUrl() { return "localhost:" + port; } @Configuration @Import(ConfigServiceTestConfiguration.class) protected static class TestConfiguration { @Bean public BizConfig bizConfig(final BizDBPropertySource bizDBPropertySource) { return new TestBizConfig(bizDBPropertySource); } } protected void sendReleaseMessage(String message) { ReleaseMessage releaseMessage = new ReleaseMessage(message); releaseMessageRepository.save(releaseMessage); } public Release buildRelease(String name, String comment, Namespace namespace, Map configurations, String owner) { Release release = new Release(); release.setReleaseKey(ReleaseKeyGenerator.generateReleaseKey(namespace)); release.setDataChangeCreatedTime(new Date()); release.setDataChangeCreatedBy(owner); release.setDataChangeLastModifiedBy(owner); release.setName(name); release.setComment(comment); release.setAppId(namespace.getAppId()); release.setClusterName(namespace.getClusterName()); release.setNamespaceName(namespace.getNamespaceName()); release.setConfigurations(GSON.toJson(configurations)); release = releaseRepository.save(release); return release; } protected void periodicSendMessage(ExecutorService executorService, String message, AtomicBoolean stop) { executorService.submit(() -> { // wait for the request connected to server while (!stop.get() && !Thread.currentThread().isInterrupted()) { try { TimeUnit.MILLISECONDS.sleep(100); } catch (InterruptedException e) { } // double check if (stop.get()) { break; } sendReleaseMessage(message); } }); } private static class TestBizConfig extends BizConfig { public TestBizConfig(final BizDBPropertySource propertySource) { super(propertySource); } @Override public int appNamespaceCacheScanInterval() { // should be short enough to update the AppNamespace cache in time return 1; } @Override public TimeUnit appNamespaceCacheScanIntervalTimeUnit() { return TimeUnit.MILLISECONDS; } } protected String assembleKey(String appId, String cluster, String namespace) { return Joiner.on(ConfigConsts.CLUSTER_NAMESPACE_SEPARATOR).join(appId, cluster, namespace); } private static class BaseUrlUriTemplateHandler implements UriTemplateHandler { private static final String BASE_URL_VARIABLE = "{baseurl}"; private final UriTemplateHandler delegate; private BaseUrlUriTemplateHandler(UriTemplateHandler delegate) { this.delegate = delegate; } @Override public URI expand(String uriTemplate, Map uriVariables) { if (!uriTemplate.contains(BASE_URL_VARIABLE) || !uriVariables.containsKey("baseurl")) { return delegate.expand(uriTemplate, uriVariables); } Map remainingUriVariables = new LinkedHashMap<>(uriVariables); Object baseUrl = remainingUriVariables.remove("baseurl"); return delegate.expand(uriTemplate.replace(BASE_URL_VARIABLE, String.valueOf(baseUrl)), remainingUriVariables); } @Override public URI expand(String uriTemplate, Object... uriVariables) { if (!uriTemplate.contains(BASE_URL_VARIABLE) || uriVariables.length == 0) { return delegate.expand(uriTemplate, uriVariables); } Object[] remainingUriVariables = Arrays.copyOfRange(uriVariables, 1, uriVariables.length); return delegate.expand( uriTemplate.replace(BASE_URL_VARIABLE, String.valueOf(uriVariables[0])), remainingUriVariables); } } } ================================================ FILE: apollo-configservice/src/test/java/com/ctrip/framework/apollo/configservice/integration/ConfigControllerIntegrationTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.configservice.integration; import com.ctrip.framework.apollo.configservice.service.AppNamespaceServiceWithCache; import com.ctrip.framework.apollo.core.ConfigConsts; import com.ctrip.framework.apollo.core.dto.ApolloConfig; import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.test.context.jdbc.Sql; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.web.client.HttpStatusCodeException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import static org.junit.Assert.assertEquals; /** * @author Jason Song(song_s@ctrip.com) */ public class ConfigControllerIntegrationTest extends AbstractBaseIntegrationTest { private String someAppId; private String somePublicAppId; private String someCluster; private String someNamespace; private String somePublicNamespace; private String someDC; private String someDefaultCluster; private String someClientIp; private ExecutorService executorService; @Autowired private AppNamespaceServiceWithCache appNamespaceServiceWithCache; @Before public void setUp() throws Exception { ReflectionTestUtils.invokeMethod(appNamespaceServiceWithCache, "reset"); someAppId = "someAppId"; someCluster = "someCluster"; someNamespace = "someNamespace"; somePublicAppId = "somePublicAppId"; somePublicNamespace = "somePublicNamespace"; someDC = "someDC"; someDefaultCluster = ConfigConsts.CLUSTER_NAME_DEFAULT; someClientIp = "1.1.1.1"; executorService = Executors.newFixedThreadPool(1); } @Test @Sql(scripts = "/integration-test/test-release.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/integration-test/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testQueryConfigWithDefaultClusterAndDefaultNamespaceOK() throws Exception { ResponseEntity response = restTemplate.getForEntity("http://{baseurl}/configs/{appId}/{clusterName}/{namespace}", ApolloConfig.class, getHostUrl(), someAppId, ConfigConsts.CLUSTER_NAME_DEFAULT, ConfigConsts.NAMESPACE_APPLICATION); ApolloConfig result = response.getBody(); assertEquals(HttpStatus.OK, response.getStatusCode()); assertEquals("TEST-RELEASE-KEY1", result.getReleaseKey()); assertEquals("v1", result.getConfigurations().get("k1")); } @Test @Sql(scripts = "/integration-test/test-release.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/integration-test/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testQueryConfigWithDefaultClusterAndDefaultNamespaceAndIncorrectCase() throws Exception { ResponseEntity response = restTemplate.getForEntity("http://{baseurl}/configs/{appId}/{clusterName}/{namespace}", ApolloConfig.class, getHostUrl(), someAppId, ConfigConsts.CLUSTER_NAME_DEFAULT, ConfigConsts.NAMESPACE_APPLICATION.toUpperCase()); ApolloConfig result = response.getBody(); assertEquals(HttpStatus.OK, response.getStatusCode()); assertEquals("TEST-RELEASE-KEY1", result.getReleaseKey()); assertEquals("v1", result.getConfigurations().get("k1")); } @Test @Sql(scripts = "/integration-test/test-release.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/integration-test/test-gray-release.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/integration-test/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testQueryGrayConfigWithDefaultClusterAndDefaultNamespaceOK() throws Exception { AtomicBoolean stop = new AtomicBoolean(); periodicSendMessage(executorService, assembleKey(someAppId, ConfigConsts.CLUSTER_NAME_DEFAULT, ConfigConsts.NAMESPACE_APPLICATION), stop); TimeUnit.MILLISECONDS.sleep(500); stop.set(true); ResponseEntity response = restTemplate.getForEntity( "http://{baseurl}/configs/{appId}/{clusterName}/{namespace}?ip={clientIp}", ApolloConfig.class, getHostUrl(), someAppId, ConfigConsts.CLUSTER_NAME_DEFAULT, ConfigConsts.NAMESPACE_APPLICATION, someClientIp); ApolloConfig result = response.getBody(); assertEquals(HttpStatus.OK, response.getStatusCode()); assertEquals("TEST-GRAY-RELEASE-KEY1", result.getReleaseKey()); assertEquals("v1-gray", result.getConfigurations().get("k1")); } @Test @Sql(scripts = "/integration-test/test-release.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/integration-test/test-gray-release.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/integration-test/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testQueryGrayConfigWithDefaultClusterAndDefaultNamespaceAndIncorrectCase() throws Exception { AtomicBoolean stop = new AtomicBoolean(); periodicSendMessage(executorService, assembleKey(someAppId, ConfigConsts.CLUSTER_NAME_DEFAULT, ConfigConsts.NAMESPACE_APPLICATION), stop); TimeUnit.MILLISECONDS.sleep(500); stop.set(true); ResponseEntity response = restTemplate.getForEntity( "http://{baseurl}/configs/{appId}/{clusterName}/{namespace}?ip={clientIp}", ApolloConfig.class, getHostUrl(), someAppId, ConfigConsts.CLUSTER_NAME_DEFAULT, ConfigConsts.NAMESPACE_APPLICATION.toUpperCase(), someClientIp); ApolloConfig result = response.getBody(); assertEquals(HttpStatus.OK, response.getStatusCode()); assertEquals("TEST-GRAY-RELEASE-KEY1", result.getReleaseKey()); assertEquals("v1-gray", result.getConfigurations().get("k1")); } @Test @Sql(scripts = "/integration-test/test-release.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/integration-test/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testQueryConfigFileWithDefaultClusterAndDefaultNamespaceOK() throws Exception { ResponseEntity response = restTemplate.getForEntity("http://{baseurl}/configs/{appId}/{clusterName}/{namespace}", ApolloConfig.class, getHostUrl(), someAppId, ConfigConsts.CLUSTER_NAME_DEFAULT, ConfigConsts.NAMESPACE_APPLICATION + ".properties"); ApolloConfig result = response.getBody(); assertEquals(HttpStatus.OK, response.getStatusCode()); assertEquals("TEST-RELEASE-KEY1", result.getReleaseKey()); assertEquals("v1", result.getConfigurations().get("k1")); } @Test @Sql(scripts = "/integration-test/test-release.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/integration-test/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testQueryConfigWithNamespaceOK() throws Exception { ResponseEntity response = restTemplate.getForEntity("http://{baseurl}/configs/{appId}/{clusterName}/{namespace}", ApolloConfig.class, getHostUrl(), someAppId, someCluster, someNamespace); ApolloConfig result = response.getBody(); assertEquals(HttpStatus.OK, response.getStatusCode()); assertEquals("TEST-RELEASE-KEY2", result.getReleaseKey()); assertEquals("v2", result.getConfigurations().get("k2")); } @Test @Sql(scripts = "/integration-test/test-release.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/integration-test/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testQueryConfigFileWithNamespaceOK() throws Exception { ResponseEntity response = restTemplate.getForEntity( "http://{baseurl}/configs/{appId}/{clusterName}/{namespace}", ApolloConfig.class, getHostUrl(), someAppId, ConfigConsts.CLUSTER_NAME_DEFAULT, someNamespace + ".xml"); ApolloConfig result = response.getBody(); assertEquals(HttpStatus.OK, response.getStatusCode()); assertEquals("TEST-RELEASE-KEY5", result.getReleaseKey()); assertEquals("v1-file", result.getConfigurations().get("k1")); assertEquals("v2-file", result.getConfigurations().get("k2")); } @Test public void testQueryConfigError() throws Exception { String someNamespaceNotExists = "someNamespaceNotExists"; HttpStatusCodeException httpException = null; try { ResponseEntity response = restTemplate.getForEntity("http://{baseurl}/configs/{appId}/{clusterName}/{namespace}", ApolloConfig.class, getHostUrl(), someAppId, someCluster, someNamespaceNotExists); } catch (HttpStatusCodeException ex) { httpException = ex; } assertEquals(HttpStatus.NOT_FOUND, httpException.getStatusCode()); } @Test @Sql(scripts = "/integration-test/test-release.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/integration-test/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testQueryConfigNotModified() throws Exception { String releaseKey = "TEST-RELEASE-KEY2"; ResponseEntity response = restTemplate.getForEntity( "http://{baseurl}/configs/{appId}/{clusterName}/{namespace}?releaseKey={releaseKey}", ApolloConfig.class, getHostUrl(), someAppId, someCluster, someNamespace, releaseKey); assertEquals(HttpStatus.NOT_MODIFIED, response.getStatusCode()); } @Test @Sql(scripts = "/integration-test/test-release.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/integration-test/test-gray-release.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/integration-test/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testQueryPublicGrayConfigWithNoOverride() throws Exception { AtomicBoolean stop = new AtomicBoolean(); periodicSendMessage(executorService, assembleKey(somePublicAppId, ConfigConsts.CLUSTER_NAME_DEFAULT, somePublicNamespace), stop); TimeUnit.MILLISECONDS.sleep(500); stop.set(true); ResponseEntity response = restTemplate.getForEntity( "http://{baseurl}/configs/{appId}/{clusterName}/{namespace}?ip={clientIp}", ApolloConfig.class, getHostUrl(), someAppId, someCluster, somePublicNamespace, someClientIp); ApolloConfig result = response.getBody(); assertEquals(HttpStatus.OK, response.getStatusCode()); assertEquals("TEST-GRAY-RELEASE-KEY2", result.getReleaseKey()); assertEquals("gray-v1", result.getConfigurations().get("k1")); assertEquals("gray-v2", result.getConfigurations().get("k2")); } @Test @Sql(scripts = "/integration-test/test-release.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/integration-test/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testQueryPublicConfigWithDataCenterFoundAndNoOverride() throws Exception { ResponseEntity response = restTemplate.getForEntity( "http://{baseurl}/configs/{appId}/{clusterName}/{namespace}?dataCenter={dateCenter}", ApolloConfig.class, getHostUrl(), someAppId, someCluster, somePublicNamespace, someDC); ApolloConfig result = response.getBody(); assertEquals("TEST-RELEASE-KEY4", result.getReleaseKey()); assertEquals(someAppId, result.getAppId()); assertEquals(someCluster, result.getCluster()); assertEquals(somePublicNamespace, result.getNamespaceName()); assertEquals("someDC-v1", result.getConfigurations().get("k1")); assertEquals("someDC-v2", result.getConfigurations().get("k2")); } @Test @Sql(scripts = "/integration-test/test-release.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/integration-test/test-release-public-dc-override.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/integration-test/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testQueryPublicConfigWithDataCenterFoundAndOverride() throws Exception { ResponseEntity response = restTemplate.getForEntity( "http://{baseurl}/configs/{appId}/{clusterName}/{namespace}?dataCenter={dateCenter}", ApolloConfig.class, getHostUrl(), someAppId, someDefaultCluster, somePublicNamespace, someDC); ApolloConfig result = response.getBody(); assertEquals( "TEST-RELEASE-KEY6" + ConfigConsts.CLUSTER_NAMESPACE_SEPARATOR + "TEST-RELEASE-KEY4", result.getReleaseKey()); assertEquals(someAppId, result.getAppId()); assertEquals(someDC, result.getCluster()); assertEquals(somePublicNamespace, result.getNamespaceName()); assertEquals("override-someDC-v1", result.getConfigurations().get("k1")); assertEquals("someDC-v2", result.getConfigurations().get("k2")); } @Test @Sql(scripts = "/integration-test/test-release.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/integration-test/test-release-public-dc-override.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/integration-test/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testQueryPublicConfigWithIncorrectCaseAndDataCenterFoundAndOverride() throws Exception { ResponseEntity response = restTemplate.getForEntity( "http://{baseurl}/configs/{appId}/{clusterName}/{namespace}?dataCenter={dateCenter}", ApolloConfig.class, getHostUrl(), someAppId, someDefaultCluster, somePublicNamespace.toUpperCase(), someDC); ApolloConfig result = response.getBody(); assertEquals( "TEST-RELEASE-KEY6" + ConfigConsts.CLUSTER_NAMESPACE_SEPARATOR + "TEST-RELEASE-KEY4", result.getReleaseKey()); assertEquals("override-someDC-v1", result.getConfigurations().get("k1")); assertEquals("someDC-v2", result.getConfigurations().get("k2")); } @Test @Sql(scripts = "/integration-test/test-release.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/integration-test/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testQueryPublicConfigWithDataCenterNotFoundAndNoOverride() throws Exception { String someDCNotFound = "someDCNotFound"; ResponseEntity response = restTemplate.getForEntity( "http://{baseurl}/configs/{appId}/{clusterName}/{namespace}?dataCenter={dateCenter}", ApolloConfig.class, getHostUrl(), someAppId, someCluster, somePublicNamespace, someDCNotFound); ApolloConfig result = response.getBody(); assertEquals("TEST-RELEASE-KEY3", result.getReleaseKey()); assertEquals(someAppId, result.getAppId()); assertEquals(someCluster, result.getCluster()); assertEquals(somePublicNamespace, result.getNamespaceName()); assertEquals("default-v1", result.getConfigurations().get("k1")); assertEquals("default-v2", result.getConfigurations().get("k2")); } @Test @Sql(scripts = "/integration-test/test-release.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/integration-test/test-release-public-default-override.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/integration-test/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testQueryPublicConfigWithDataCenterNotFoundAndOverride() throws Exception { String someDCNotFound = "someDCNotFound"; ResponseEntity response = restTemplate.getForEntity( "http://{baseurl}/configs/{appId}/{clusterName}/{namespace}?dataCenter={dateCenter}", ApolloConfig.class, getHostUrl(), someAppId, someDefaultCluster, somePublicNamespace, someDCNotFound); ApolloConfig result = response.getBody(); assertEquals( "TEST-RELEASE-KEY5" + ConfigConsts.CLUSTER_NAMESPACE_SEPARATOR + "TEST-RELEASE-KEY3", result.getReleaseKey()); assertEquals(someAppId, result.getAppId()); assertEquals(someDefaultCluster, result.getCluster()); assertEquals(somePublicNamespace, result.getNamespaceName()); assertEquals("override-v1", result.getConfigurations().get("k1")); assertEquals("default-v2", result.getConfigurations().get("k2")); } @Test @Sql(scripts = "/integration-test/test-release.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/integration-test/test-release-public-default-override.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/integration-test/test-gray-release.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/integration-test/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testQueryPublicGrayConfigWithOverride() throws Exception { AtomicBoolean stop = new AtomicBoolean(); periodicSendMessage(executorService, assembleKey(somePublicAppId, ConfigConsts.CLUSTER_NAME_DEFAULT, somePublicNamespace), stop); TimeUnit.MILLISECONDS.sleep(500); stop.set(true); ResponseEntity response = restTemplate.getForEntity( "http://{baseurl}/configs/{appId}/{clusterName}/{namespace}?ip={clientIp}", ApolloConfig.class, getHostUrl(), someAppId, someDefaultCluster, somePublicNamespace, someClientIp); ApolloConfig result = response.getBody(); assertEquals(HttpStatus.OK, response.getStatusCode()); assertEquals( "TEST-RELEASE-KEY5" + ConfigConsts.CLUSTER_NAMESPACE_SEPARATOR + "TEST-GRAY-RELEASE-KEY2", result.getReleaseKey()); assertEquals("override-v1", result.getConfigurations().get("k1")); assertEquals("gray-v2", result.getConfigurations().get("k2")); } @Test @Sql(scripts = "/integration-test/test-release.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/integration-test/test-release-public-default-override.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/integration-test/test-gray-release.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/integration-test/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testQueryPublicGrayConfigWithIncorrectCaseAndOverride() throws Exception { AtomicBoolean stop = new AtomicBoolean(); periodicSendMessage(executorService, assembleKey(somePublicAppId, ConfigConsts.CLUSTER_NAME_DEFAULT, somePublicNamespace), stop); TimeUnit.MILLISECONDS.sleep(500); stop.set(true); ResponseEntity response = restTemplate.getForEntity( "http://{baseurl}/configs/{appId}/{clusterName}/{namespace}?ip={clientIp}", ApolloConfig.class, getHostUrl(), someAppId, someDefaultCluster, somePublicNamespace.toUpperCase(), someClientIp); ApolloConfig result = response.getBody(); assertEquals(HttpStatus.OK, response.getStatusCode()); assertEquals( "TEST-RELEASE-KEY5" + ConfigConsts.CLUSTER_NAMESPACE_SEPARATOR + "TEST-GRAY-RELEASE-KEY2", result.getReleaseKey()); assertEquals("override-v1", result.getConfigurations().get("k1")); assertEquals("gray-v2", result.getConfigurations().get("k2")); } @Test @Sql(scripts = "/integration-test/test-release.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/integration-test/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testQueryPrivateConfigFileWithPublicNamespaceExists() throws Exception { String namespaceName = "anotherNamespace"; ResponseEntity response = restTemplate.getForEntity( "http://{baseurl}/configs/{appId}/{clusterName}/{namespace}", ApolloConfig.class, getHostUrl(), someAppId, ConfigConsts.CLUSTER_NAME_DEFAULT, namespaceName); ApolloConfig result = response.getBody(); assertEquals("TEST-RELEASE-KEY6", result.getReleaseKey()); assertEquals(someAppId, result.getAppId()); assertEquals(ConfigConsts.CLUSTER_NAME_DEFAULT, result.getCluster()); assertEquals(namespaceName, result.getNamespaceName()); assertEquals("v1-file", result.getConfigurations().get("k1")); assertEquals(null, result.getConfigurations().get("k2")); } @Test public void testQueryConfigForNoAppIdPlaceHolderWithPrivateNamespace() throws Exception { HttpStatusCodeException httpException = null; try { ResponseEntity response = restTemplate.getForEntity("http://{baseurl}/configs/{appId}/{clusterName}/{namespace}", ApolloConfig.class, getHostUrl(), ConfigConsts.NO_APPID_PLACEHOLDER, someCluster, ConfigConsts.NAMESPACE_APPLICATION); } catch (HttpStatusCodeException ex) { httpException = ex; } assertEquals(HttpStatus.NOT_FOUND, httpException.getStatusCode()); } @Test @Sql(scripts = "/integration-test/test-release.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/integration-test/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testQueryPublicConfigForNoAppIdPlaceHolder() throws Exception { ResponseEntity response = restTemplate.getForEntity( "http://{baseurl}/configs/{appId}/{clusterName}/{namespace}?dataCenter={dateCenter}", ApolloConfig.class, getHostUrl(), ConfigConsts.NO_APPID_PLACEHOLDER, someCluster, somePublicNamespace, someDC); ApolloConfig result = response.getBody(); assertEquals("TEST-RELEASE-KEY4", result.getReleaseKey()); assertEquals(ConfigConsts.NO_APPID_PLACEHOLDER, result.getAppId()); assertEquals(someCluster, result.getCluster()); assertEquals(somePublicNamespace, result.getNamespaceName()); assertEquals("someDC-v1", result.getConfigurations().get("k1")); assertEquals("someDC-v2", result.getConfigurations().get("k2")); } } ================================================ FILE: apollo-configservice/src/test/java/com/ctrip/framework/apollo/configservice/integration/ConfigFileControllerIntegrationTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.configservice.integration; import com.ctrip.framework.apollo.configservice.service.AppNamespaceServiceWithCache; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; import com.google.common.reflect.TypeToken; import com.google.gson.Gson; import com.ctrip.framework.apollo.biz.entity.Namespace; import com.ctrip.framework.apollo.core.ConfigConsts; import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.test.context.jdbc.Sql; import java.lang.reflect.Type; import java.util.List; import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import org.springframework.test.util.ReflectionTestUtils; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; /** * @author Jason Song(song_s@ctrip.com) */ public class ConfigFileControllerIntegrationTest extends AbstractBaseIntegrationTest { private String someAppId; private String somePublicAppId; private String someCluster; private String someNamespace; private String somePublicNamespace; private String someDC; private String someDefaultCluster; private String grayClientIp; private String grayClientLabel; private String nonGrayClientIp; private String nonGrayClientLabel; private static final Gson GSON = new Gson(); private ExecutorService executorService; private Type mapResponseType = new TypeToken>() {}.getType(); @Autowired private AppNamespaceServiceWithCache appNamespaceServiceWithCache; @Before public void setUp() throws Exception { ReflectionTestUtils.invokeMethod(appNamespaceServiceWithCache, "reset"); someDefaultCluster = ConfigConsts.CLUSTER_NAME_DEFAULT; someAppId = "someAppId"; somePublicAppId = "somePublicAppId"; someCluster = "someCluster"; someNamespace = "someNamespace"; somePublicNamespace = "somePublicNamespace"; someDC = "someDC"; grayClientIp = "1.1.1.1"; grayClientLabel = "myLabel"; nonGrayClientIp = "2.2.2.2"; nonGrayClientLabel = "appLabel"; executorService = Executors.newFixedThreadPool(1); } @Test @Sql(scripts = "/integration-test/test-release.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/integration-test/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testQueryConfigAsProperties() throws Exception { ResponseEntity response = restTemplate.getForEntity("http://{baseurl}/configfiles/{appId}/{clusterName}/{namespace}", String.class, getHostUrl(), someAppId, someCluster, someNamespace); String result = response.getBody(); assertEquals(HttpStatus.OK, response.getStatusCode()); assertTrue(result.contains("k2=v2")); } @Test @Sql(scripts = "/integration-test/test-release.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/integration-test/test-gray-release.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/integration-test/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testQueryConfigAsPropertiesWithGrayRelease() throws Exception { AtomicBoolean stop = new AtomicBoolean(); periodicSendMessage(executorService, assembleKey(someAppId, ConfigConsts.CLUSTER_NAME_DEFAULT, ConfigConsts.NAMESPACE_APPLICATION), stop); TimeUnit.MILLISECONDS.sleep(500); stop.set(true); ResponseEntity response = restTemplate.getForEntity( "http://{baseurl}/configfiles/{appId}/{clusterName}/{namespace}?ip={clientIp}&label={clientLabel}", String.class, getHostUrl(), someAppId, someDefaultCluster, ConfigConsts.NAMESPACE_APPLICATION, grayClientIp, grayClientLabel); ResponseEntity anotherResponse = restTemplate.getForEntity( "http://{baseurl}/configfiles/{appId}/{clusterName}/{namespace}?ip={clientIp}&label={clientLabel}", String.class, getHostUrl(), someAppId, someDefaultCluster, ConfigConsts.NAMESPACE_APPLICATION, nonGrayClientIp, nonGrayClientLabel); String result = response.getBody(); String anotherResult = anotherResponse.getBody(); assertEquals(HttpStatus.OK, response.getStatusCode()); assertTrue(result.contains("k1=v1-gray")); assertEquals(HttpStatus.OK, anotherResponse.getStatusCode()); assertFalse(anotherResult.contains("k1=v1-gray")); assertTrue(anotherResult.contains("k1=v1")); } @Test @Sql(scripts = "/integration-test/test-release.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/integration-test/test-release-public-dc-override.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/integration-test/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testQueryPublicConfigAsProperties() throws Exception { ResponseEntity response = restTemplate.getForEntity( "http://{baseurl}/configfiles/{appId}/{clusterName}/{namespace}?dataCenter={dateCenter}", String.class, getHostUrl(), someAppId, someDefaultCluster, somePublicNamespace, someDC); String result = response.getBody(); assertEquals(HttpStatus.OK, response.getStatusCode()); assertTrue(result.contains("k1=override-someDC-v1")); assertTrue(result.contains("k2=someDC-v2")); } @Test @Sql(scripts = "/integration-test/test-release.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/integration-test/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testQueryConfigAsJson() throws Exception { ResponseEntity response = restTemplate.getForEntity( "http://{baseurl}/configfiles/json/{appId}/{clusterName}/{namespace}", String.class, getHostUrl(), someAppId, someCluster, someNamespace); Map configs = GSON.fromJson(response.getBody(), mapResponseType); assertEquals(HttpStatus.OK, response.getStatusCode()); assertEquals("v2", configs.get("k2")); } @Test @Sql(scripts = "/integration-test/test-release.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/integration-test/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testQueryConfigAsJsonWithIncorrectCase() throws Exception { ResponseEntity response = restTemplate.getForEntity( "http://{baseurl}/configfiles/json/{appId}/{clusterName}/{namespace}", String.class, getHostUrl(), someAppId, someCluster, someNamespace.toUpperCase()); Map configs = GSON.fromJson(response.getBody(), mapResponseType); assertEquals(HttpStatus.OK, response.getStatusCode()); assertEquals("v2", configs.get("k2")); } @Test @Sql(scripts = "/integration-test/test-release.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/integration-test/test-release-public-dc-override.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/integration-test/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testQueryPublicConfigAsJson() throws Exception { ResponseEntity response = restTemplate.getForEntity( "http://{baseurl}/configfiles/json/{appId}/{clusterName}/{namespace}?dataCenter={dateCenter}", String.class, getHostUrl(), someAppId, someDefaultCluster, somePublicNamespace, someDC); Map configs = GSON.fromJson(response.getBody(), mapResponseType); assertEquals(HttpStatus.OK, response.getStatusCode()); assertEquals("override-someDC-v1", configs.get("k1")); assertEquals("someDC-v2", configs.get("k2")); } @Test @Sql(scripts = "/integration-test/test-release.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/integration-test/test-release-public-dc-override.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/integration-test/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testQueryPublicConfigAsJsonWithIncorrectCase() throws Exception { ResponseEntity response = restTemplate.getForEntity( "http://{baseurl}/configfiles/json/{appId}/{clusterName}/{namespace}?dataCenter={dateCenter}", String.class, getHostUrl(), someAppId, someDefaultCluster, somePublicNamespace.toUpperCase(), someDC); Map configs = GSON.fromJson(response.getBody(), mapResponseType); assertEquals(HttpStatus.OK, response.getStatusCode()); assertEquals("override-someDC-v1", configs.get("k1")); assertEquals("someDC-v2", configs.get("k2")); } @Test @Sql(scripts = "/integration-test/test-release.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/integration-test/test-release-public-default-override.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/integration-test/test-gray-release.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/integration-test/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testQueryPublicConfigAsJsonWithGrayRelease() throws Exception { AtomicBoolean stop = new AtomicBoolean(); periodicSendMessage(executorService, assembleKey(somePublicAppId, ConfigConsts.CLUSTER_NAME_DEFAULT, somePublicNamespace), stop); TimeUnit.MILLISECONDS.sleep(500); stop.set(true); ResponseEntity response = restTemplate.getForEntity( "http://{baseurl}/configfiles/json/{appId}/{clusterName}/{namespace}?ip={clientIp}&label={clientLabel}", String.class, getHostUrl(), someAppId, someDefaultCluster, somePublicNamespace, grayClientIp, grayClientLabel); ResponseEntity anotherResponse = restTemplate.getForEntity( "http://{baseurl}/configfiles/json/{appId}/{clusterName}/{namespace}?ip={clientIp}&label={clientLabel}", String.class, getHostUrl(), someAppId, someDefaultCluster, somePublicNamespace, nonGrayClientIp, nonGrayClientLabel); Map configs = GSON.fromJson(response.getBody(), mapResponseType); Map anotherConfigs = GSON.fromJson(anotherResponse.getBody(), mapResponseType); assertEquals(HttpStatus.OK, response.getStatusCode()); assertEquals(HttpStatus.OK, anotherResponse.getStatusCode()); assertEquals("override-v1", configs.get("k1")); assertEquals("gray-v2", configs.get("k2")); assertEquals("override-v1", anotherConfigs.get("k1")); assertEquals("default-v2", anotherConfigs.get("k2")); } @Test @Sql(scripts = "/integration-test/test-release.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/integration-test/test-release-public-default-override.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/integration-test/test-gray-release.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/integration-test/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testQueryPublicConfigAsJsonWithGrayReleaseAndIncorrectCase() throws Exception { AtomicBoolean stop = new AtomicBoolean(); periodicSendMessage(executorService, assembleKey(somePublicAppId, ConfigConsts.CLUSTER_NAME_DEFAULT, somePublicNamespace), stop); TimeUnit.MILLISECONDS.sleep(500); stop.set(true); ResponseEntity response = restTemplate.getForEntity( "http://{baseurl}/configfiles/json/{appId}/{clusterName}/{namespace}?ip={clientIp}&label={clientLabel}", String.class, getHostUrl(), someAppId, someDefaultCluster, somePublicNamespace.toUpperCase(), grayClientIp, grayClientLabel); ResponseEntity anotherResponse = restTemplate.getForEntity( "http://{baseurl}/configfiles/json/{appId}/{clusterName}/{namespace}?ip={clientIp}&label={clientLabel}", String.class, getHostUrl(), someAppId, someDefaultCluster, somePublicNamespace.toUpperCase(), nonGrayClientIp, nonGrayClientLabel); Map configs = GSON.fromJson(response.getBody(), mapResponseType); Map anotherConfigs = GSON.fromJson(anotherResponse.getBody(), mapResponseType); assertEquals(HttpStatus.OK, response.getStatusCode()); assertEquals(HttpStatus.OK, anotherResponse.getStatusCode()); assertEquals("override-v1", configs.get("k1")); assertEquals("gray-v2", configs.get("k2")); assertEquals("override-v1", anotherConfigs.get("k1")); assertEquals("default-v2", anotherConfigs.get("k2")); } @Test @Sql(scripts = "/integration-test/test-release.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/integration-test/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testConfigChanged() throws Exception { ResponseEntity response = restTemplate.getForEntity("http://{baseurl}/configfiles/{appId}/{clusterName}/{namespace}", String.class, getHostUrl(), someAppId, someCluster, someNamespace); String result = response.getBody(); assertEquals(HttpStatus.OK, response.getStatusCode()); assertTrue(result.contains("k2=v2")); String someReleaseName = "someReleaseName"; String someReleaseComment = "someReleaseComment"; Namespace namespace = new Namespace(); namespace.setAppId(someAppId); namespace.setClusterName(someCluster); namespace.setNamespaceName(someNamespace); String someOwner = "someOwner"; Map newConfigurations = ImmutableMap.of("k1", "v1-changed", "k2", "v2-changed"); buildRelease(someReleaseName, someReleaseComment, namespace, newConfigurations, someOwner); ResponseEntity anotherResponse = restTemplate.getForEntity("http://{baseurl}/configfiles/{appId}/{clusterName}/{namespace}", String.class, getHostUrl(), someAppId, someCluster, someNamespace); assertEquals(response.getBody(), anotherResponse.getBody()); List keys = Lists.newArrayList(someAppId, someCluster, someNamespace); String message = String.join(ConfigConsts.CLUSTER_NAMESPACE_SEPARATOR, keys); sendReleaseMessage(message); TimeUnit.MILLISECONDS.sleep(500); ResponseEntity newResponse = restTemplate.getForEntity("http://{baseurl}/configfiles/{appId}/{clusterName}/{namespace}", String.class, getHostUrl(), someAppId, someCluster, someNamespace); result = newResponse.getBody(); assertEquals(HttpStatus.OK, response.getStatusCode()); assertTrue(result.contains("k1=v1-changed")); assertTrue(result.contains("k2=v2-changed")); } } ================================================ FILE: apollo-configservice/src/test/java/com/ctrip/framework/apollo/configservice/integration/NotificationControllerIntegrationTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.configservice.integration; import com.ctrip.framework.apollo.configservice.service.AppNamespaceServiceWithCache; import com.ctrip.framework.apollo.configservice.service.ReleaseMessageServiceWithCache; import com.ctrip.framework.apollo.core.ConfigConsts; import com.ctrip.framework.apollo.core.dto.ApolloConfigNotification; import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.test.context.jdbc.Sql; import org.springframework.test.util.ReflectionTestUtils; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicBoolean; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; /** * @author Jason Song(song_s@ctrip.com) */ public class NotificationControllerIntegrationTest extends AbstractBaseIntegrationTest { private String someAppId; private String someCluster; private String defaultNamespace; private String somePublicNamespace; private ExecutorService executorService; @Autowired private ReleaseMessageServiceWithCache releaseMessageServiceWithCache; @Autowired private AppNamespaceServiceWithCache appNamespaceServiceWithCache; @Before public void setUp() throws Exception { ReflectionTestUtils.invokeMethod(releaseMessageServiceWithCache, "reset"); ReflectionTestUtils.invokeMethod(appNamespaceServiceWithCache, "reset"); someAppId = "someAppId"; someCluster = ConfigConsts.CLUSTER_NAME_DEFAULT; defaultNamespace = ConfigConsts.NAMESPACE_APPLICATION; somePublicNamespace = "somePublicNamespace"; executorService = Executors.newFixedThreadPool(1); } @Test(timeout = 5000L) @Sql(scripts = "/integration-test/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testPollNotificationWithDefaultNamespace() throws Exception { AtomicBoolean stop = new AtomicBoolean(); periodicSendMessage(executorService, assembleKey(someAppId, someCluster, defaultNamespace), stop); ResponseEntity result = restTemplate.getForEntity( "http://{baseurl}/notifications?appId={appId}&cluster={clusterName}&namespace={namespace}", ApolloConfigNotification.class, getHostUrl(), someAppId, someCluster, defaultNamespace); stop.set(true); ApolloConfigNotification notification = result.getBody(); assertEquals(HttpStatus.OK, result.getStatusCode()); assertEquals(defaultNamespace, notification.getNamespaceName()); assertNotEquals(0, notification.getNotificationId()); } @Test(timeout = 5000L) @Sql(scripts = "/integration-test/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testPollNotificationWithDefaultNamespaceAsFile() throws Exception { AtomicBoolean stop = new AtomicBoolean(); periodicSendMessage(executorService, assembleKey(someAppId, someCluster, defaultNamespace), stop); ResponseEntity result = restTemplate.getForEntity( "http://{baseurl}/notifications?appId={appId}&cluster={clusterName}&namespace={namespace}", ApolloConfigNotification.class, getHostUrl(), someAppId, someCluster, defaultNamespace + ".properties"); stop.set(true); ApolloConfigNotification notification = result.getBody(); assertEquals(HttpStatus.OK, result.getStatusCode()); assertEquals(defaultNamespace, notification.getNamespaceName()); assertNotEquals(0, notification.getNotificationId()); } @Test(timeout = 5000L) @Sql(scripts = "/integration-test/test-release.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/integration-test/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testPollNotificationWithPrivateNamespaceAsFile() throws Exception { String namespace = "someNamespace.xml"; AtomicBoolean stop = new AtomicBoolean(); periodicSendMessage(executorService, assembleKey(someAppId, ConfigConsts.CLUSTER_NAME_DEFAULT, namespace), stop); ResponseEntity result = restTemplate.getForEntity( "http://{baseurl}/notifications?appId={appId}&cluster={clusterName}&namespace={namespace}", ApolloConfigNotification.class, getHostUrl(), someAppId, someCluster, namespace); stop.set(true); ApolloConfigNotification notification = result.getBody(); assertEquals(HttpStatus.OK, result.getStatusCode()); assertEquals(namespace, notification.getNamespaceName()); assertNotEquals(0, notification.getNotificationId()); } @Test(timeout = 5000L) @Sql(scripts = "/integration-test/test-release-message.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/integration-test/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testPollNotificationWithDefaultNamespaceWithNotificationIdNull() throws Exception { ResponseEntity result = restTemplate.getForEntity( "http://{baseurl}/notifications?appId={appId}&cluster={clusterName}&namespace={namespace}", ApolloConfigNotification.class, getHostUrl(), someAppId, someCluster, defaultNamespace); ApolloConfigNotification notification = result.getBody(); assertEquals(HttpStatus.OK, result.getStatusCode()); assertEquals(defaultNamespace, notification.getNamespaceName()); assertEquals(10, notification.getNotificationId()); } @Test(timeout = 5000L) @Sql(scripts = "/integration-test/test-release.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/integration-test/test-release-message.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/integration-test/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testPollNotificationWithDefaultNamespaceWithNotificationIdOutDated() throws Exception { long someOutDatedNotificationId = 1; ResponseEntity result = restTemplate.getForEntity( "http://{baseurl}/notifications?appId={appId}&cluster={clusterName}&namespace={namespace}¬ificationId={notificationId}", ApolloConfigNotification.class, getHostUrl(), someAppId, someCluster, defaultNamespace, someOutDatedNotificationId); ApolloConfigNotification notification = result.getBody(); assertEquals(HttpStatus.OK, result.getStatusCode()); assertEquals(defaultNamespace, notification.getNamespaceName()); assertEquals(10, notification.getNotificationId()); } @Test(timeout = 5000L) @Sql(scripts = "/integration-test/test-release.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/integration-test/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testPollNotificationWthPublicNamespaceAndNoDataCenter() throws Exception { String publicAppId = "somePublicAppId"; AtomicBoolean stop = new AtomicBoolean(); periodicSendMessage(executorService, assembleKey(publicAppId, ConfigConsts.CLUSTER_NAME_DEFAULT, somePublicNamespace), stop); ResponseEntity result = restTemplate.getForEntity( "http://{baseurl}/notifications?appId={appId}&cluster={clusterName}&namespace={namespace}", ApolloConfigNotification.class, getHostUrl(), someAppId, someCluster, somePublicNamespace); stop.set(true); ApolloConfigNotification notification = result.getBody(); assertEquals(HttpStatus.OK, result.getStatusCode()); assertEquals(somePublicNamespace, notification.getNamespaceName()); assertNotEquals(0, notification.getNotificationId()); } @Test(timeout = 5000L) @Sql(scripts = "/integration-test/test-release.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/integration-test/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testPollNotificationWthPublicNamespaceAndDataCenter() throws Exception { String publicAppId = "somePublicAppId"; String someDC = "someDC"; AtomicBoolean stop = new AtomicBoolean(); periodicSendMessage(executorService, assembleKey(publicAppId, someDC, somePublicNamespace), stop); ResponseEntity result = restTemplate.getForEntity( "http://{baseurl}/notifications?appId={appId}&cluster={clusterName}&namespace={namespace}&dataCenter={dataCenter}", ApolloConfigNotification.class, getHostUrl(), someAppId, someCluster, somePublicNamespace, someDC); stop.set(true); ApolloConfigNotification notification = result.getBody(); assertEquals(HttpStatus.OK, result.getStatusCode()); assertEquals(somePublicNamespace, notification.getNamespaceName()); assertNotEquals(0, notification.getNotificationId()); } @Test(timeout = 5000L) @Sql(scripts = "/integration-test/test-release.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/integration-test/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testPollNotificationWthPublicNamespaceAsFile() throws Exception { String publicAppId = "somePublicAppId"; String someDC = "someDC"; AtomicBoolean stop = new AtomicBoolean(); periodicSendMessage(executorService, assembleKey(publicAppId, someDC, somePublicNamespace), stop); ResponseEntity result = restTemplate.getForEntity( "http://{baseurl}/notifications?appId={appId}&cluster={clusterName}&namespace={namespace}&dataCenter={dataCenter}", ApolloConfigNotification.class, getHostUrl(), someAppId, someCluster, somePublicNamespace + ".properties", someDC); stop.set(true); ApolloConfigNotification notification = result.getBody(); assertEquals(HttpStatus.OK, result.getStatusCode()); assertEquals(somePublicNamespace, notification.getNamespaceName()); assertNotEquals(0, notification.getNotificationId()); } @Test(timeout = 5000L) @Sql(scripts = "/integration-test/test-release.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/integration-test/test-release-message.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/integration-test/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testPollNotificationWithPublicNamespaceWithNotificationIdOutDated() throws Exception { long someOutDatedNotificationId = 1; ResponseEntity result = restTemplate.getForEntity( "http://{baseurl}/notifications?appId={appId}&cluster={clusterName}&namespace={namespace}¬ificationId={notificationId}", ApolloConfigNotification.class, getHostUrl(), someAppId, someCluster, somePublicNamespace, someOutDatedNotificationId); ApolloConfigNotification notification = result.getBody(); assertEquals(HttpStatus.OK, result.getStatusCode()); assertEquals(somePublicNamespace, notification.getNamespaceName()); assertEquals(20, notification.getNotificationId()); } } ================================================ FILE: apollo-configservice/src/test/java/com/ctrip/framework/apollo/configservice/integration/NotificationControllerV2IntegrationTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.configservice.integration; import com.ctrip.framework.apollo.core.dto.ApolloNotificationMessages; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import com.google.gson.Gson; import com.ctrip.framework.apollo.configservice.service.ReleaseMessageServiceWithCache; import com.ctrip.framework.apollo.core.ConfigConsts; import com.ctrip.framework.apollo.core.dto.ApolloConfigNotification; import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.test.context.jdbc.Sql; import org.springframework.test.util.ReflectionTestUtils; import java.util.List; import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicBoolean; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertTrue; /** * @author Jason Song(song_s@ctrip.com) */ public class NotificationControllerV2IntegrationTest extends AbstractBaseIntegrationTest { @Autowired private Gson gson; @Autowired private ReleaseMessageServiceWithCache releaseMessageServiceWithCache; private String someAppId; private String someCluster; private String defaultNamespace; private String somePublicNamespace; private ExecutorService executorService; private ParameterizedTypeReference> typeReference; @Before public void setUp() throws Exception { ReflectionTestUtils.invokeMethod(releaseMessageServiceWithCache, "reset"); someAppId = "someAppId"; someCluster = ConfigConsts.CLUSTER_NAME_DEFAULT; defaultNamespace = ConfigConsts.NAMESPACE_APPLICATION; somePublicNamespace = "somePublicNamespace"; executorService = Executors.newFixedThreadPool(1); typeReference = new ParameterizedTypeReference>() {}; } @Test(timeout = 5000L) @Sql(scripts = "/integration-test/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testPollNotificationWithDefaultNamespace() throws Exception { AtomicBoolean stop = new AtomicBoolean(); String key = assembleKey(someAppId, someCluster, defaultNamespace); periodicSendMessage(executorService, key, stop); ResponseEntity> result = restTemplate.exchange( "http://{baseurl}/notifications/v2?appId={appId}&cluster={clusterName}¬ifications={notifications}", HttpMethod.GET, null, typeReference, getHostUrl(), someAppId, someCluster, transformApolloConfigNotificationsToString(defaultNamespace, ConfigConsts.NOTIFICATION_ID_PLACEHOLDER)); stop.set(true); List notifications = result.getBody(); assertEquals(HttpStatus.OK, result.getStatusCode()); assertEquals(1, notifications.size()); assertEquals(defaultNamespace, notifications.get(0).getNamespaceName()); assertNotEquals(0, notifications.get(0).getNotificationId()); ApolloNotificationMessages messages = result.getBody().get(0).getMessages(); assertEquals(1, messages.getDetails().size()); assertTrue(messages.has(key)); assertNotEquals(ConfigConsts.NOTIFICATION_ID_PLACEHOLDER, messages.get(key).longValue()); } @Test(timeout = 5000L) @Sql(scripts = "/integration-test/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testPollNotificationWithDefaultNamespaceAsFile() throws Exception { AtomicBoolean stop = new AtomicBoolean(); String key = assembleKey(someAppId, someCluster, defaultNamespace); periodicSendMessage(executorService, key, stop); ResponseEntity> result = restTemplate.exchange( "http://{baseurl}/notifications/v2?appId={appId}&cluster={clusterName}¬ifications={notifications}", HttpMethod.GET, null, typeReference, getHostUrl(), someAppId, someCluster, transformApolloConfigNotificationsToString(defaultNamespace + ".properties", ConfigConsts.NOTIFICATION_ID_PLACEHOLDER)); stop.set(true); List notifications = result.getBody(); assertEquals(HttpStatus.OK, result.getStatusCode()); assertEquals(1, notifications.size()); assertEquals(defaultNamespace, notifications.get(0).getNamespaceName()); assertNotEquals(0, notifications.get(0).getNotificationId()); ApolloNotificationMessages messages = result.getBody().get(0).getMessages(); assertEquals(1, messages.getDetails().size()); assertTrue(messages.has(key)); assertNotEquals(ConfigConsts.NOTIFICATION_ID_PLACEHOLDER, messages.get(key).longValue()); } @Test @Sql(scripts = "/integration-test/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testPollNotificationWithMultipleNamespaces() throws Exception { AtomicBoolean stop = new AtomicBoolean(); String key = assembleKey(someAppId, someCluster, somePublicNamespace); periodicSendMessage(executorService, key, stop); ResponseEntity> result = restTemplate.exchange( "http://{baseurl}/notifications/v2?appId={appId}&cluster={clusterName}¬ifications={notifications}", HttpMethod.GET, null, typeReference, getHostUrl(), someAppId, someCluster, transformApolloConfigNotificationsToString(defaultNamespace + ".properties", ConfigConsts.NOTIFICATION_ID_PLACEHOLDER, defaultNamespace, ConfigConsts.NOTIFICATION_ID_PLACEHOLDER, somePublicNamespace, ConfigConsts.NOTIFICATION_ID_PLACEHOLDER)); stop.set(true); List notifications = result.getBody(); assertEquals(HttpStatus.OK, result.getStatusCode()); assertEquals(1, notifications.size()); assertEquals(somePublicNamespace, notifications.get(0).getNamespaceName()); assertNotEquals(0, notifications.get(0).getNotificationId()); ApolloNotificationMessages messages = result.getBody().get(0).getMessages(); assertEquals(1, messages.getDetails().size()); assertTrue(messages.has(key)); assertNotEquals(ConfigConsts.NOTIFICATION_ID_PLACEHOLDER, messages.get(key).longValue()); } @Test @Sql(scripts = "/integration-test/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testPollNotificationWithMultipleNamespacesAndIncorrectCase() throws Exception { AtomicBoolean stop = new AtomicBoolean(); String key = assembleKey(someAppId, someCluster, somePublicNamespace); periodicSendMessage(executorService, key, stop); String someDefaultNamespaceWithIncorrectCase = defaultNamespace.toUpperCase(); String somePublicNamespaceWithIncorrectCase = somePublicNamespace.toUpperCase(); ResponseEntity> result = restTemplate.exchange( "http://{baseurl}/notifications/v2?appId={appId}&cluster={clusterName}¬ifications={notifications}", HttpMethod.GET, null, typeReference, getHostUrl(), someAppId, someCluster, transformApolloConfigNotificationsToString(defaultNamespace + ".properties", ConfigConsts.NOTIFICATION_ID_PLACEHOLDER, someDefaultNamespaceWithIncorrectCase, ConfigConsts.NOTIFICATION_ID_PLACEHOLDER, somePublicNamespaceWithIncorrectCase, ConfigConsts.NOTIFICATION_ID_PLACEHOLDER)); stop.set(true); List notifications = result.getBody(); assertEquals(HttpStatus.OK, result.getStatusCode()); assertEquals(1, notifications.size()); assertEquals(somePublicNamespaceWithIncorrectCase, notifications.get(0).getNamespaceName()); assertNotEquals(0, notifications.get(0).getNotificationId()); ApolloNotificationMessages messages = result.getBody().get(0).getMessages(); assertEquals(1, messages.getDetails().size()); assertTrue(messages.has(key)); assertNotEquals(ConfigConsts.NOTIFICATION_ID_PLACEHOLDER, messages.get(key).longValue()); } @Test(timeout = 5000L) @Sql(scripts = "/integration-test/test-release.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/integration-test/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testPollNotificationWithPrivateNamespaceAsFile() throws Exception { String namespace = "someNamespace.xml"; AtomicBoolean stop = new AtomicBoolean(); String key = assembleKey(someAppId, ConfigConsts.CLUSTER_NAME_DEFAULT, namespace); periodicSendMessage(executorService, key, stop); ResponseEntity> result = restTemplate.exchange( "http://{baseurl}/notifications/v2?appId={appId}&cluster={clusterName}¬ifications={notifications}", HttpMethod.GET, null, typeReference, getHostUrl(), someAppId, someCluster, transformApolloConfigNotificationsToString(namespace, ConfigConsts.NOTIFICATION_ID_PLACEHOLDER)); stop.set(true); List notifications = result.getBody(); assertEquals(HttpStatus.OK, result.getStatusCode()); assertEquals(1, notifications.size()); assertEquals(namespace, notifications.get(0).getNamespaceName()); assertNotEquals(0, notifications.get(0).getNotificationId()); ApolloNotificationMessages messages = result.getBody().get(0).getMessages(); assertEquals(1, messages.getDetails().size()); assertTrue(messages.has(key)); assertNotEquals(ConfigConsts.NOTIFICATION_ID_PLACEHOLDER, messages.get(key).longValue()); } @Test(timeout = 5000L) @Sql(scripts = "/integration-test/test-release.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/integration-test/test-release-message.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/integration-test/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testPollNotificationWithDefaultNamespaceWithNotificationIdOutDated() throws Exception { long someOutDatedNotificationId = 1; ResponseEntity> result = restTemplate.exchange( "http://{baseurl}/notifications/v2?appId={appId}&cluster={clusterName}¬ifications={notifications}", HttpMethod.GET, null, typeReference, getHostUrl(), someAppId, someCluster, transformApolloConfigNotificationsToString(defaultNamespace, someOutDatedNotificationId)); long newNotificationId = 10; List notifications = result.getBody(); assertEquals(HttpStatus.OK, result.getStatusCode()); assertEquals(1, notifications.size()); assertEquals(defaultNamespace, notifications.get(0).getNamespaceName()); assertEquals(newNotificationId, notifications.get(0).getNotificationId()); String key = assembleKey(someAppId, ConfigConsts.CLUSTER_NAME_DEFAULT, ConfigConsts.NAMESPACE_APPLICATION); ApolloNotificationMessages messages = result.getBody().get(0).getMessages(); assertEquals(1, messages.getDetails().size()); assertTrue(messages.has(key)); assertEquals(newNotificationId, messages.get(key).longValue()); } @Test(timeout = 5000L) @Sql(scripts = "/integration-test/test-release.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/integration-test/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testPollNotificationWthPublicNamespaceAndNoDataCenter() throws Exception { String publicAppId = "somePublicAppId"; AtomicBoolean stop = new AtomicBoolean(); String key = assembleKey(publicAppId, ConfigConsts.CLUSTER_NAME_DEFAULT, somePublicNamespace); periodicSendMessage(executorService, key, stop); ResponseEntity> result = restTemplate.exchange( "http://{baseurl}/notifications/v2?appId={appId}&cluster={clusterName}¬ifications={notifications}", HttpMethod.GET, null, typeReference, getHostUrl(), someAppId, someCluster, transformApolloConfigNotificationsToString(somePublicNamespace, ConfigConsts.NOTIFICATION_ID_PLACEHOLDER)); stop.set(true); List notifications = result.getBody(); assertEquals(HttpStatus.OK, result.getStatusCode()); assertEquals(1, notifications.size()); assertEquals(somePublicNamespace, notifications.get(0).getNamespaceName()); assertNotEquals(0, notifications.get(0).getNotificationId()); ApolloNotificationMessages messages = result.getBody().get(0).getMessages(); assertEquals(1, messages.getDetails().size()); assertTrue(messages.has(key)); assertNotEquals(ConfigConsts.NOTIFICATION_ID_PLACEHOLDER, messages.get(key).longValue()); } @Test(timeout = 5000L) @Sql(scripts = "/integration-test/test-release.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/integration-test/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testPollNotificationWthPublicNamespaceAndDataCenter() throws Exception { String publicAppId = "somePublicAppId"; String someDC = "someDC"; AtomicBoolean stop = new AtomicBoolean(); String key = assembleKey(publicAppId, someDC, somePublicNamespace); periodicSendMessage(executorService, key, stop); ResponseEntity> result = restTemplate.exchange( "http://{baseurl}/notifications/v2?appId={appId}&cluster={clusterName}¬ifications={notifications}&dataCenter={dataCenter}", HttpMethod.GET, null, typeReference, getHostUrl(), someAppId, someCluster, transformApolloConfigNotificationsToString(somePublicNamespace, ConfigConsts.NOTIFICATION_ID_PLACEHOLDER), someDC); stop.set(true); List notifications = result.getBody(); assertEquals(HttpStatus.OK, result.getStatusCode()); assertEquals(1, notifications.size()); assertEquals(somePublicNamespace, notifications.get(0).getNamespaceName()); assertNotEquals(0, notifications.get(0).getNotificationId()); ApolloNotificationMessages messages = result.getBody().get(0).getMessages(); assertEquals(1, messages.getDetails().size()); assertTrue(messages.has(key)); assertNotEquals(ConfigConsts.NOTIFICATION_ID_PLACEHOLDER, messages.get(key).longValue()); } @Test(timeout = 10000L) @Sql(scripts = "/integration-test/test-release.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/integration-test/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testPollNotificationWthMultipleNamespacesAndMultipleNamespacesChanged() throws Exception { String publicAppId = "somePublicAppId"; String someDC = "someDC"; AtomicBoolean stop = new AtomicBoolean(); String key = assembleKey(publicAppId, someDC, somePublicNamespace); periodicSendMessage(executorService, key, stop); ResponseEntity> result = restTemplate.exchange( "http://{baseurl}/notifications/v2?appId={appId}&cluster={clusterName}¬ifications={notifications}&dataCenter={dataCenter}", HttpMethod.GET, null, typeReference, getHostUrl(), someAppId, someCluster, transformApolloConfigNotificationsToString(defaultNamespace, ConfigConsts.NOTIFICATION_ID_PLACEHOLDER, somePublicNamespace, ConfigConsts.NOTIFICATION_ID_PLACEHOLDER), someDC); stop.set(true); List notifications = result.getBody(); assertEquals(HttpStatus.OK, result.getStatusCode()); assertEquals(1, notifications.size()); assertEquals(somePublicNamespace, notifications.get(0).getNamespaceName()); assertNotEquals(0, notifications.get(0).getNotificationId()); ApolloNotificationMessages messages = result.getBody().get(0).getMessages(); assertEquals(1, messages.getDetails().size()); assertTrue(messages.has(key)); assertNotEquals(ConfigConsts.NOTIFICATION_ID_PLACEHOLDER, messages.get(key).longValue()); } @Test(timeout = 5000L) @Sql(scripts = "/integration-test/test-release.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/integration-test/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testPollNotificationWthPublicNamespaceAsFile() throws Exception { String publicAppId = "somePublicAppId"; String someDC = "someDC"; AtomicBoolean stop = new AtomicBoolean(); String key = assembleKey(publicAppId, someDC, somePublicNamespace); periodicSendMessage(executorService, key, stop); ResponseEntity> result = restTemplate.exchange( "http://{baseurl}/notifications/v2?appId={appId}&cluster={clusterName}¬ifications={notifications}&dataCenter={dataCenter}", HttpMethod.GET, null, typeReference, getHostUrl(), someAppId, someCluster, transformApolloConfigNotificationsToString(somePublicNamespace + ".properties", ConfigConsts.NOTIFICATION_ID_PLACEHOLDER), someDC); stop.set(true); List notifications = result.getBody(); assertEquals(HttpStatus.OK, result.getStatusCode()); assertEquals(1, notifications.size()); assertEquals(somePublicNamespace, notifications.get(0).getNamespaceName()); assertNotEquals(0, notifications.get(0).getNotificationId()); ApolloNotificationMessages messages = result.getBody().get(0).getMessages(); assertEquals(1, messages.getDetails().size()); assertTrue(messages.has(key)); assertNotEquals(ConfigConsts.NOTIFICATION_ID_PLACEHOLDER, messages.get(key).longValue()); } @Test(timeout = 5000L) @Sql(scripts = "/integration-test/test-release.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/integration-test/test-release-message.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/integration-test/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testPollNotificationWithPublicNamespaceWithNotificationIdOutDated() throws Exception { String publicAppId = "somePublicAppId"; long someOutDatedNotificationId = 1; ResponseEntity> result = restTemplate.exchange( "http://{baseurl}/notifications/v2?appId={appId}&cluster={clusterName}¬ifications={notifications}", HttpMethod.GET, null, typeReference, getHostUrl(), someAppId, someCluster, transformApolloConfigNotificationsToString(somePublicNamespace, someOutDatedNotificationId)); long newNotificationId = 20; List notifications = result.getBody(); assertEquals(HttpStatus.OK, result.getStatusCode()); assertEquals(1, notifications.size()); assertEquals(somePublicNamespace, notifications.get(0).getNamespaceName()); assertEquals(newNotificationId, notifications.get(0).getNotificationId()); String key = assembleKey(publicAppId, ConfigConsts.CLUSTER_NAME_DEFAULT, somePublicNamespace); ApolloNotificationMessages messages = result.getBody().get(0).getMessages(); assertEquals(1, messages.getDetails().size()); assertTrue(messages.has(key)); assertEquals(newNotificationId, messages.get(key).longValue()); } @Test(timeout = 5000L) @Sql(scripts = "/integration-test/test-release.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/integration-test/test-release-message.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/integration-test/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testPollNotificationWithMultiplePublicNamespaceWithIncorrectCaseWithNotificationIdOutDated() throws Exception { String publicAppId = "somePublicAppId"; long someOutDatedNotificationId = 1; long newNotificationId = 20; String somePublicNameWithIncorrectCase = somePublicNamespace.toUpperCase(); // the same namespace with difference character case, and difference notification id ResponseEntity> result = restTemplate.exchange( "http://{baseurl}/notifications/v2?appId={appId}&cluster={clusterName}¬ifications={notifications}", HttpMethod.GET, null, typeReference, getHostUrl(), someAppId, someCluster, transformApolloConfigNotificationsToString(somePublicNamespace, newNotificationId, somePublicNameWithIncorrectCase, someOutDatedNotificationId)); List notifications = result.getBody(); assertEquals(HttpStatus.OK, result.getStatusCode()); assertEquals(1, notifications.size()); assertEquals(somePublicNameWithIncorrectCase, notifications.get(0).getNamespaceName()); assertEquals(newNotificationId, notifications.get(0).getNotificationId()); String key = assembleKey(publicAppId, ConfigConsts.CLUSTER_NAME_DEFAULT, somePublicNamespace); ApolloNotificationMessages messages = result.getBody().get(0).getMessages(); assertEquals(1, messages.getDetails().size()); assertTrue(messages.has(key)); assertEquals(newNotificationId, messages.get(key).longValue()); } @Test(timeout = 5000L) @Sql(scripts = "/integration-test/test-release.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/integration-test/test-release-message.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/integration-test/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testPollNotificationWithMultiplePublicNamespaceWithIncorrectCase2WithNotificationIdOutDated() throws Exception { String publicAppId = "somePublicAppId"; long someOutDatedNotificationId = 1; long newNotificationId = 20; String somePublicNameWithIncorrectCase = somePublicNamespace.toUpperCase(); // the same namespace with difference character case, and difference notification id ResponseEntity> result = restTemplate.exchange( "http://{baseurl}/notifications/v2?appId={appId}&cluster={clusterName}¬ifications={notifications}", HttpMethod.GET, null, typeReference, getHostUrl(), someAppId, someCluster, transformApolloConfigNotificationsToString(somePublicNameWithIncorrectCase, someOutDatedNotificationId, somePublicNamespace, newNotificationId)); List notifications = result.getBody(); assertEquals(HttpStatus.OK, result.getStatusCode()); assertEquals(1, notifications.size()); assertEquals(somePublicNameWithIncorrectCase, notifications.get(0).getNamespaceName()); assertEquals(newNotificationId, notifications.get(0).getNotificationId()); String key = assembleKey(publicAppId, ConfigConsts.CLUSTER_NAME_DEFAULT, somePublicNamespace); ApolloNotificationMessages messages = result.getBody().get(0).getMessages(); assertEquals(1, messages.getDetails().size()); assertTrue(messages.has(key)); assertEquals(newNotificationId, messages.get(key).longValue()); } @Test(timeout = 5000L) @Sql(scripts = "/integration-test/test-release.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/integration-test/test-release-message.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/integration-test/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testPollNotificationWithMultiplePublicNamespaceWithIncorrectCase3WithNotificationIdOutDated() throws Exception { String publicAppId = "somePublicAppId"; long someOutDatedNotificationId = 1; long newNotificationId = 20; String somePublicNameWithIncorrectCase = somePublicNamespace.toUpperCase(); // the same namespace with difference character case, and difference notification id ResponseEntity> result = restTemplate.exchange( "http://{baseurl}/notifications/v2?appId={appId}&cluster={clusterName}¬ifications={notifications}", HttpMethod.GET, null, typeReference, getHostUrl(), someAppId, someCluster, transformApolloConfigNotificationsToString(somePublicNameWithIncorrectCase, newNotificationId, somePublicNamespace, someOutDatedNotificationId)); List notifications = result.getBody(); assertEquals(HttpStatus.OK, result.getStatusCode()); assertEquals(1, notifications.size()); assertEquals(somePublicNamespace, notifications.get(0).getNamespaceName()); assertEquals(newNotificationId, notifications.get(0).getNotificationId()); String key = assembleKey(publicAppId, ConfigConsts.CLUSTER_NAME_DEFAULT, somePublicNamespace); ApolloNotificationMessages messages = result.getBody().get(0).getMessages(); assertEquals(1, messages.getDetails().size()); assertTrue(messages.has(key)); assertEquals(newNotificationId, messages.get(key).longValue()); } @Test(timeout = 5000L) @Sql(scripts = "/integration-test/test-release.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/integration-test/test-release-message.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/integration-test/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testPollNotificationWithMultiplePublicNamespaceWithIncorrectCase4WithNotificationIdOutDated() throws Exception { String publicAppId = "somePublicAppId"; long someOutDatedNotificationId = 1; long newNotificationId = 20; String somePublicNameWithIncorrectCase = somePublicNamespace.toUpperCase(); // the same namespace with difference character case, and difference notification id ResponseEntity> result = restTemplate.exchange( "http://{baseurl}/notifications/v2?appId={appId}&cluster={clusterName}¬ifications={notifications}", HttpMethod.GET, null, typeReference, getHostUrl(), someAppId, someCluster, transformApolloConfigNotificationsToString(somePublicNamespace, someOutDatedNotificationId, somePublicNameWithIncorrectCase, newNotificationId)); List notifications = result.getBody(); assertEquals(HttpStatus.OK, result.getStatusCode()); assertEquals(1, notifications.size()); assertEquals(somePublicNamespace, notifications.get(0).getNamespaceName()); assertEquals(newNotificationId, notifications.get(0).getNotificationId()); String key = assembleKey(publicAppId, ConfigConsts.CLUSTER_NAME_DEFAULT, somePublicNamespace); ApolloNotificationMessages messages = result.getBody().get(0).getMessages(); assertEquals(1, messages.getDetails().size()); assertTrue(messages.has(key)); assertEquals(newNotificationId, messages.get(key).longValue()); } @Test(timeout = 5000L) @Sql(scripts = "/integration-test/test-release.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/integration-test/test-release-message.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/integration-test/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testPollNotificationWithMultipleNamespacesAndNotificationIdsOutDated() throws Exception { String publicAppId = "somePublicAppId"; long someOutDatedNotificationId = 1; long newDefaultNamespaceNotificationId = 10; long newPublicNamespaceNotification = 20; ResponseEntity> result = restTemplate.exchange( "http://{baseurl}/notifications/v2?appId={appId}&cluster={clusterName}¬ifications={notifications}", HttpMethod.GET, null, typeReference, getHostUrl(), someAppId, someCluster, transformApolloConfigNotificationsToString(somePublicNamespace, someOutDatedNotificationId, defaultNamespace, someOutDatedNotificationId)); List notifications = result.getBody(); assertEquals(HttpStatus.OK, result.getStatusCode()); assertEquals(2, notifications.size()); Set outDatedNamespaces = Sets.newHashSet(notifications.get(0).getNamespaceName(), notifications.get(1).getNamespaceName()); assertEquals(Sets.newHashSet(defaultNamespace, somePublicNamespace), outDatedNamespaces); Set newNotificationIds = Sets.newHashSet(notifications.get(0).getNotificationId(), notifications.get(1).getNotificationId()); assertEquals(Sets.newHashSet(newDefaultNamespaceNotificationId, newPublicNamespaceNotification), newNotificationIds); String defaultNamespaceKey = assembleKey(someAppId, ConfigConsts.CLUSTER_NAME_DEFAULT, ConfigConsts.NAMESPACE_APPLICATION); String publicNamespaceKey = assembleKey(publicAppId, ConfigConsts.CLUSTER_NAME_DEFAULT, somePublicNamespace); ApolloNotificationMessages firstMessages = notifications.get(0).getMessages(); ApolloNotificationMessages secondMessages = notifications.get(1).getMessages(); assertEquals(1, firstMessages.getDetails().size()); assertEquals(1, secondMessages.getDetails().size()); assertTrue((firstMessages.has(defaultNamespaceKey) && firstMessages.get(defaultNamespaceKey).equals(newDefaultNamespaceNotificationId)) || (firstMessages.has(publicNamespaceKey) && firstMessages.get(publicNamespaceKey).equals(newPublicNamespaceNotification))); assertTrue((secondMessages.has(defaultNamespaceKey) && secondMessages.get(defaultNamespaceKey).equals(newDefaultNamespaceNotificationId)) || (secondMessages.has(publicNamespaceKey) && secondMessages.get(publicNamespaceKey).equals(newPublicNamespaceNotification))); } @Test(timeout = 5000L) @Sql(scripts = "/integration-test/test-release.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/integration-test/test-release-message.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/integration-test/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testPollNotificationWithMultipleNamespacesAndNotificationIdsOutDatedAndIncorrectCase() throws Exception { String publicAppId = "somePublicAppId"; long someOutDatedNotificationId = 1; long newDefaultNamespaceNotificationId = 10; long newPublicNamespaceNotification = 20; String someDefaultNamespaceWithIncorrectCase = defaultNamespace.toUpperCase(); String somePublicNamespaceWithIncorrectCase = somePublicNamespace.toUpperCase(); ResponseEntity> result = restTemplate.exchange( "http://{baseurl}/notifications/v2?appId={appId}&cluster={clusterName}¬ifications={notifications}", HttpMethod.GET, null, typeReference, getHostUrl(), someAppId, someCluster, transformApolloConfigNotificationsToString(somePublicNamespaceWithIncorrectCase, someOutDatedNotificationId, someDefaultNamespaceWithIncorrectCase, someOutDatedNotificationId)); List notifications = result.getBody(); assertEquals(HttpStatus.OK, result.getStatusCode()); assertEquals(2, notifications.size()); Set outDatedNamespaces = Sets.newHashSet(notifications.get(0).getNamespaceName(), notifications.get(1).getNamespaceName()); assertEquals(Sets.newHashSet(someDefaultNamespaceWithIncorrectCase, somePublicNamespaceWithIncorrectCase), outDatedNamespaces); Set newNotificationIds = Sets.newHashSet(notifications.get(0).getNotificationId(), notifications.get(1).getNotificationId()); assertEquals(Sets.newHashSet(newDefaultNamespaceNotificationId, newPublicNamespaceNotification), newNotificationIds); String defaultNamespaceKey = assembleKey(someAppId, ConfigConsts.CLUSTER_NAME_DEFAULT, ConfigConsts.NAMESPACE_APPLICATION); String publicNamespaceKey = assembleKey(publicAppId, ConfigConsts.CLUSTER_NAME_DEFAULT, somePublicNamespace); ApolloNotificationMessages firstMessages = notifications.get(0).getMessages(); ApolloNotificationMessages secondMessages = notifications.get(1).getMessages(); assertEquals(1, firstMessages.getDetails().size()); assertEquals(1, secondMessages.getDetails().size()); assertTrue((firstMessages.has(defaultNamespaceKey) && firstMessages.get(defaultNamespaceKey).equals(newDefaultNamespaceNotificationId)) || (firstMessages.has(publicNamespaceKey) && firstMessages.get(publicNamespaceKey).equals(newPublicNamespaceNotification))); assertTrue((secondMessages.has(defaultNamespaceKey) && secondMessages.get(defaultNamespaceKey).equals(newDefaultNamespaceNotificationId)) || (secondMessages.has(publicNamespaceKey) && secondMessages.get(publicNamespaceKey).equals(newPublicNamespaceNotification))); } private String transformApolloConfigNotificationsToString(String namespace, long notificationId) { List notifications = Lists.newArrayList(assembleApolloConfigNotification(namespace, notificationId)); return gson.toJson(notifications); } private String transformApolloConfigNotificationsToString(String namespace, long notificationId, String anotherNamespace, long anotherNotificationId) { List notifications = Lists.newArrayList(assembleApolloConfigNotification(namespace, notificationId), assembleApolloConfigNotification(anotherNamespace, anotherNotificationId)); return gson.toJson(notifications); } private String transformApolloConfigNotificationsToString(String namespace, long notificationId, String anotherNamespace, long anotherNotificationId, String yetAnotherNamespace, long yetAnotherNotificationId) { List notifications = Lists.newArrayList(assembleApolloConfigNotification(namespace, notificationId), assembleApolloConfigNotification(anotherNamespace, anotherNotificationId), assembleApolloConfigNotification(yetAnotherNamespace, yetAnotherNotificationId)); return gson.toJson(notifications); } private ApolloConfigNotification assembleApolloConfigNotification(String namespace, long notificationId) { return new ApolloConfigNotification(namespace, notificationId); } } ================================================ FILE: apollo-configservice/src/test/java/com/ctrip/framework/apollo/configservice/service/AccessKeyServiceWithCacheTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.configservice.service; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.Mockito.when; import static org.awaitility.Awaitility.*; import com.ctrip.framework.apollo.biz.config.BizConfig; import com.ctrip.framework.apollo.biz.entity.AccessKey; import com.ctrip.framework.apollo.biz.repository.AccessKeyRepository; import com.google.common.collect.Lists; import java.util.Date; import java.util.concurrent.TimeUnit; import org.awaitility.Awaitility; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; /** * @author nisiyong */ @RunWith(MockitoJUnitRunner.Silent.class) public class AccessKeyServiceWithCacheTest { private AccessKeyServiceWithCache accessKeyServiceWithCache; @Mock private AccessKeyRepository accessKeyRepository; @Mock private BizConfig bizConfig; private int scanInterval; private TimeUnit scanIntervalTimeUnit; @Before public void setUp() { accessKeyServiceWithCache = new AccessKeyServiceWithCache(accessKeyRepository, bizConfig); scanInterval = 50; scanIntervalTimeUnit = TimeUnit.MILLISECONDS; when(bizConfig.accessKeyCacheScanInterval()).thenReturn(scanInterval); when(bizConfig.accessKeyCacheScanIntervalTimeUnit()).thenReturn(scanIntervalTimeUnit); when(bizConfig.accessKeyCacheRebuildInterval()).thenReturn(scanInterval); when(bizConfig.accessKeyCacheRebuildIntervalTimeUnit()).thenReturn(scanIntervalTimeUnit); Awaitility.reset(); Awaitility.setDefaultTimeout(scanInterval * 100, scanIntervalTimeUnit); Awaitility.setDefaultPollInterval(scanInterval, scanIntervalTimeUnit); } @Test public void testGetAvailableSecrets() throws Exception { String appId = "someAppId"; AccessKey firstAccessKey = assembleAccessKey(1L, appId, "secret-1", false, false, 1577808000000L); AccessKey secondAccessKey = assembleAccessKey(2L, appId, "secret-2", false, false, 1577808001000L); AccessKey thirdAccessKey = assembleAccessKey(3L, appId, "secret-3", true, false, 1577808005000L); // Initialize accessKeyServiceWithCache.afterPropertiesSet(); assertThat(accessKeyServiceWithCache.getAvailableSecrets(appId)).isEmpty(); // Add access key, disable by default when(accessKeyRepository .findFirst500ByDataChangeLastModifiedTimeGreaterThanEqualAndDataChangeLastModifiedTimeLessThanOrderByDataChangeLastModifiedTimeAsc( new Date(0L), new Date())) .thenReturn(Lists.newArrayList(firstAccessKey, secondAccessKey)); when(accessKeyRepository.findAllById(anyList())) .thenReturn(Lists.newArrayList(firstAccessKey, secondAccessKey)); await().untilAsserted( () -> assertThat(accessKeyServiceWithCache.getAvailableSecrets(appId)).isEmpty()); } public AccessKey assembleAccessKey(Long id, String appId, String secret, boolean enabled, boolean deleted, long dataChangeLastModifiedTime) { AccessKey accessKey = new AccessKey(); accessKey.setId(id); accessKey.setAppId(appId); accessKey.setSecret(secret); accessKey.setEnabled(enabled); accessKey.setDeleted(deleted); accessKey.setDataChangeLastModifiedTime(new Date(dataChangeLastModifiedTime)); return accessKey; } /** * the referenced object is not reclaimable by garbage collection at least until after the * invocation of this method. see the java 9 method {@link java.lang.ref.Reference#reachabilityFence} * see the netty consistency method for JDK 6-8 {@link io.netty.util.ResourceLeakDetector.DefaultResourceLeak#reachabilityFence0} * * @param ref the reference */ private static void reachabilityFence(Object ref) { if (ref != null) { synchronized (ref) { } } } } ================================================ FILE: apollo-configservice/src/test/java/com/ctrip/framework/apollo/configservice/service/AppNamespaceServiceWithCacheTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.configservice.service; import com.ctrip.framework.apollo.biz.config.BizConfig; import com.ctrip.framework.apollo.biz.repository.AppNamespaceRepository; import com.ctrip.framework.apollo.common.entity.AppNamespace; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import org.awaitility.Awaitility; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import java.util.Calendar; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.List; import java.util.Set; import java.util.concurrent.TimeUnit; import static org.awaitility.Awaitility.await; import static org.junit.Assert.*; import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.Mockito.when; /** * @author Jason Song(song_s@ctrip.com) */ @RunWith(MockitoJUnitRunner.Silent.class) public class AppNamespaceServiceWithCacheTest { private AppNamespaceServiceWithCache appNamespaceServiceWithCache; @Mock private AppNamespaceRepository appNamespaceRepository; @Mock private BizConfig bizConfig; private int scanInterval; private TimeUnit scanIntervalTimeUnit; private Comparator appNamespaceComparator = (o1, o2) -> (int) (o1.getId() - o2.getId()); @Before public void setUp() throws Exception { appNamespaceServiceWithCache = new AppNamespaceServiceWithCache(appNamespaceRepository, bizConfig); scanInterval = 50; scanIntervalTimeUnit = TimeUnit.MILLISECONDS; when(bizConfig.appNamespaceCacheRebuildInterval()).thenReturn(scanInterval); when(bizConfig.appNamespaceCacheRebuildIntervalTimeUnit()).thenReturn(scanIntervalTimeUnit); when(bizConfig.appNamespaceCacheScanInterval()).thenReturn(scanInterval); when(bizConfig.appNamespaceCacheScanIntervalTimeUnit()).thenReturn(scanIntervalTimeUnit); Awaitility.reset(); Awaitility.setDefaultTimeout(scanInterval * 100, scanIntervalTimeUnit); Awaitility.setDefaultPollInterval(scanInterval, scanIntervalTimeUnit); } @Test public void testAppNamespace() throws Exception { String someAppId = "someAppId"; String somePrivateNamespace = "somePrivateNamespace"; String somePrivateNamespaceWithIncorrectCase = somePrivateNamespace.toUpperCase(); long somePrivateNamespaceId = 1; String yetAnotherPrivateNamespace = "anotherPrivateNamespace"; long yetAnotherPrivateNamespaceId = 4; String anotherPublicNamespace = "anotherPublicNamespace"; long anotherPublicNamespaceId = 5; String somePublicAppId = "somePublicAppId"; String somePublicNamespace = "somePublicNamespace"; String somePublicNamespaceWithIncorrectCase = somePublicNamespace.toUpperCase(); long somePublicNamespaceId = 2; String anotherPrivateNamespace = "anotherPrivateNamespace"; long anotherPrivateNamespaceId = 3; AppNamespace somePrivateAppNamespace = assembleAppNamespace(somePrivateNamespaceId, someAppId, somePrivateNamespace, false); AppNamespace somePublicAppNamespace = assembleAppNamespace(somePublicNamespaceId, somePublicAppId, somePublicNamespace, true); AppNamespace anotherPrivateAppNamespace = assembleAppNamespace(anotherPrivateNamespaceId, somePublicAppId, anotherPrivateNamespace, false); AppNamespace yetAnotherPrivateAppNamespace = assembleAppNamespace(yetAnotherPrivateNamespaceId, someAppId, yetAnotherPrivateNamespace, false); AppNamespace anotherPublicAppNamespace = assembleAppNamespace(anotherPublicNamespaceId, someAppId, anotherPublicNamespace, true); Set someAppIdNamespaces = Sets.newHashSet(somePrivateNamespace, yetAnotherPrivateNamespace, anotherPublicNamespace); Set someAppIdNamespacesWithIncorrectCase = Sets.newHashSet( somePrivateNamespaceWithIncorrectCase, yetAnotherPrivateNamespace, anotherPublicNamespace); Set somePublicAppIdNamespaces = Sets.newHashSet(somePublicNamespace, anotherPrivateNamespace); Set publicNamespaces = Sets.newHashSet(somePublicNamespace, anotherPublicNamespace); Set publicNamespacesWithIncorrectCase = Sets.newHashSet(somePublicNamespaceWithIncorrectCase, anotherPublicNamespace); List appNamespaceIds = Lists.newArrayList(somePrivateNamespaceId, somePublicNamespaceId, anotherPrivateNamespaceId, yetAnotherPrivateNamespaceId, anotherPublicNamespaceId); List allAppNamespaces = Lists.newArrayList(somePrivateAppNamespace, somePublicAppNamespace, anotherPrivateAppNamespace, yetAnotherPrivateAppNamespace, anotherPublicAppNamespace); // Test init appNamespaceServiceWithCache.afterPropertiesSet(); // Should have no record now assertNull( appNamespaceServiceWithCache.findByAppIdAndNamespace(someAppId, somePrivateNamespace)); assertNull(appNamespaceServiceWithCache.findByAppIdAndNamespace(someAppId, somePrivateNamespaceWithIncorrectCase)); assertNull(appNamespaceServiceWithCache.findByAppIdAndNamespace(someAppId, yetAnotherPrivateNamespace)); assertNull( appNamespaceServiceWithCache.findByAppIdAndNamespace(someAppId, anotherPublicNamespace)); assertTrue(appNamespaceServiceWithCache.findByAppIdAndNamespaces(someAppId, someAppIdNamespaces) .isEmpty()); assertTrue(appNamespaceServiceWithCache .findByAppIdAndNamespaces(someAppId, someAppIdNamespacesWithIncorrectCase).isEmpty()); assertNull( appNamespaceServiceWithCache.findByAppIdAndNamespace(somePublicAppId, somePublicNamespace)); assertNull(appNamespaceServiceWithCache.findByAppIdAndNamespace(somePublicAppId, somePublicNamespaceWithIncorrectCase)); assertNull(appNamespaceServiceWithCache.findByAppIdAndNamespace(somePublicAppId, anotherPrivateNamespace)); assertTrue(appNamespaceServiceWithCache .findByAppIdAndNamespaces(somePublicAppId, somePublicAppIdNamespaces).isEmpty()); assertNull(appNamespaceServiceWithCache.findPublicNamespaceByName(somePublicNamespace)); assertNull(appNamespaceServiceWithCache .findPublicNamespaceByName(somePublicNamespaceWithIncorrectCase)); assertNull(appNamespaceServiceWithCache.findPublicNamespaceByName(anotherPublicNamespace)); assertTrue( appNamespaceServiceWithCache.findPublicNamespacesByNames(publicNamespaces).isEmpty()); assertTrue(appNamespaceServiceWithCache .findPublicNamespacesByNames(publicNamespacesWithIncorrectCase).isEmpty()); // Add 1 private namespace and 1 public namespace when(appNamespaceRepository.findFirst500ByIdGreaterThanOrderByIdAsc(0)) .thenReturn(Lists.newArrayList(somePrivateAppNamespace, somePublicAppNamespace)); when(appNamespaceRepository .findAllById(Lists.newArrayList(somePrivateNamespaceId, somePublicNamespaceId))) .thenReturn(Lists.newArrayList(somePrivateAppNamespace, somePublicAppNamespace)); await().untilAsserted(() -> { assertEquals(somePrivateAppNamespace, appNamespaceServiceWithCache.findByAppIdAndNamespace(someAppId, somePrivateNamespace)); assertEquals(somePrivateAppNamespace, appNamespaceServiceWithCache .findByAppIdAndNamespace(someAppId, somePrivateNamespaceWithIncorrectCase)); check(Lists.newArrayList(somePrivateAppNamespace), appNamespaceServiceWithCache.findByAppIdAndNamespaces(someAppId, someAppIdNamespaces)); check(Lists.newArrayList(somePrivateAppNamespace), appNamespaceServiceWithCache .findByAppIdAndNamespaces(someAppId, someAppIdNamespacesWithIncorrectCase)); assertEquals(somePublicAppNamespace, appNamespaceServiceWithCache .findByAppIdAndNamespace(somePublicAppId, somePublicNamespace)); assertEquals(somePublicAppNamespace, appNamespaceServiceWithCache .findByAppIdAndNamespace(somePublicAppId, somePublicNamespaceWithIncorrectCase)); check(Lists.newArrayList(somePublicAppNamespace), appNamespaceServiceWithCache .findByAppIdAndNamespaces(somePublicAppId, somePublicAppIdNamespaces)); assertEquals(somePublicAppNamespace, appNamespaceServiceWithCache.findPublicNamespaceByName(somePublicNamespace)); assertEquals(somePublicAppNamespace, appNamespaceServiceWithCache .findPublicNamespaceByName(somePublicNamespaceWithIncorrectCase)); check(Lists.newArrayList(somePublicAppNamespace), appNamespaceServiceWithCache.findPublicNamespacesByNames(publicNamespaces)); check(Lists.newArrayList(somePublicAppNamespace), appNamespaceServiceWithCache .findPublicNamespacesByNames(publicNamespacesWithIncorrectCase)); }); // Add 2 private namespaces and 1 public namespace when(appNamespaceRepository.findFirst500ByIdGreaterThanOrderByIdAsc(somePublicNamespaceId)) .thenReturn(Lists.newArrayList(anotherPrivateAppNamespace, yetAnotherPrivateAppNamespace, anotherPublicAppNamespace)); when(appNamespaceRepository.findAllById(appNamespaceIds)).thenReturn(allAppNamespaces); await().untilAsserted(() -> { check( Lists.newArrayList(somePrivateAppNamespace, yetAnotherPrivateAppNamespace, anotherPublicAppNamespace), Lists.newArrayList( appNamespaceServiceWithCache.findByAppIdAndNamespace(someAppId, somePrivateNamespace), appNamespaceServiceWithCache.findByAppIdAndNamespace(someAppId, yetAnotherPrivateNamespace), appNamespaceServiceWithCache.findByAppIdAndNamespace(someAppId, anotherPublicNamespace))); check( Lists.newArrayList(somePrivateAppNamespace, yetAnotherPrivateAppNamespace, anotherPublicAppNamespace), appNamespaceServiceWithCache.findByAppIdAndNamespaces(someAppId, someAppIdNamespaces)); check(Lists.newArrayList(somePublicAppNamespace, anotherPrivateAppNamespace), Lists.newArrayList( appNamespaceServiceWithCache.findByAppIdAndNamespace(somePublicAppId, somePublicNamespace), appNamespaceServiceWithCache.findByAppIdAndNamespace(somePublicAppId, anotherPrivateNamespace))); check(Lists.newArrayList(somePublicAppNamespace, anotherPrivateAppNamespace), appNamespaceServiceWithCache.findByAppIdAndNamespaces(somePublicAppId, somePublicAppIdNamespaces)); check(Lists.newArrayList(somePublicAppNamespace, anotherPublicAppNamespace), Lists.newArrayList( appNamespaceServiceWithCache.findPublicNamespaceByName(somePublicNamespace), appNamespaceServiceWithCache.findPublicNamespaceByName(anotherPublicNamespace))); check(Lists.newArrayList(somePublicAppNamespace, anotherPublicAppNamespace), appNamespaceServiceWithCache.findPublicNamespacesByNames(publicNamespaces)); }); // Update name String somePrivateNamespaceNew = "somePrivateNamespaceNew"; AppNamespace somePrivateAppNamespaceNew = assembleAppNamespace(somePrivateAppNamespace.getId(), somePrivateAppNamespace.getAppId(), somePrivateNamespaceNew, somePrivateAppNamespace.isPublic()); somePrivateAppNamespaceNew.setDataChangeLastModifiedTime( newDateWithDelta(somePrivateAppNamespace.getDataChangeLastModifiedTime(), 1)); // Update appId String someAppIdNew = "someAppIdNew"; AppNamespace yetAnotherPrivateAppNamespaceNew = assembleAppNamespace(yetAnotherPrivateAppNamespace.getId(), someAppIdNew, yetAnotherPrivateAppNamespace.getName(), false); yetAnotherPrivateAppNamespaceNew.setDataChangeLastModifiedTime( newDateWithDelta(yetAnotherPrivateAppNamespace.getDataChangeLastModifiedTime(), 1)); // Update isPublic AppNamespace somePublicAppNamespaceNew = assembleAppNamespace(somePublicAppNamespace.getId(), somePublicAppNamespace.getAppId(), somePublicAppNamespace.getName(), !somePublicAppNamespace.isPublic()); somePublicAppNamespaceNew.setDataChangeLastModifiedTime( newDateWithDelta(somePublicAppNamespace.getDataChangeLastModifiedTime(), 1)); // Delete 1 private and 1 public // should prepare for the case after deleted first, or in 2 rebuild intervals, all will be // deleted List appNamespaceIdsAfterDelete = Lists.newArrayList(somePrivateNamespaceId, somePublicNamespaceId, yetAnotherPrivateNamespaceId); when(appNamespaceRepository.findAllById(appNamespaceIdsAfterDelete)) .thenReturn(Lists.newArrayList(somePrivateAppNamespaceNew, yetAnotherPrivateAppNamespaceNew, somePublicAppNamespaceNew)); // do delete when(appNamespaceRepository.findAllById(appNamespaceIds)).thenReturn(Lists.newArrayList( somePrivateAppNamespaceNew, yetAnotherPrivateAppNamespaceNew, somePublicAppNamespaceNew)); await().untilAsserted(() -> { assertNull( appNamespaceServiceWithCache.findByAppIdAndNamespace(someAppId, somePrivateNamespace)); assertNull(appNamespaceServiceWithCache.findByAppIdAndNamespace(someAppId, yetAnotherPrivateNamespace)); assertNull( appNamespaceServiceWithCache.findByAppIdAndNamespace(someAppId, anotherPublicNamespace)); check(Collections.emptyList(), appNamespaceServiceWithCache.findByAppIdAndNamespaces(someAppId, someAppIdNamespaces)); assertEquals(somePublicAppNamespaceNew, appNamespaceServiceWithCache .findByAppIdAndNamespace(somePublicAppId, somePublicNamespace)); check(Lists.newArrayList(somePublicAppNamespaceNew), appNamespaceServiceWithCache .findByAppIdAndNamespaces(somePublicAppId, somePublicAppIdNamespaces)); assertNull(appNamespaceServiceWithCache.findPublicNamespaceByName(somePublicNamespace)); assertNull(appNamespaceServiceWithCache.findPublicNamespaceByName(anotherPublicNamespace)); check(Collections.emptyList(), appNamespaceServiceWithCache.findPublicNamespacesByNames(publicNamespaces)); assertEquals(somePrivateAppNamespaceNew, appNamespaceServiceWithCache.findByAppIdAndNamespace(someAppId, somePrivateNamespaceNew)); check(Lists.newArrayList(somePrivateAppNamespaceNew), appNamespaceServiceWithCache .findByAppIdAndNamespaces(someAppId, Sets.newHashSet(somePrivateNamespaceNew))); assertEquals(yetAnotherPrivateAppNamespaceNew, appNamespaceServiceWithCache .findByAppIdAndNamespace(someAppIdNew, yetAnotherPrivateNamespace)); check(Lists.newArrayList(yetAnotherPrivateAppNamespaceNew), appNamespaceServiceWithCache .findByAppIdAndNamespaces(someAppIdNew, Sets.newHashSet(yetAnotherPrivateNamespace))); }); } private void check(List someList, List anotherList) { someList.sort(appNamespaceComparator); anotherList.sort(appNamespaceComparator); assertEquals(someList, anotherList); } private Date newDateWithDelta(Date date, int deltaInSeconds) { Calendar calendar = Calendar.getInstance(); calendar.setTime(date); calendar.add(Calendar.SECOND, deltaInSeconds); return calendar.getTime(); } private AppNamespace assembleAppNamespace(long id, String appId, String name, boolean isPublic) { AppNamespace appNamespace = new AppNamespace(); appNamespace.setId(id); appNamespace.setAppId(appId); appNamespace.setName(name); appNamespace.setPublic(isPublic); appNamespace.setDataChangeLastModifiedTime(new Date()); return appNamespace; } /** * fix issue #5502 */ @Test public void doubleCheckForAppNamespaceCache() throws Exception { String appId = "app1"; String namespaceName = "name1"; AppNamespace oldNs = assembleAppNamespace(1, appId, namespaceName, false); AppNamespace newNs = assembleAppNamespace(2, appId, namespaceName, false); when(appNamespaceRepository.findFirst500ByIdGreaterThanOrderByIdAsc(0)) .thenReturn(Lists.newArrayList(oldNs)); invokePrivateMethod("scanNewAppNamespaces"); AppNamespace cached = appNamespaceServiceWithCache.findByAppIdAndNamespace(appId, namespaceName); assertNotNull(cached); assertEquals(1, cached.getId()); when(appNamespaceRepository.findFirst500ByIdGreaterThanOrderByIdAsc(1)) .thenReturn(Lists.newArrayList(newNs)); invokePrivateMethod("scanNewAppNamespaces"); cached = appNamespaceServiceWithCache.findByAppIdAndNamespace(appId, namespaceName); assertNotNull(cached); assertEquals(2, cached.getId()); when(appNamespaceRepository.findAllById(anyList())) .thenAnswer(invocation -> Lists.newArrayList(newNs)); invokePrivateMethod("updateAndDeleteCache"); cached = appNamespaceServiceWithCache.findByAppIdAndNamespace(appId, namespaceName); assertNotNull("new cache is accidentally deleted", cached); assertEquals(2, cached.getId()); } private void invokePrivateMethod(String methodName) throws Exception { java.lang.reflect.Method method = AppNamespaceServiceWithCache.class.getDeclaredMethod(methodName); method.setAccessible(true); method.invoke(appNamespaceServiceWithCache); } } ================================================ FILE: apollo-configservice/src/test/java/com/ctrip/framework/apollo/configservice/service/ReleaseMessageServiceWithCacheTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.configservice.service; import com.ctrip.framework.apollo.biz.config.BizConfig; import com.ctrip.framework.apollo.biz.entity.ReleaseMessage; import com.ctrip.framework.apollo.biz.message.Topics; import com.ctrip.framework.apollo.biz.repository.ReleaseMessageRepository; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Set; import java.util.concurrent.TimeUnit; import static org.awaitility.Awaitility.await; import static org.junit.Assert.*; import static org.mockito.Mockito.*; /** * @author Jason Song(song_s@ctrip.com) */ @RunWith(MockitoJUnitRunner.class) public class ReleaseMessageServiceWithCacheTest { private ReleaseMessageServiceWithCache releaseMessageServiceWithCache; @Mock private ReleaseMessageRepository releaseMessageRepository; @Mock private BizConfig bizConfig; private int scanInterval; private TimeUnit scanIntervalTimeUnit; @Before public void setUp() throws Exception { releaseMessageServiceWithCache = new ReleaseMessageServiceWithCache(releaseMessageRepository, bizConfig); scanInterval = 10; scanIntervalTimeUnit = TimeUnit.MILLISECONDS; when(bizConfig.releaseMessageCacheScanInterval()).thenReturn(scanInterval); when(bizConfig.releaseMessageCacheScanIntervalTimeUnit()).thenReturn(scanIntervalTimeUnit); } @Test public void testWhenNoReleaseMessages() throws Exception { when(releaseMessageRepository.findFirst500ByIdGreaterThanOrderByIdAsc(0L)).thenReturn (Collections.emptyList()); releaseMessageServiceWithCache.afterPropertiesSet(); String someMessage = "someMessage"; String anotherMessage = "anotherMessage"; Set messages = Sets.newHashSet(someMessage, anotherMessage); assertNull(releaseMessageServiceWithCache.findLatestReleaseMessageForMessages(messages)); assertTrue(releaseMessageServiceWithCache.findLatestReleaseMessagesGroupByMessages(messages) .isEmpty()); } @Test public void testWhenHasReleaseMsgAndHasRepeatMsg() throws Exception { String someMsgContent = "msg1"; ReleaseMessage someMsg = assembleReleaseMsg(1, someMsgContent); String anotherMsgContent = "msg2"; ReleaseMessage anotherMsg = assembleReleaseMsg(2, anotherMsgContent); ReleaseMessage anotherRepeatMsg = assembleReleaseMsg(3, anotherMsgContent); when(releaseMessageRepository.findFirst500ByIdGreaterThanOrderByIdAsc(0L)) .thenReturn(Arrays.asList(someMsg, anotherMsg, anotherRepeatMsg)); releaseMessageServiceWithCache.afterPropertiesSet(); verify(bizConfig).releaseMessageCacheScanInterval(); ReleaseMessage latestReleaseMsg = releaseMessageServiceWithCache .findLatestReleaseMessageForMessages(Sets.newHashSet(someMsgContent, anotherMsgContent)); assertNotNull(latestReleaseMsg); assertEquals(3, latestReleaseMsg.getId()); assertEquals(anotherMsgContent, latestReleaseMsg.getMessage()); List latestReleaseMsgGroupByMsgContent = releaseMessageServiceWithCache.findLatestReleaseMessagesGroupByMessages( Sets.newLinkedHashSet(Arrays.asList(someMsgContent, anotherMsgContent))); assertEquals(2, latestReleaseMsgGroupByMsgContent.size()); assertEquals(3, latestReleaseMsgGroupByMsgContent.get(1).getId()); assertEquals(anotherMsgContent, latestReleaseMsgGroupByMsgContent.get(1).getMessage()); assertEquals(1, latestReleaseMsgGroupByMsgContent.get(0).getId()); assertEquals(someMsgContent, latestReleaseMsgGroupByMsgContent.get(0).getMessage()); } @Test public void testWhenReleaseMsgSizeBiggerThan500() throws Exception { String someMsgContent = "msg1"; List firstBatchReleaseMsg = new ArrayList<>(500); for (int i = 0; i < 500; i++) { firstBatchReleaseMsg.add(assembleReleaseMsg(i + 1, someMsgContent)); } String antherMsgContent = "msg2"; ReleaseMessage antherMsg = assembleReleaseMsg(501, antherMsgContent); when(releaseMessageRepository.findFirst500ByIdGreaterThanOrderByIdAsc(0L)) .thenReturn(firstBatchReleaseMsg); when(releaseMessageRepository.findFirst500ByIdGreaterThanOrderByIdAsc(500L)) .thenReturn(Collections.singletonList(antherMsg)); releaseMessageServiceWithCache.afterPropertiesSet(); verify(releaseMessageRepository, times(1)).findFirst500ByIdGreaterThanOrderByIdAsc(500L); ReleaseMessage latestReleaseMsg = releaseMessageServiceWithCache .findLatestReleaseMessageForMessages(Sets.newHashSet(someMsgContent, antherMsgContent)); assertNotNull(latestReleaseMsg); assertEquals(501, latestReleaseMsg.getId()); assertEquals(antherMsgContent, latestReleaseMsg.getMessage()); List msgContentList = Arrays.asList(someMsgContent, antherMsgContent); List latestReleaseMsgGroupByMsgContent = releaseMessageServiceWithCache .findLatestReleaseMessagesGroupByMessages(Sets.newLinkedHashSet(msgContentList)); assertEquals(2, latestReleaseMsgGroupByMsgContent.size()); assertEquals(500, latestReleaseMsgGroupByMsgContent.get(0).getId()); assertEquals(501, latestReleaseMsgGroupByMsgContent.get(1).getId()); } @Test public void testNewReleaseMessagesBeforeHandleMessage() throws Exception { String someMessageContent = "someMessage"; long someMessageId = 1; ReleaseMessage someMessage = assembleReleaseMsg(someMessageId, someMessageContent); when(releaseMessageRepository.findFirst500ByIdGreaterThanOrderByIdAsc(0L)) .thenReturn(Lists.newArrayList(someMessage)); releaseMessageServiceWithCache.afterPropertiesSet(); ReleaseMessage latestReleaseMsg = releaseMessageServiceWithCache .findLatestReleaseMessageForMessages(Sets.newHashSet(someMessageContent)); List latestReleaseMsgGroupByMsgContent = releaseMessageServiceWithCache .findLatestReleaseMessagesGroupByMessages(Sets.newHashSet(someMessageContent)); assertEquals(someMessageId, latestReleaseMsg.getId()); assertEquals(someMessageContent, latestReleaseMsg.getMessage()); assertEquals(latestReleaseMsg, latestReleaseMsgGroupByMsgContent.get(0)); long newMessageId = 2; ReleaseMessage newMessage = assembleReleaseMsg(newMessageId, someMessageContent); when(releaseMessageRepository.findFirst500ByIdGreaterThanOrderByIdAsc(someMessageId)) .thenReturn(Lists.newArrayList(newMessage)); await().atMost(scanInterval * 500, scanIntervalTimeUnit).untilAsserted(() -> { ReleaseMessage newLatestReleaseMsg = releaseMessageServiceWithCache .findLatestReleaseMessageForMessages(Sets.newHashSet(someMessageContent)); List newLatestReleaseMsgGroupByMsgContent = releaseMessageServiceWithCache .findLatestReleaseMessagesGroupByMessages(Sets.newHashSet(someMessageContent)); assertEquals(newMessageId, newLatestReleaseMsg.getId()); assertEquals(someMessageContent, newLatestReleaseMsg.getMessage()); assertEquals(newLatestReleaseMsg, newLatestReleaseMsgGroupByMsgContent.get(0)); }); } @Test public void testNewReleasesWithHandleMessage() throws Exception { String someMessageContent = "someMessage"; long someMessageId = 1; ReleaseMessage someMessage = assembleReleaseMsg(someMessageId, someMessageContent); when(releaseMessageRepository.findFirst500ByIdGreaterThanOrderByIdAsc(0L)) .thenReturn(Lists.newArrayList(someMessage)); releaseMessageServiceWithCache.afterPropertiesSet(); ReleaseMessage latestReleaseMsg = releaseMessageServiceWithCache .findLatestReleaseMessageForMessages(Sets.newHashSet(someMessageContent)); List latestReleaseMsgGroupByMsgContent = releaseMessageServiceWithCache .findLatestReleaseMessagesGroupByMessages(Sets.newHashSet(someMessageContent)); assertEquals(someMessageId, latestReleaseMsg.getId()); assertEquals(someMessageContent, latestReleaseMsg.getMessage()); assertEquals(latestReleaseMsg, latestReleaseMsgGroupByMsgContent.get(0)); long newMessageId = 2; ReleaseMessage newMessage = assembleReleaseMsg(newMessageId, someMessageContent); releaseMessageServiceWithCache.handleMessage(newMessage, Topics.APOLLO_RELEASE_TOPIC); ReleaseMessage newLatestReleaseMsg = releaseMessageServiceWithCache .findLatestReleaseMessageForMessages(Sets.newHashSet(someMessageContent)); List newLatestReleaseMsgGroupByMsgContent = releaseMessageServiceWithCache .findLatestReleaseMessagesGroupByMessages(Sets.newHashSet(someMessageContent)); assertEquals(newMessageId, newLatestReleaseMsg.getId()); assertEquals(someMessageContent, newLatestReleaseMsg.getMessage()); assertEquals(newLatestReleaseMsg, newLatestReleaseMsgGroupByMsgContent.get(0)); } private ReleaseMessage assembleReleaseMsg(long id, String msgContent) { ReleaseMessage msg = new ReleaseMessage(msgContent); msg.setId(id); return msg; } } ================================================ FILE: apollo-configservice/src/test/java/com/ctrip/framework/apollo/configservice/service/config/ConfigServiceWithCacheAndCacheKeyIgnoreCaseTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.configservice.service.config; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import static org.mockito.ArgumentMatchers.matches; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import com.ctrip.framework.apollo.biz.config.BizConfig; import com.ctrip.framework.apollo.biz.entity.Release; import com.ctrip.framework.apollo.biz.entity.ReleaseMessage; import com.ctrip.framework.apollo.biz.grayReleaseRule.GrayReleaseRulesHolder; import com.ctrip.framework.apollo.biz.message.Topics; import com.ctrip.framework.apollo.biz.service.ReleaseMessageService; import com.ctrip.framework.apollo.biz.service.ReleaseService; import com.ctrip.framework.apollo.biz.utils.ReleaseMessageKeyGenerator; import com.ctrip.framework.apollo.core.dto.ApolloNotificationMessages; import com.google.common.collect.Lists; import io.micrometer.core.instrument.MeterRegistry; import java.util.regex.Pattern; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; /** * @author kl (http://kailing.pub) * @since 2023/3/28 */ @RunWith(MockitoJUnitRunner.class) public class ConfigServiceWithCacheAndCacheKeyIgnoreCaseTest { private ConfigServiceWithCache configServiceWithCache; @Mock private ReleaseService releaseService; @Mock private ReleaseMessageService releaseMessageService; @Mock private Release someRelease; @Mock private ReleaseMessage someReleaseMessage; @Mock private BizConfig bizConfig; @Mock private MeterRegistry meterRegistry; @Mock private GrayReleaseRulesHolder grayReleaseRulesHolder; private String someAppId; private String someClusterName; private String someNamespaceName; private String lowerCaseSomeKey; private String normalSomeKey; private long someNotificationId; private ApolloNotificationMessages someNotificationMessages; @Before public void setUp() throws Exception { configServiceWithCache = new ConfigServiceWithCache(releaseService, releaseMessageService, grayReleaseRulesHolder, bizConfig, meterRegistry); when(bizConfig.isConfigServiceCacheKeyIgnoreCase()).thenReturn(true); configServiceWithCache.initialize(); someAppId = "someAppId"; someClusterName = "someClusterName"; someNamespaceName = "someNamespaceName"; someNotificationId = 1; normalSomeKey = ReleaseMessageKeyGenerator.generate(someAppId, someClusterName, someNamespaceName); lowerCaseSomeKey = normalSomeKey.toLowerCase(); someNotificationMessages = new ApolloNotificationMessages(); } @Test public void testFindActiveOne() { long someId = 1; when(releaseService.findActiveOne(someId)).thenReturn(someRelease); assertEquals(someRelease, configServiceWithCache.findActiveOne(someId, someNotificationMessages)); verify(releaseService, times(1)).findActiveOne(someId); } @Test public void testFindActiveOneWithSameIdMultipleTimes() { long someId = 1; when(releaseService.findActiveOne(someId)).thenReturn(someRelease); assertEquals(someRelease, configServiceWithCache.findActiveOne(someId, someNotificationMessages)); assertEquals(someRelease, configServiceWithCache.findActiveOne(someId, someNotificationMessages)); assertEquals(someRelease, configServiceWithCache.findActiveOne(someId, someNotificationMessages)); verify(releaseService, times(1)).findActiveOne(someId); } @Test public void testFindActiveOneWithMultipleIdMultipleTimes() { long someId = 1; long anotherId = 2; Release anotherRelease = mock(Release.class); when(releaseService.findActiveOne(someId)).thenReturn(someRelease); when(releaseService.findActiveOne(anotherId)).thenReturn(anotherRelease); assertEquals(someRelease, configServiceWithCache.findActiveOne(someId, someNotificationMessages)); assertEquals(someRelease, configServiceWithCache.findActiveOne(someId, someNotificationMessages)); assertEquals(anotherRelease, configServiceWithCache.findActiveOne(anotherId, someNotificationMessages)); assertEquals(anotherRelease, configServiceWithCache.findActiveOne(anotherId, someNotificationMessages)); verify(releaseService, times(1)).findActiveOne(someId); verify(releaseService, times(1)).findActiveOne(anotherId); } @Test public void testFindActiveOneWithReleaseNotFoundMultipleTimes() { long someId = 1; when(releaseService.findActiveOne(someId)).thenReturn(null); assertNull(configServiceWithCache.findActiveOne(someId, someNotificationMessages)); assertNull(configServiceWithCache.findActiveOne(someId, someNotificationMessages)); assertNull(configServiceWithCache.findActiveOne(someId, someNotificationMessages)); verify(releaseService, times(1)).findActiveOne(someId); } @Test public void testFindLatestActiveRelease() { when(releaseMessageService.findLatestReleaseMessageForMessages(Lists.newArrayList(lowerCaseSomeKey))) .thenReturn(someReleaseMessage); when(releaseService.findLatestActiveRelease( matchesCaseInsensitive(someAppId), matchesCaseInsensitive(someClusterName), matchesCaseInsensitive(someNamespaceName) )).thenReturn(someRelease); when(someReleaseMessage.getId()).thenReturn(someNotificationId); Release release = configServiceWithCache.findLatestActiveRelease(someAppId, someClusterName, someNamespaceName, someNotificationMessages); Release anotherRelease = configServiceWithCache.findLatestActiveRelease(someAppId, someClusterName, someNamespaceName, someNotificationMessages); int retryTimes = 100; for (int i = 0; i < retryTimes; i++) { configServiceWithCache.findLatestActiveRelease(someAppId, someClusterName, someNamespaceName, someNotificationMessages); } assertEquals(someRelease, release); assertEquals(someRelease, anotherRelease); verify(releaseMessageService, times(1)).findLatestReleaseMessageForMessages( Lists.newArrayList(lowerCaseSomeKey)); verify(releaseService, times(1)).findLatestActiveRelease( someAppId.toLowerCase(), someClusterName.toLowerCase(), someNamespaceName.toLowerCase()); } @Test public void testFindLatestActiveReleaseWithReleaseNotFound() { when(releaseMessageService.findLatestReleaseMessageForMessages( Lists.newArrayList(lowerCaseSomeKey))).thenReturn(null); when(releaseService.findLatestActiveRelease( matchesCaseInsensitive(someAppId), matchesCaseInsensitive(someClusterName), matchesCaseInsensitive(someNamespaceName) )).thenReturn(null); Release release = configServiceWithCache.findLatestActiveRelease(someAppId, someClusterName, someNamespaceName, someNotificationMessages); Release anotherRelease = configServiceWithCache.findLatestActiveRelease(someAppId, someClusterName, someNamespaceName, someNotificationMessages); int retryTimes = 100; for (int i = 0; i < retryTimes; i++) { configServiceWithCache.findLatestActiveRelease(someAppId, someClusterName, someNamespaceName, someNotificationMessages); } assertNull(release); assertNull(anotherRelease); verify(releaseMessageService, times(1)).findLatestReleaseMessageForMessages( Lists.newArrayList(lowerCaseSomeKey)); verify(releaseService, times(1)).findLatestActiveRelease( someAppId.toLowerCase(), someClusterName.toLowerCase(), someNamespaceName.toLowerCase()); } @Test public void testFindLatestActiveReleaseWithDirtyRelease() { long someNewNotificationId = someNotificationId + 1; ReleaseMessage anotherReleaseMessage = mock(ReleaseMessage.class); Release anotherRelease = mock(Release.class); when(releaseMessageService .findLatestReleaseMessageForMessages(Lists.newArrayList(lowerCaseSomeKey))) .thenReturn(someReleaseMessage); when(releaseService.findLatestActiveRelease(matchesCaseInsensitive(someAppId), matchesCaseInsensitive(someClusterName), matchesCaseInsensitive(someNamespaceName))) .thenReturn(someRelease); when(someReleaseMessage.getId()).thenReturn(someNotificationId); Release release = configServiceWithCache.findLatestActiveRelease(someAppId, someClusterName, someNamespaceName, someNotificationMessages); when(releaseMessageService .findLatestReleaseMessageForMessages(Lists.newArrayList(lowerCaseSomeKey))) .thenReturn(anotherReleaseMessage); when(releaseService.findLatestActiveRelease(matchesCaseInsensitive(someAppId), matchesCaseInsensitive(someClusterName), matchesCaseInsensitive(someNamespaceName))) .thenReturn(anotherRelease); when(anotherReleaseMessage.getId()).thenReturn(someNewNotificationId); Release stillOldRelease = configServiceWithCache.findLatestActiveRelease(someAppId, someClusterName, someNamespaceName, someNotificationMessages); someNotificationMessages.put( ReleaseMessageKeyGenerator.generate(someAppId, someClusterName, someNamespaceName), someNewNotificationId); Release shouldBeNewRelease = configServiceWithCache.findLatestActiveRelease(someAppId, someClusterName, someNamespaceName, someNotificationMessages); assertEquals(someRelease, release); assertEquals(someRelease, stillOldRelease); assertEquals(anotherRelease, shouldBeNewRelease); verify(releaseMessageService, times(2)) .findLatestReleaseMessageForMessages(Lists.newArrayList(lowerCaseSomeKey)); verify(releaseService, times(2)).findLatestActiveRelease(someAppId.toLowerCase(), someClusterName.toLowerCase(), someNamespaceName.toLowerCase()); } @Test public void testFindLatestActiveReleaseWithReleaseMessageNotification() { long someNewNotificationId = someNotificationId + 1; ReleaseMessage anotherReleaseMessage = mock(ReleaseMessage.class); Release anotherRelease = mock(Release.class); when(releaseMessageService .findLatestReleaseMessageForMessages(Lists.newArrayList(lowerCaseSomeKey))) .thenReturn(someReleaseMessage); when(releaseService.findLatestActiveRelease(matchesCaseInsensitive(someAppId), matchesCaseInsensitive(someClusterName), matchesCaseInsensitive(someNamespaceName))) .thenReturn(someRelease); when(someReleaseMessage.getId()).thenReturn(someNotificationId); Release release = configServiceWithCache.findLatestActiveRelease(someAppId, someClusterName, someNamespaceName, someNotificationMessages); when(releaseMessageService .findLatestReleaseMessageForMessages(Lists.newArrayList(lowerCaseSomeKey))) .thenReturn(anotherReleaseMessage); when(releaseService.findLatestActiveRelease(matchesCaseInsensitive(someAppId), matchesCaseInsensitive(someClusterName), matchesCaseInsensitive(someNamespaceName))) .thenReturn(anotherRelease); when(anotherReleaseMessage.getMessage()).thenReturn(lowerCaseSomeKey); when(anotherReleaseMessage.getId()).thenReturn(someNewNotificationId); Release stillOldRelease = configServiceWithCache.findLatestActiveRelease(someAppId, someClusterName, someNamespaceName, someNotificationMessages); configServiceWithCache.handleMessage(anotherReleaseMessage, Topics.APOLLO_RELEASE_TOPIC); Release shouldBeNewRelease = configServiceWithCache.findLatestActiveRelease(someAppId, someClusterName, someNamespaceName, someNotificationMessages); assertEquals(someRelease, release); assertEquals(someRelease, stillOldRelease); assertEquals(anotherRelease, shouldBeNewRelease); verify(releaseMessageService, times(2)) .findLatestReleaseMessageForMessages(Lists.newArrayList(lowerCaseSomeKey)); verify(releaseService, times(2)).findLatestActiveRelease(someAppId.toLowerCase(), someClusterName.toLowerCase(), someNamespaceName.toLowerCase()); when(anotherReleaseMessage.getMessage()).thenReturn(normalSomeKey); when(anotherReleaseMessage.getId()).thenReturn(someNewNotificationId); configServiceWithCache.handleMessage(anotherReleaseMessage, Topics.APOLLO_RELEASE_TOPIC); shouldBeNewRelease = configServiceWithCache.findLatestActiveRelease(someAppId, someClusterName, someNamespaceName, someNotificationMessages); assertEquals(anotherRelease, shouldBeNewRelease); } @Test public void testFindLatestActiveReleaseWithIrrelevantMessages() { long someNewNotificationId = someNotificationId + 1; String someIrrelevantKey = "someIrrelevantKey"; when(releaseMessageService .findLatestReleaseMessageForMessages(Lists.newArrayList(lowerCaseSomeKey))) .thenReturn(someReleaseMessage); when(releaseService.findLatestActiveRelease(matchesCaseInsensitive(someAppId), matchesCaseInsensitive(someClusterName), matchesCaseInsensitive(someNamespaceName))) .thenReturn(someRelease); when(someReleaseMessage.getId()).thenReturn(someNotificationId); Release release = configServiceWithCache.findLatestActiveRelease(someAppId, someClusterName, someNamespaceName, someNotificationMessages); Release stillOldRelease = configServiceWithCache.findLatestActiveRelease(someAppId, someClusterName, someNamespaceName, someNotificationMessages); someNotificationMessages.put(someIrrelevantKey, someNewNotificationId); Release shouldStillBeOldRelease = configServiceWithCache.findLatestActiveRelease(someAppId, someClusterName, someNamespaceName, someNotificationMessages); assertEquals(someRelease, release); assertEquals(someRelease, stillOldRelease); assertEquals(someRelease, shouldStillBeOldRelease); verify(releaseMessageService, times(1)) .findLatestReleaseMessageForMessages(Lists.newArrayList(lowerCaseSomeKey)); verify(releaseService, times(1)).findLatestActiveRelease(someAppId.toLowerCase(), someClusterName.toLowerCase(), someNamespaceName.toLowerCase()); } private String matchesCaseInsensitive(final String regex) { return matches(Pattern.compile(regex, Pattern.CASE_INSENSITIVE)); } } ================================================ FILE: apollo-configservice/src/test/java/com/ctrip/framework/apollo/configservice/service/config/ConfigServiceWithCacheTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.configservice.service.config; import com.ctrip.framework.apollo.biz.grayReleaseRule.GrayReleaseRulesHolder; import com.ctrip.framework.apollo.biz.config.BizConfig; import com.ctrip.framework.apollo.core.dto.ApolloNotificationMessages; import com.google.common.collect.Lists; import com.ctrip.framework.apollo.biz.entity.Release; import com.ctrip.framework.apollo.biz.entity.ReleaseMessage; import com.ctrip.framework.apollo.biz.message.Topics; import com.ctrip.framework.apollo.biz.service.ReleaseMessageService; import com.ctrip.framework.apollo.biz.service.ReleaseService; import com.ctrip.framework.apollo.biz.utils.ReleaseMessageKeyGenerator; import com.google.common.collect.Sets; import io.micrometer.core.instrument.MeterRegistry; import java.util.Map; import java.util.Set; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; /** * @author Jason Song(song_s@ctrip.com) */ @RunWith(MockitoJUnitRunner.class) public class ConfigServiceWithCacheTest { private ConfigServiceWithCache configServiceWithCache; @Mock private ReleaseService releaseService; @Mock private ReleaseMessageService releaseMessageService; @Mock private Release someRelease; @Mock private ReleaseMessage someReleaseMessage; @Mock private BizConfig bizConfig; @Mock private MeterRegistry meterRegistry; @Mock private GrayReleaseRulesHolder grayReleaseRulesHolder; private String someAppId; private String someClusterName; private String someNamespaceName; private String someKey; private long someNotificationId; private ApolloNotificationMessages someNotificationMessages; @Before public void setUp() throws Exception { configServiceWithCache = new ConfigServiceWithCache(releaseService, releaseMessageService, grayReleaseRulesHolder, bizConfig, meterRegistry); configServiceWithCache.initialize(); someAppId = "someAppId"; someClusterName = "someClusterName"; someNamespaceName = "someNamespaceName"; someNotificationId = 1; someKey = ReleaseMessageKeyGenerator.generate(someAppId, someClusterName, someNamespaceName); someNotificationMessages = new ApolloNotificationMessages(); } @Test public void testFindActiveOne() throws Exception { long someId = 1; when(releaseService.findActiveOne(someId)).thenReturn(someRelease); assertEquals(someRelease, configServiceWithCache.findActiveOne(someId, someNotificationMessages)); verify(releaseService, times(1)).findActiveOne(someId); } @Test public void testFindReleasesByReleaseKeys() { String someReleaseKey = "someReleaseKey"; long someId = 1; Set someReleaseKeys = Sets.newHashSet(someReleaseKey); when(releaseService.findByReleaseKey(someReleaseKey)).thenReturn(someRelease); when(releaseService.findActiveOne(someId)).thenReturn(someRelease); when(someRelease.getId()).thenReturn(someId); Map someReleaseMap = null; someReleaseMap = configServiceWithCache.findReleasesByReleaseKeys(someReleaseKeys); assertEquals(1, someReleaseMap.size()); assertEquals(someRelease, someReleaseMap.get(someReleaseKey)); verify(releaseService, times(1)).findByReleaseKey(someReleaseKey); } @Test public void testFindActiveOneWithSameIdMultipleTimes() throws Exception { long someId = 1; when(releaseService.findActiveOne(someId)).thenReturn(someRelease); assertEquals(someRelease, configServiceWithCache.findActiveOne(someId, someNotificationMessages)); assertEquals(someRelease, configServiceWithCache.findActiveOne(someId, someNotificationMessages)); assertEquals(someRelease, configServiceWithCache.findActiveOne(someId, someNotificationMessages)); verify(releaseService, times(1)).findActiveOne(someId); } @Test public void testFindReleasesByReleaseKeysWithSameIdMultipleTimes() { String someReleaseKey = "someReleaseKey"; long someId = 1; Set someReleaseKeys = Sets.newHashSet(someReleaseKey); when(releaseService.findByReleaseKey(someReleaseKey)).thenReturn(someRelease); when(releaseService.findActiveOne(someId)).thenReturn(someRelease); when(someRelease.getId()).thenReturn(someId); Map someReleaseMap = null; Map otherReleaseMap = null; someReleaseMap = configServiceWithCache.findReleasesByReleaseKeys(someReleaseKeys); otherReleaseMap = configServiceWithCache.findReleasesByReleaseKeys(someReleaseKeys); assertEquals(1, someReleaseMap.size()); assertEquals(someRelease, someReleaseMap.get(someReleaseKey)); assertEquals(1, otherReleaseMap.size()); assertEquals(someRelease, otherReleaseMap.get(someReleaseKey)); verify(releaseService, times(1)).findByReleaseKey(someReleaseKey); verify(releaseService, times(1)).findActiveOne(someId); } @Test public void testFindActiveOneWithMultipleIdMultipleTimes() throws Exception { long someId = 1; long anotherId = 2; Release anotherRelease = mock(Release.class); when(releaseService.findActiveOne(someId)).thenReturn(someRelease); when(releaseService.findActiveOne(anotherId)).thenReturn(anotherRelease); assertEquals(someRelease, configServiceWithCache.findActiveOne(someId, someNotificationMessages)); assertEquals(someRelease, configServiceWithCache.findActiveOne(someId, someNotificationMessages)); assertEquals(anotherRelease, configServiceWithCache.findActiveOne(anotherId, someNotificationMessages)); assertEquals(anotherRelease, configServiceWithCache.findActiveOne(anotherId, someNotificationMessages)); verify(releaseService, times(1)).findActiveOne(someId); verify(releaseService, times(1)).findActiveOne(anotherId); } @Test public void testFindReleasesByReleaseKeysNotFoundMultipleTimes() throws Exception { String someReleaseKey = "someReleaseKey"; Set someReleaseKeys = Sets.newHashSet(someReleaseKey); when(releaseService.findByReleaseKey(someReleaseKey)).thenReturn(null); assertEquals(0, configServiceWithCache.findReleasesByReleaseKeys(someReleaseKeys).size()); assertEquals(0, configServiceWithCache.findReleasesByReleaseKeys(someReleaseKeys).size()); assertEquals(0, configServiceWithCache.findReleasesByReleaseKeys(someReleaseKeys).size()); verify(releaseService, times(1)).findByReleaseKey(someReleaseKey); } @Test public void testFindActiveOneWithReleaseNotFoundMultipleTimes() throws Exception { long someId = 1; when(releaseService.findActiveOne(someId)).thenReturn(null); assertNull(configServiceWithCache.findActiveOne(someId, someNotificationMessages)); assertNull(configServiceWithCache.findActiveOne(someId, someNotificationMessages)); assertNull(configServiceWithCache.findActiveOne(someId, someNotificationMessages)); verify(releaseService, times(1)).findActiveOne(someId); } @Test public void testFindLatestActiveRelease() throws Exception { when(releaseMessageService.findLatestReleaseMessageForMessages(Lists.newArrayList(someKey))).thenReturn (someReleaseMessage); when(releaseService.findLatestActiveRelease(someAppId, someClusterName, someNamespaceName)).thenReturn (someRelease); when(someReleaseMessage.getId()).thenReturn(someNotificationId); Release release = configServiceWithCache.findLatestActiveRelease(someAppId, someClusterName, someNamespaceName, someNotificationMessages); Release anotherRelease = configServiceWithCache.findLatestActiveRelease(someAppId, someClusterName, someNamespaceName, someNotificationMessages); int retryTimes = 100; for (int i = 0; i < retryTimes; i++) { configServiceWithCache.findLatestActiveRelease(someAppId, someClusterName, someNamespaceName, someNotificationMessages); } assertEquals(someRelease, release); assertEquals(someRelease, anotherRelease); verify(releaseMessageService, times(1)).findLatestReleaseMessageForMessages(Lists.newArrayList(someKey)); verify(releaseService, times(1)).findLatestActiveRelease(someAppId, someClusterName, someNamespaceName); } @Test public void testFindLatestActiveReleaseWithReleaseNotFound() throws Exception { when(releaseMessageService.findLatestReleaseMessageForMessages(Lists.newArrayList(someKey))).thenReturn(null); when(releaseService.findLatestActiveRelease(someAppId, someClusterName, someNamespaceName)).thenReturn(null); Release release = configServiceWithCache.findLatestActiveRelease(someAppId, someClusterName, someNamespaceName, someNotificationMessages); Release anotherRelease = configServiceWithCache.findLatestActiveRelease(someAppId, someClusterName, someNamespaceName, someNotificationMessages); int retryTimes = 100; for (int i = 0; i < retryTimes; i++) { configServiceWithCache.findLatestActiveRelease(someAppId, someClusterName, someNamespaceName, someNotificationMessages); } assertNull(release); assertNull(anotherRelease); verify(releaseMessageService, times(1)).findLatestReleaseMessageForMessages(Lists.newArrayList(someKey)); verify(releaseService, times(1)).findLatestActiveRelease(someAppId, someClusterName, someNamespaceName); } @Test public void testFindLatestActiveReleaseWithDirtyRelease() throws Exception { long someNewNotificationId = someNotificationId + 1; ReleaseMessage anotherReleaseMessage = mock(ReleaseMessage.class); Release anotherRelease = mock(Release.class); when(releaseMessageService.findLatestReleaseMessageForMessages(Lists.newArrayList(someKey))) .thenReturn(someReleaseMessage); when(releaseService.findLatestActiveRelease(someAppId, someClusterName, someNamespaceName)) .thenReturn(someRelease); when(someReleaseMessage.getId()).thenReturn(someNotificationId); Release release = configServiceWithCache.findLatestActiveRelease(someAppId, someClusterName, someNamespaceName, someNotificationMessages); when(releaseMessageService.findLatestReleaseMessageForMessages(Lists.newArrayList(someKey))) .thenReturn(anotherReleaseMessage); when(releaseService.findLatestActiveRelease(someAppId, someClusterName, someNamespaceName)) .thenReturn(anotherRelease); when(anotherReleaseMessage.getId()).thenReturn(someNewNotificationId); Release stillOldRelease = configServiceWithCache.findLatestActiveRelease(someAppId, someClusterName, someNamespaceName, someNotificationMessages); someNotificationMessages.put(someKey, someNewNotificationId); Release shouldBeNewRelease = configServiceWithCache.findLatestActiveRelease(someAppId, someClusterName, someNamespaceName, someNotificationMessages); assertEquals(someRelease, release); assertEquals(someRelease, stillOldRelease); assertEquals(anotherRelease, shouldBeNewRelease); verify(releaseMessageService, times(2)) .findLatestReleaseMessageForMessages(Lists.newArrayList(someKey)); verify(releaseService, times(2)).findLatestActiveRelease(someAppId, someClusterName, someNamespaceName); } @Test public void testFindLatestActiveReleaseWithReleaseMessageNotification() throws Exception { long someNewNotificationId = someNotificationId + 1; ReleaseMessage anotherReleaseMessage = mock(ReleaseMessage.class); Release anotherRelease = mock(Release.class); when(releaseMessageService.findLatestReleaseMessageForMessages(Lists.newArrayList(someKey))) .thenReturn(someReleaseMessage); when(releaseService.findLatestActiveRelease(someAppId, someClusterName, someNamespaceName)) .thenReturn(someRelease); when(someReleaseMessage.getId()).thenReturn(someNotificationId); Release release = configServiceWithCache.findLatestActiveRelease(someAppId, someClusterName, someNamespaceName, someNotificationMessages); when(releaseMessageService.findLatestReleaseMessageForMessages(Lists.newArrayList(someKey))) .thenReturn(anotherReleaseMessage); when(releaseService.findLatestActiveRelease(someAppId, someClusterName, someNamespaceName)) .thenReturn(anotherRelease); when(anotherReleaseMessage.getMessage()).thenReturn(someKey); when(anotherReleaseMessage.getId()).thenReturn(someNewNotificationId); Release stillOldRelease = configServiceWithCache.findLatestActiveRelease(someAppId, someClusterName, someNamespaceName, someNotificationMessages); configServiceWithCache.handleMessage(anotherReleaseMessage, Topics.APOLLO_RELEASE_TOPIC); Release shouldBeNewRelease = configServiceWithCache.findLatestActiveRelease(someAppId, someClusterName, someNamespaceName, someNotificationMessages); assertEquals(someRelease, release); assertEquals(someRelease, stillOldRelease); assertEquals(anotherRelease, shouldBeNewRelease); verify(releaseMessageService, times(2)) .findLatestReleaseMessageForMessages(Lists.newArrayList(someKey)); verify(releaseService, times(2)).findLatestActiveRelease(someAppId, someClusterName, someNamespaceName); } @Test public void testFindLatestActiveReleaseWithIrrelevantMessages() throws Exception { long someNewNotificationId = someNotificationId + 1; String someIrrelevantKey = "someIrrelevantKey"; when(releaseMessageService.findLatestReleaseMessageForMessages(Lists.newArrayList(someKey))) .thenReturn(someReleaseMessage); when(releaseService.findLatestActiveRelease(someAppId, someClusterName, someNamespaceName)) .thenReturn(someRelease); when(someReleaseMessage.getId()).thenReturn(someNotificationId); Release release = configServiceWithCache.findLatestActiveRelease(someAppId, someClusterName, someNamespaceName, someNotificationMessages); Release stillOldRelease = configServiceWithCache.findLatestActiveRelease(someAppId, someClusterName, someNamespaceName, someNotificationMessages); someNotificationMessages.put(someIrrelevantKey, someNewNotificationId); Release shouldStillBeOldRelease = configServiceWithCache.findLatestActiveRelease(someAppId, someClusterName, someNamespaceName, someNotificationMessages); assertEquals(someRelease, release); assertEquals(someRelease, stillOldRelease); assertEquals(someRelease, shouldStillBeOldRelease); verify(releaseMessageService, times(1)) .findLatestReleaseMessageForMessages(Lists.newArrayList(someKey)); verify(releaseService, times(1)).findLatestActiveRelease(someAppId, someClusterName, someNamespaceName); } } ================================================ FILE: apollo-configservice/src/test/java/com/ctrip/framework/apollo/configservice/service/config/DefaultConfigServiceTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.configservice.service.config; import static org.junit.Assert.*; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import com.ctrip.framework.apollo.biz.entity.Release; import com.ctrip.framework.apollo.biz.grayReleaseRule.GrayReleaseRulesHolder; import com.ctrip.framework.apollo.biz.service.ReleaseService; import com.ctrip.framework.apollo.core.ConfigConsts; import com.ctrip.framework.apollo.core.dto.ApolloNotificationMessages; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; /** * @author Jason Song(song_s@ctrip.com) */ @RunWith(MockitoJUnitRunner.class) public class DefaultConfigServiceTest { private DefaultConfigService configService; private String someClientAppId; private String someConfigAppId; private String someClusterName; private String defaultClusterName; private String defaultNamespaceName; private String someDataCenter; private String someClientIp; private String someClientLabel; @Mock private ApolloNotificationMessages someNotificationMessages; @Mock private ReleaseService releaseService; @Mock private GrayReleaseRulesHolder grayReleaseRulesHolder; @Mock private Release someRelease; @Before public void setUp() throws Exception { configService = new DefaultConfigService(releaseService, grayReleaseRulesHolder); someClientAppId = "1234"; someConfigAppId = "1"; someClusterName = "someClusterName"; defaultClusterName = ConfigConsts.CLUSTER_NAME_DEFAULT; defaultNamespaceName = ConfigConsts.NAMESPACE_APPLICATION; someDataCenter = "someDC"; someClientIp = "someClientIp"; someClientLabel = "someClientLabel"; when(grayReleaseRulesHolder.findReleaseIdFromGrayReleaseRule(anyString(), anyString(), anyString(), anyString(), anyString(), anyString())).thenReturn(null); } @Test public void testLoadConfig() throws Exception { when(releaseService.findLatestActiveRelease(someConfigAppId, someClusterName, defaultNamespaceName)) .thenReturn(someRelease); Release release = configService .loadConfig(someClientAppId, someClientIp, someClientLabel, someConfigAppId, someClusterName, defaultNamespaceName, someDataCenter, someNotificationMessages); verify(releaseService, times(1)).findLatestActiveRelease(someConfigAppId, someClusterName, defaultNamespaceName); assertEquals(someRelease, release); } @Test public void testLoadConfigWithGrayRelease() throws Exception { Release grayRelease = mock(Release.class); long grayReleaseId = 999; when(grayReleaseRulesHolder.findReleaseIdFromGrayReleaseRule(someClientAppId, someClientIp, someClientLabel, someConfigAppId, someClusterName, defaultNamespaceName)) .thenReturn(grayReleaseId); when(releaseService.findActiveOne(grayReleaseId)).thenReturn(grayRelease); Release release = configService.loadConfig(someClientAppId, someClientIp, someClientLabel, someConfigAppId, someClusterName, defaultNamespaceName, someDataCenter, someNotificationMessages); verify(releaseService, times(1)).findActiveOne(grayReleaseId); verify(releaseService, never()).findLatestActiveRelease(someConfigAppId, someClusterName, defaultNamespaceName); assertEquals(grayRelease, release); } @Test public void testLoadConfigWithReleaseNotFound() throws Exception { when(releaseService.findLatestActiveRelease(someConfigAppId, someClusterName, defaultNamespaceName)) .thenReturn(null); Release release = configService .loadConfig(someClientAppId, someClientIp, someClientLabel, someConfigAppId, someClusterName, defaultNamespaceName, someDataCenter, someNotificationMessages); assertNull(release); } @Test public void testLoadConfigWithDefaultClusterWithDataCenterRelease() throws Exception { when(releaseService.findLatestActiveRelease(someConfigAppId, someDataCenter, defaultNamespaceName)) .thenReturn(someRelease); Release release = configService .loadConfig(someClientAppId, someClientIp, someClientLabel, someConfigAppId, defaultClusterName, defaultNamespaceName, someDataCenter, someNotificationMessages); verify(releaseService, times(1)).findLatestActiveRelease(someConfigAppId, someDataCenter, defaultNamespaceName); assertEquals(someRelease, release); } @Test public void testLoadConfigWithDefaultClusterWithNoDataCenterRelease() throws Exception { when(releaseService.findLatestActiveRelease(someConfigAppId, someDataCenter, defaultNamespaceName)) .thenReturn(null); when(releaseService.findLatestActiveRelease(someConfigAppId, defaultClusterName, defaultNamespaceName)) .thenReturn(someRelease); Release release = configService .loadConfig(someClientAppId, someClientIp, someClientLabel, someConfigAppId, defaultClusterName, defaultNamespaceName, someDataCenter, someNotificationMessages); verify(releaseService, times(1)).findLatestActiveRelease(someConfigAppId, someDataCenter, defaultNamespaceName); verify(releaseService, times(1)) .findLatestActiveRelease(someConfigAppId, defaultClusterName, defaultNamespaceName); assertEquals(someRelease, release); } } ================================================ FILE: apollo-configservice/src/test/java/com/ctrip/framework/apollo/configservice/service/config/DefaultIncrementalSyncServiceTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.configservice.service.config; import com.ctrip.framework.apollo.biz.utils.ReleaseMessageKeyGenerator; import com.ctrip.framework.apollo.configservice.service.config.DefaultIncrementalSyncService.ReleaseKeyPair; import com.ctrip.framework.apollo.core.dto.ConfigurationChange; import com.google.common.cache.Cache; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Maps; import java.lang.reflect.Field; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.junit.MockitoJUnitRunner; import java.util.List; import java.util.Map; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertSame; /** * @author jason */ @RunWith(MockitoJUnitRunner.class) public class DefaultIncrementalSyncServiceTest { private DefaultIncrementalSyncService defaultIncrementalSyncService; private String someKey; private String someReleaseKey; private String someAppId; private String someClusterName; private String someNamespaceName; private String newReleaseKey; private String someClientSideReleaseKey; private String someLatestMergedReleaseKey; private Map someClientSideConfigurations; private Map someLatestReleaseConfigurations; private Cache> configurationChangeCache; @Before public void setUp() throws Exception { defaultIncrementalSyncService = new DefaultIncrementalSyncService(); configurationChangeCache = getConfigurationChangeCache(defaultIncrementalSyncService); someReleaseKey = "someReleaseKey"; someAppId = "someAppId"; someClusterName = "someClusterName"; someNamespaceName = "someNamespaceName"; newReleaseKey = "someReleaseKey"; someKey = ReleaseMessageKeyGenerator.generate(someAppId, someClusterName, someNamespaceName); someClientSideReleaseKey = "client-release-key-v1"; someLatestMergedReleaseKey = "latest-release-key-v1"; someClientSideConfigurations = Maps.newHashMap(); someClientSideConfigurations.put("k1", "v1"); someClientSideConfigurations.put("k2", "v2"); someLatestReleaseConfigurations = Maps.newHashMap(); someLatestReleaseConfigurations.put("k1", "v1-new"); someLatestReleaseConfigurations.put("k3", "v3"); } @SuppressWarnings("unchecked") private Cache> getConfigurationChangeCache( DefaultIncrementalSyncService service) { try { Field cacheField = service.getClass().getDeclaredField("configurationChangeCache"); cacheField.setAccessible(true); return (Cache>) cacheField.get(service); } catch (Exception e) { throw new RuntimeException("Failed to get cache field", e); } } @Test public void testConfigurationChangeCacheHit() { List firstResult = defaultIncrementalSyncService.getConfigurationChanges( someLatestMergedReleaseKey, someLatestReleaseConfigurations, someClientSideReleaseKey, someClientSideConfigurations); ReleaseKeyPair key = new ReleaseKeyPair(someClientSideReleaseKey, someLatestMergedReleaseKey); List cachedResult = configurationChangeCache.getIfPresent(key); assertNotNull(cachedResult); assertSame(firstResult, cachedResult); List secondResult = defaultIncrementalSyncService.getConfigurationChanges( someLatestMergedReleaseKey, someLatestReleaseConfigurations, someClientSideReleaseKey, someClientSideConfigurations); assertSame(firstResult, secondResult); } @Test public void testConfigurationChangeCacheMiss() { defaultIncrementalSyncService.getConfigurationChanges(someLatestMergedReleaseKey, someLatestReleaseConfigurations, someClientSideReleaseKey, someClientSideConfigurations); String differentLatestMergedReleaseKey = "different-latest-key"; ReleaseKeyPair differentKey = new ReleaseKeyPair(someClientSideReleaseKey, differentLatestMergedReleaseKey); assertNull(configurationChangeCache.getIfPresent(differentKey)); } @Test public void testConfigurationChangeCacheWithNullValues() { List result = defaultIncrementalSyncService .getConfigurationChanges(someLatestMergedReleaseKey, null, someClientSideReleaseKey, null); ReleaseKeyPair key = new ReleaseKeyPair(someClientSideReleaseKey, someLatestMergedReleaseKey); List cachedResult = configurationChangeCache.getIfPresent(key); assertNotNull(cachedResult); assertSame(result, cachedResult); Assert.assertTrue(result.isEmpty()); } @Test public void testChangeConfigurationsWithAdd() { String key1 = "key1"; String value1 = "value1"; String key2 = "key2"; String value2 = "value2"; Map latestConfig = ImmutableMap.of(key1, value1, key2, value2); Map clientSideConfigurations = ImmutableMap.of(key1, value1); List result = defaultIncrementalSyncService.getConfigurationChanges( newReleaseKey, latestConfig, someReleaseKey, clientSideConfigurations); assertEquals(1, result.size()); assertEquals(key2, result.get(0).getKey()); assertEquals(value2, result.get(0).getNewValue()); assertEquals("ADDED", result.get(0).getConfigurationChangeType()); } @Test public void testChangeConfigurationsWithLatestConfigIsNULL() { String key1 = "key1"; String value1 = "value1"; Map clientSideConfigurations = ImmutableMap.of(key1, value1); List result = defaultIncrementalSyncService .getConfigurationChanges(newReleaseKey, null, someReleaseKey, clientSideConfigurations); assertEquals(1, result.size()); assertEquals(key1, result.get(0).getKey()); assertEquals(null, result.get(0).getNewValue()); assertEquals("DELETED", result.get(0).getConfigurationChangeType()); } @Test public void testChangeConfigurationsWithHistoryConfigIsNULL() { String key1 = "key1"; String value1 = "value1"; Map latestConfig = ImmutableMap.of(key1, value1); List result = defaultIncrementalSyncService .getConfigurationChanges(newReleaseKey, latestConfig, someReleaseKey, null); assertEquals(1, result.size()); assertEquals(key1, result.get(0).getKey()); assertEquals(value1, result.get(0).getNewValue()); assertEquals("ADDED", result.get(0).getConfigurationChangeType()); } @Test public void testChangeConfigurationsWithUpdate() { String key1 = "key1"; String value1 = "value1"; String anotherValue1 = "anotherValue1"; Map latestConfig = ImmutableMap.of(key1, anotherValue1); Map clientSideConfigurations = ImmutableMap.of(key1, value1); List result = defaultIncrementalSyncService.getConfigurationChanges( newReleaseKey, latestConfig, someReleaseKey, clientSideConfigurations); assertEquals(1, result.size()); assertEquals(key1, result.get(0).getKey()); assertEquals(anotherValue1, result.get(0).getNewValue()); assertEquals("MODIFIED", result.get(0).getConfigurationChangeType()); } @Test public void testChangeConfigurationsWithDelete() { String key1 = "key1"; String value1 = "value1"; Map latestConfig = ImmutableMap.of(); Map clientSideConfigurations = ImmutableMap.of(key1, value1); List result = defaultIncrementalSyncService.getConfigurationChanges( newReleaseKey, latestConfig, someReleaseKey, clientSideConfigurations); assertEquals(1, result.size()); assertEquals(key1, result.get(0).getKey()); assertEquals(null, result.get(0).getNewValue()); assertEquals("DELETED", result.get(0).getConfigurationChangeType()); } } ================================================ FILE: apollo-configservice/src/test/java/com/ctrip/framework/apollo/configservice/util/AccessKeyUtilTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.configservice.util; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import com.ctrip.framework.apollo.configservice.service.AccessKeyServiceWithCache; import com.google.common.collect.Lists; import java.util.List; import jakarta.servlet.http.HttpServletRequest; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; /** * @author nisiyong */ @RunWith(MockitoJUnitRunner.class) public class AccessKeyUtilTest { private AccessKeyUtil accessKeyUtil; @Mock private AccessKeyServiceWithCache accessKeyServiceWithCache; @Mock private HttpServletRequest request; @Before public void setUp() { accessKeyUtil = new AccessKeyUtil(accessKeyServiceWithCache); } @Test public void testFindAvailableSecret() { String appId = "someAppId"; List returnSecrets = Lists.newArrayList("someSecret"); when(accessKeyServiceWithCache.getAvailableSecrets(appId)).thenReturn(returnSecrets); List availableSecret = accessKeyUtil.findAvailableSecret(appId); assertThat(availableSecret).containsExactly("someSecret"); verify(accessKeyServiceWithCache).getAvailableSecrets(appId); } @Test public void testExtractAppIdFromRequest1() { when(request.getServletPath()).thenReturn("/configs/someAppId/default/application"); String appId = accessKeyUtil.extractAppIdFromRequest(request); assertThat(appId).isEqualTo("someAppId"); } @Test public void testExtractAppIdFromRequest2() { when(request.getServletPath()).thenReturn("/configfiles/json/someAppId/default/application"); String appId = accessKeyUtil.extractAppIdFromRequest(request); assertThat(appId).isEqualTo("someAppId"); } @Test public void testExtractAppIdFromRequest3() { when(request.getServletPath()).thenReturn("/configfiles/someAppId/default/application"); String appId = accessKeyUtil.extractAppIdFromRequest(request); assertThat(appId).isEqualTo("someAppId"); } @Test public void testExtractAppIdFromRequest4() { when(request.getServletPath()).thenReturn("/notifications/v2"); when(request.getParameter("appId")).thenReturn("someAppId"); String appId = accessKeyUtil.extractAppIdFromRequest(request); assertThat(appId).isEqualTo("someAppId"); } @Test public void buildSignature() { String path = "/configs/someAppId/default/application"; String query = "ip=10.0.0.1"; String timestamp = "1575018989200"; String secret = "someSecret"; String actualSignature = accessKeyUtil.buildSignature(path, query, timestamp, secret); String expectedSignature = "WYjjyJFei6DYiaMlwZjew2O/Yqk="; assertThat(actualSignature).isEqualTo(expectedSignature); } } ================================================ FILE: apollo-configservice/src/test/java/com/ctrip/framework/apollo/configservice/util/InstanceConfigAuditUtilTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.configservice.util; import com.ctrip.framework.apollo.biz.config.BizConfig; import com.ctrip.framework.apollo.biz.entity.Instance; import com.ctrip.framework.apollo.biz.entity.InstanceConfig; import com.ctrip.framework.apollo.biz.service.InstanceService; import io.micrometer.core.instrument.MeterRegistry; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import org.springframework.test.util.ReflectionTestUtils; import java.util.Objects; import java.util.concurrent.BlockingQueue; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.*; /** * @author Jason Song(song_s@ctrip.com) */ @RunWith(MockitoJUnitRunner.class) public class InstanceConfigAuditUtilTest { private InstanceConfigAuditUtil instanceConfigAuditUtil; @Mock private InstanceService instanceService; @Mock private BizConfig bizConfig; @Mock private MeterRegistry meterRegistry; private BlockingQueue audits; private String someAppId; private String someConfigClusterName; private String someClusterName; private String someDataCenter; private String someIp; private String someConfigAppId; private String someConfigNamespace; private String someReleaseKey; private InstanceConfigAuditUtil.InstanceConfigAuditModel someAuditModel; @Before public void setUp() throws Exception { when(bizConfig.getInstanceConfigAuditMaxSize()).thenReturn(100); when(bizConfig.getInstanceCacheMaxSize()).thenReturn(100); when(bizConfig.getInstanceConfigCacheMaxSize()).thenReturn(100); instanceConfigAuditUtil = new InstanceConfigAuditUtil(instanceService, bizConfig, meterRegistry); audits = (BlockingQueue) ReflectionTestUtils.getField(instanceConfigAuditUtil, "audits"); someAppId = "someAppId"; someClusterName = "someClusterName"; someDataCenter = "someDataCenter"; someIp = "someIp"; someConfigAppId = "someConfigAppId"; someConfigClusterName = "someConfigClusterName"; someConfigNamespace = "someConfigNamespace"; someReleaseKey = "someReleaseKey"; someAuditModel = new InstanceConfigAuditUtil.InstanceConfigAuditModel(someAppId, someClusterName, someDataCenter, someIp, someConfigAppId, someConfigClusterName, someConfigNamespace, someReleaseKey); } @Test public void testAudit() throws Exception { boolean result = instanceConfigAuditUtil.audit(someAppId, someClusterName, someDataCenter, someIp, someConfigAppId, someConfigClusterName, someConfigNamespace, someReleaseKey); InstanceConfigAuditUtil.InstanceConfigAuditModel audit = audits.poll(); assertTrue(result); assertTrue(Objects.equals(someAuditModel, audit)); } @Test public void testDoAudit() throws Exception { long someInstanceId = 1; Instance someInstance = mock(Instance.class); when(someInstance.getId()).thenReturn(someInstanceId); when(instanceService.createInstance(any(Instance.class))).thenReturn(someInstance); instanceConfigAuditUtil.doAudit(someAuditModel); verify(instanceService, times(1)).findInstance(someAppId, someClusterName, someDataCenter, someIp); verify(instanceService, times(1)).createInstance(any(Instance.class)); verify(instanceService, times(1)).findInstanceConfig(someInstanceId, someConfigAppId, someConfigNamespace); verify(instanceService, times(1)).createInstanceConfig(any(InstanceConfig.class)); } } ================================================ FILE: apollo-configservice/src/test/java/com/ctrip/framework/apollo/configservice/util/NamespaceUtilTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.configservice.util; import com.ctrip.framework.apollo.common.entity.AppNamespace; import com.ctrip.framework.apollo.configservice.service.AppNamespaceServiceWithCache; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.*; /** * @author Jason Song(song_s@ctrip.com) */ @RunWith(MockitoJUnitRunner.class) public class NamespaceUtilTest { private NamespaceUtil namespaceUtil; @Mock private AppNamespaceServiceWithCache appNamespaceServiceWithCache; @Before public void setUp() throws Exception { namespaceUtil = new NamespaceUtil(appNamespaceServiceWithCache); } @Test public void testFilterNamespaceName() throws Exception { String someName = "a.properties"; assertEquals("a", namespaceUtil.filterNamespaceName(someName)); } @Test public void testFilterNamespaceNameUnchanged() throws Exception { String someName = "a.xml"; assertEquals(someName, namespaceUtil.filterNamespaceName(someName)); } @Test public void testFilterNamespaceNameWithMultiplePropertiesSuffix() throws Exception { String someName = "a.properties.properties"; assertEquals("a.properties", namespaceUtil.filterNamespaceName(someName)); } @Test public void testFilterNamespaceNameWithRandomCase() throws Exception { String someName = "AbC.ProPErties"; assertEquals("AbC", namespaceUtil.filterNamespaceName(someName)); } @Test public void testFilterNamespaceNameWithRandomCaseUnchanged() throws Exception { String someName = "AbCD.xMl"; assertEquals(someName, namespaceUtil.filterNamespaceName(someName)); } @Test public void testNormalizeNamespaceWithPrivateNamespace() throws Exception { String someAppId = "someAppId"; String someNamespaceName = "someNamespaceName"; String someNormalizedNamespaceName = "someNormalizedNamespaceName"; AppNamespace someAppNamespace = mock(AppNamespace.class); when(someAppNamespace.getName()).thenReturn(someNormalizedNamespaceName); when(appNamespaceServiceWithCache.findByAppIdAndNamespace(someAppId, someNamespaceName)) .thenReturn(someAppNamespace); assertEquals(someNormalizedNamespaceName, namespaceUtil.normalizeNamespace(someAppId, someNamespaceName)); verify(appNamespaceServiceWithCache, times(1)).findByAppIdAndNamespace(someAppId, someNamespaceName); verify(appNamespaceServiceWithCache, never()).findPublicNamespaceByName(someNamespaceName); } @Test public void testNormalizeNamespaceWithPublicNamespace() throws Exception { String someAppId = "someAppId"; String someNamespaceName = "someNamespaceName"; String someNormalizedNamespaceName = "someNormalizedNamespaceName"; AppNamespace someAppNamespace = mock(AppNamespace.class); when(someAppNamespace.getName()).thenReturn(someNormalizedNamespaceName); when(appNamespaceServiceWithCache.findByAppIdAndNamespace(someAppId, someNamespaceName)) .thenReturn(null); when(appNamespaceServiceWithCache.findPublicNamespaceByName(someNamespaceName)) .thenReturn(someAppNamespace); assertEquals(someNormalizedNamespaceName, namespaceUtil.normalizeNamespace(someAppId, someNamespaceName)); verify(appNamespaceServiceWithCache, times(1)).findByAppIdAndNamespace(someAppId, someNamespaceName); verify(appNamespaceServiceWithCache, times(1)).findPublicNamespaceByName(someNamespaceName); } @Test public void testNormalizeNamespaceFailed() throws Exception { String someAppId = "someAppId"; String someNamespaceName = "someNamespaceName"; when(appNamespaceServiceWithCache.findByAppIdAndNamespace(someAppId, someNamespaceName)) .thenReturn(null); when(appNamespaceServiceWithCache.findPublicNamespaceByName(someNamespaceName)) .thenReturn(null); assertEquals(someNamespaceName, namespaceUtil.normalizeNamespace(someAppId, someNamespaceName)); verify(appNamespaceServiceWithCache, times(1)).findByAppIdAndNamespace(someAppId, someNamespaceName); verify(appNamespaceServiceWithCache, times(1)).findPublicNamespaceByName(someNamespaceName); } } ================================================ FILE: apollo-configservice/src/test/java/com/ctrip/framework/apollo/configservice/util/WatchKeysUtilTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.configservice.util; import com.ctrip.framework.apollo.common.entity.AppNamespace; import com.ctrip.framework.apollo.configservice.service.AppNamespaceServiceWithCache; import com.ctrip.framework.apollo.core.ConfigConsts; import com.google.common.base.Joiner; import com.google.common.collect.Lists; import com.google.common.collect.Multimap; import com.google.common.collect.Sets; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import java.util.Collection; import java.util.Set; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.when; /** * @author Jason Song(song_s@ctrip.com) */ @RunWith(MockitoJUnitRunner.class) public class WatchKeysUtilTest { @Mock private AppNamespaceServiceWithCache appNamespaceService; @Mock private AppNamespace someAppNamespace; @Mock private AppNamespace anotherAppNamespace; @Mock private AppNamespace somePublicAppNamespace; private WatchKeysUtil watchKeysUtil; private String someAppId; private String someCluster; private String someNamespace; private String anotherNamespace; private String somePublicNamespace; private String defaultCluster; private String someDC; private String somePublicAppId; @Before public void setUp() throws Exception { watchKeysUtil = new WatchKeysUtil(appNamespaceService); someAppId = "someId"; someCluster = "someCluster"; someNamespace = "someName"; anotherNamespace = "anotherName"; somePublicNamespace = "somePublicName"; defaultCluster = ConfigConsts.CLUSTER_NAME_DEFAULT; someDC = "someDC"; somePublicAppId = "somePublicId"; when(someAppNamespace.getName()).thenReturn(someNamespace); when(anotherAppNamespace.getName()).thenReturn(anotherNamespace); when(appNamespaceService.findByAppIdAndNamespaces(someAppId, Sets.newHashSet(someNamespace))) .thenReturn(Lists.newArrayList(someAppNamespace)); when(appNamespaceService.findByAppIdAndNamespaces(someAppId, Sets.newHashSet(someNamespace, anotherNamespace))) .thenReturn(Lists.newArrayList(someAppNamespace, anotherAppNamespace)); when(appNamespaceService.findByAppIdAndNamespaces(someAppId, Sets.newHashSet(someNamespace, anotherNamespace, somePublicNamespace))) .thenReturn(Lists.newArrayList(someAppNamespace, anotherAppNamespace)); when(somePublicAppNamespace.getAppId()).thenReturn(somePublicAppId); when(somePublicAppNamespace.getName()).thenReturn(somePublicNamespace); when(appNamespaceService.findPublicNamespacesByNames(Sets.newHashSet(somePublicNamespace))) .thenReturn(Lists.newArrayList(somePublicAppNamespace)); when(appNamespaceService .findPublicNamespacesByNames(Sets.newHashSet(someNamespace, somePublicNamespace))) .thenReturn(Lists.newArrayList(somePublicAppNamespace)); } @Test public void testAssembleAllWatchKeysWithOneNamespaceAndDefaultCluster() throws Exception { Set watchKeys = watchKeysUtil.assembleAllWatchKeys(someAppId, defaultCluster, someNamespace, null); Set clusters = Sets.newHashSet(defaultCluster); assertEquals(clusters.size(), watchKeys.size()); assertWatchKeys(someAppId, clusters, someNamespace, watchKeys); } @Test public void testAssembleAllWatchKeysWithOneNamespaceAndSomeDC() throws Exception { Set watchKeys = watchKeysUtil.assembleAllWatchKeys(someAppId, someDC, someNamespace, someDC); Set clusters = Sets.newHashSet(defaultCluster, someDC); assertEquals(clusters.size(), watchKeys.size()); assertWatchKeys(someAppId, clusters, someNamespace, watchKeys); } @Test public void testAssembleAllWatchKeysWithOneNamespaceAndSomeDCAndSomeCluster() throws Exception { Set watchKeys = watchKeysUtil.assembleAllWatchKeys(someAppId, someCluster, someNamespace, someDC); Set clusters = Sets.newHashSet(defaultCluster, someCluster, someDC); assertEquals(clusters.size(), watchKeys.size()); assertWatchKeys(someAppId, clusters, someNamespace, watchKeys); } @Test public void testAssembleAllWatchKeysWithMultipleNamespaces() throws Exception { Multimap watchKeysMap = watchKeysUtil.assembleAllWatchKeys(someAppId, someCluster, Sets.newHashSet(someNamespace, anotherNamespace), someDC); Set clusters = Sets.newHashSet(defaultCluster, someCluster, someDC); assertEquals(clusters.size() * 2, watchKeysMap.size()); assertWatchKeys(someAppId, clusters, someNamespace, watchKeysMap.get(someNamespace)); assertWatchKeys(someAppId, clusters, anotherNamespace, watchKeysMap.get(anotherNamespace)); } @Test public void testAssembleAllWatchKeysWithPrivateAndPublicNamespaces() throws Exception { Multimap watchKeysMap = watchKeysUtil.assembleAllWatchKeys(someAppId, someCluster, Sets.newHashSet(someNamespace, anotherNamespace, somePublicNamespace), someDC); Set clusters = Sets.newHashSet(defaultCluster, someCluster, someDC); assertEquals(clusters.size() * 4, watchKeysMap.size()); assertWatchKeys(someAppId, clusters, someNamespace, watchKeysMap.get(someNamespace)); assertWatchKeys(someAppId, clusters, anotherNamespace, watchKeysMap.get(anotherNamespace)); assertWatchKeys(someAppId, clusters, somePublicNamespace, watchKeysMap.get(somePublicNamespace)); assertWatchKeys(somePublicAppId, clusters, somePublicNamespace, watchKeysMap.get(somePublicNamespace)); } @Test public void testAssembleWatchKeysForNoAppIdPlaceHolder() throws Exception { Multimap watchKeysMap = watchKeysUtil.assembleAllWatchKeys(ConfigConsts.NO_APPID_PLACEHOLDER, someCluster, Sets.newHashSet(someNamespace, anotherNamespace), someDC); assertTrue(watchKeysMap.isEmpty()); } @Test public void testAssembleWatchKeysForNoAppIdPlaceHolderAndPublicNamespace() throws Exception { Multimap watchKeysMap = watchKeysUtil.assembleAllWatchKeys(ConfigConsts.NO_APPID_PLACEHOLDER, someCluster, Sets.newHashSet(someNamespace, somePublicNamespace), someDC); Set clusters = Sets.newHashSet(defaultCluster, someCluster, someDC); assertEquals(clusters.size(), watchKeysMap.size()); assertWatchKeys(somePublicAppId, clusters, somePublicNamespace, watchKeysMap.get(somePublicNamespace)); } private void assertWatchKeys(String appId, Set clusters, String namespaceName, Collection watchedKeys) { for (String cluster : clusters) { String key = Joiner.on(ConfigConsts.CLUSTER_NAMESPACE_SEPARATOR).join(appId, cluster, namespaceName); assertTrue(watchedKeys.contains(key)); } } } ================================================ FILE: apollo-configservice/src/test/java/com/ctrip/framework/apollo/configservice/wrapper/CaseInsensitiveMapWrapperTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.configservice.wrapper; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import java.util.Map; import static org.junit.Assert.*; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; /** @author Jason Song(song_s@ctrip.com) */ @RunWith(MockitoJUnitRunner.class) public class CaseInsensitiveMapWrapperTest { private CaseInsensitiveMapWrapper caseInsensitiveMapWrapper; @Mock private Map someMap; @Before public void setUp() throws Exception { caseInsensitiveMapWrapper = new CaseInsensitiveMapWrapper<>(someMap); } @Test public void testGet() throws Exception { String someKey = "someKey"; Object someValue = mock(Object.class); when(someMap.get(someKey.toLowerCase())).thenReturn(someValue); assertEquals(someValue, caseInsensitiveMapWrapper.get(someKey)); verify(someMap, times(1)).get(someKey.toLowerCase()); } @Test public void testPut() throws Exception { String someKey = "someKey"; Object someValue = mock(Object.class); Object anotherValue = mock(Object.class); when(someMap.put(someKey.toLowerCase(), someValue)).thenReturn(anotherValue); assertEquals(anotherValue, caseInsensitiveMapWrapper.put(someKey, someValue)); verify(someMap, times(1)).put(someKey.toLowerCase(), someValue); } @Test public void testRemove() throws Exception { String someKey = "someKey"; Object someValue = mock(Object.class); when(someMap.remove(someKey.toLowerCase())).thenReturn(someValue); assertEquals(someValue, caseInsensitiveMapWrapper.remove(someKey)); verify(someMap, times(1)).remove(someKey.toLowerCase()); } } ================================================ FILE: apollo-configservice/src/test/java/com/ctrip/framework/apollo/configservice/wrapper/CaseInsensitiveMultimapWrapperTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.configservice.wrapper; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import java.util.Set; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import org.junit.Before; import org.junit.Test; public class CaseInsensitiveMultimapWrapperTest { private CaseInsensitiveMultimapWrapper multimap; @Before public void setUp() throws Exception { multimap = new CaseInsensitiveMultimapWrapper<>(Maps.newConcurrentMap(), Sets::newConcurrentHashSet); } @Test public void testPutAndGet() { String key = "SomeKey"; String value1 = "value1"; String value2 = "value2"; assertTrue(multimap.put(key, value1)); assertTrue(multimap.put(key.toLowerCase(), value2)); assertFalse(multimap.put(key.toUpperCase(), value1)); // already exists Set values = multimap.get(key); assertEquals(2, values.size()); assertTrue(values.contains(value1)); assertTrue(values.contains(value2)); Set valuesFromLower = multimap.get(key.toLowerCase()); assertEquals(values, valuesFromLower); } @Test public void testRemove() { String key = "SomeKey"; String value = "someValue"; multimap.put(key, value); assertTrue(multimap.containsKey(key)); assertTrue(multimap.remove(key.toUpperCase(), value)); assertFalse(multimap.containsKey(key)); assertTrue(multimap.get(key).isEmpty()); } @Test public void testContainsKey() { String key = "SomeKey"; String value = "someValue"; assertFalse(multimap.containsKey(key)); multimap.put(key, value); assertTrue(multimap.containsKey(key.toLowerCase())); assertTrue(multimap.containsKey(key.toUpperCase())); } @Test public void testSize() { multimap.put("Key1", "v1"); multimap.put("key1", "v2"); multimap.put("Key2", "v3"); assertEquals(3, multimap.size()); multimap.remove("KEY1", "v1"); assertEquals(2, multimap.size()); } @Test public void testGetEmpty() { assertTrue(multimap.get("nonExistent").isEmpty()); } @Test public void testConcurrencyRaceCondition() throws InterruptedException { final int loopCount = 100000; final String key = "RaceKey"; final String valA = "A"; final String valB = "B"; final CountDownLatch latch = new CountDownLatch(2); final AtomicBoolean running = new AtomicBoolean(true); final AtomicInteger failures = new AtomicInteger(0); final AtomicReference threadException = new AtomicReference<>(); // Thread A: toggles valA new Thread(() -> { try { while (running.get()) { multimap.put(key, valA); multimap.remove(key, valA); } } catch (Throwable e) { threadException.set(e); } finally { latch.countDown(); } }).start(); // Thread B: repeatedly adds and removes valB to test put atomicity new Thread(() -> { try { for (int i = 0; i < loopCount; i++) { multimap.put(key, valB); if (!multimap.get(key).contains(valB)) { failures.incrementAndGet(); } // Remove B to allow the set to become empty again, // giving Thread A a chance to trigger the map removal race condition again. multimap.remove(key, valB); } } catch (Throwable e) { threadException.set(e); } finally { running.set(false); latch.countDown(); } }).start(); latch.await(); if (threadException.get() != null) { throw new RuntimeException("Exception in worker thread", threadException.get()); } assertEquals("Value B should not be lost due to race condition", 0, failures.get()); } @Test public void testGetReturnsUnmodifiableView() { assertTrue(multimap.put("Key", "Value")); Set set = multimap.get("key"); assertTrue(set.contains("Value")); try { set.add("Another"); org.junit.Assert.fail("get() should return an unmodifiable view"); } catch (UnsupportedOperationException expected) { // expected } } } ================================================ FILE: apollo-configservice/src/test/java/com/ctrip/framework/apollo/metaservice/controller/HomePageControllerTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.metaservice.controller; import static org.junit.Assert.*; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import com.ctrip.framework.apollo.core.ServiceNameConsts; import com.ctrip.framework.apollo.core.dto.ServiceDTO; import com.ctrip.framework.apollo.metaservice.service.DiscoveryService; import com.google.common.collect.Lists; import java.util.List; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; @RunWith(MockitoJUnitRunner.class) public class HomePageControllerTest { @Mock private DiscoveryService discoveryService; private HomePageController homePageController; @Before public void setUp() throws Exception { homePageController = new HomePageController(discoveryService); } @Test public void testListAllServices() { ServiceDTO someServiceDto = mock(ServiceDTO.class); ServiceDTO anotherServiceDto = mock(ServiceDTO.class); when(discoveryService.getServiceInstances(ServiceNameConsts.APOLLO_CONFIGSERVICE)) .thenReturn(Lists.newArrayList(someServiceDto)); when(discoveryService.getServiceInstances(ServiceNameConsts.APOLLO_ADMINSERVICE)) .thenReturn(Lists.newArrayList(anotherServiceDto)); List allServices = homePageController.listAllServices(); assertEquals(2, allServices.size()); assertSame(someServiceDto, allServices.get(0)); assertSame(anotherServiceDto, allServices.get(1)); } } ================================================ FILE: apollo-configservice/src/test/java/com/ctrip/framework/apollo/metaservice/controller/ServiceControllerTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.metaservice.controller; import static org.junit.Assert.*; import static org.mockito.Mockito.when; import com.ctrip.framework.apollo.core.ServiceNameConsts; import com.ctrip.framework.apollo.core.dto.ServiceDTO; import com.ctrip.framework.apollo.metaservice.service.DiscoveryService; import java.util.List; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; @RunWith(MockitoJUnitRunner.class) public class ServiceControllerTest { @Mock private DiscoveryService discoveryService; @Mock private List someServices; private ServiceController serviceController; @Before public void setUp() throws Exception { serviceController = new ServiceController(discoveryService); } @Test public void testGetMetaService() { assertTrue(serviceController.getMetaService().isEmpty()); } @Test public void testGetConfigService() { String someAppId = "someAppId"; String someClientIp = "someClientIp"; when(discoveryService.getServiceInstances(ServiceNameConsts.APOLLO_CONFIGSERVICE)) .thenReturn(someServices); assertEquals(someServices, serviceController.getConfigService(someAppId, someClientIp)); } @Test public void testGetAdminService() { when(discoveryService.getServiceInstances(ServiceNameConsts.APOLLO_ADMINSERVICE)) .thenReturn(someServices); assertEquals(someServices, serviceController.getAdminService()); } } ================================================ FILE: apollo-configservice/src/test/java/com/ctrip/framework/apollo/metaservice/service/ConsulDiscoveryServiceTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.metaservice.service; import com.ctrip.framework.apollo.core.dto.ServiceDTO; import com.google.common.collect.Lists; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.consul.discovery.ConsulDiscoveryClient; import java.util.List; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; /** * @author kl (http://kailing.pub) * @since 2021/3/1 */ @RunWith(MockitoJUnitRunner.class) public class ConsulDiscoveryServiceTest { @Mock private ConsulDiscoveryClient consulDiscoveryClient; private SpringCloudInnerDiscoveryService consulDiscoveryService; private String someServiceId; @Before public void setUp() throws Exception { consulDiscoveryService = new SpringCloudInnerDiscoveryService(consulDiscoveryClient); someServiceId = "someServiceId"; } @Test public void testGetServiceInstancesWithNullInstances() { when(consulDiscoveryClient.getInstances(someServiceId)).thenReturn(null); assertTrue(consulDiscoveryService.getServiceInstances(someServiceId).isEmpty()); } @Test public void testGetServiceInstances() { String someIp = "1.2.3.4"; int somePort = 8080; String someInstanceId = "someInstanceId"; ServiceInstance someServiceInstance = mockServiceInstance(someInstanceId, someIp, somePort); when(consulDiscoveryClient.getInstances(someServiceId)) .thenReturn(Lists.newArrayList(someServiceInstance)); List serviceDTOList = consulDiscoveryService.getServiceInstances(someServiceId); ServiceDTO serviceDTO = serviceDTOList.get(0); assertEquals(1, serviceDTOList.size()); assertEquals(someServiceId, serviceDTO.getAppName()); assertEquals("http://1.2.3.4:8080/", serviceDTO.getHomepageUrl()); } private ServiceInstance mockServiceInstance(String instanceId, String ip, int port) { ServiceInstance serviceInstance = mock(ServiceInstance.class); when(serviceInstance.getInstanceId()).thenReturn(instanceId); when(serviceInstance.getHost()).thenReturn(ip); when(serviceInstance.getPort()).thenReturn(port); return serviceInstance; } } ================================================ FILE: apollo-configservice/src/test/java/com/ctrip/framework/apollo/metaservice/service/DefaultDiscoveryServiceTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.metaservice.service; import static org.junit.Assert.*; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import com.ctrip.framework.apollo.core.dto.ServiceDTO; import com.google.common.collect.Lists; import com.netflix.appinfo.InstanceInfo; import com.netflix.discovery.EurekaClient; import com.netflix.discovery.shared.Application; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.List; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; @RunWith(MockitoJUnitRunner.class) public class DefaultDiscoveryServiceTest { @Mock private EurekaClient eurekaClient; @Mock private Application someApplication; private DefaultDiscoveryService defaultDiscoveryService; private String someServiceId; @Before public void setUp() throws Exception { defaultDiscoveryService = new DefaultDiscoveryService(eurekaClient); someServiceId = "someServiceId"; } @Test public void testGetServiceInstancesWithNullInstances() { when(eurekaClient.getApplication(someServiceId)).thenReturn(null); assertTrue(defaultDiscoveryService.getServiceInstances(someServiceId).isEmpty()); } @Test public void testGetServiceInstancesWithEmptyInstances() { when(eurekaClient.getApplication(someServiceId)).thenReturn(someApplication); when(someApplication.getInstances()).thenReturn(new ArrayList<>()); assertTrue(defaultDiscoveryService.getServiceInstances(someServiceId).isEmpty()); } @Test public void testGetServiceInstances() throws URISyntaxException { String someUri = "http://1.2.3.4:8080/some-path/"; String someInstanceId = "someInstanceId"; InstanceInfo someServiceInstance = mockServiceInstance(someServiceId, someInstanceId, someUri); String anotherUri = "http://2.3.4.5:9090/anotherPath"; String anotherInstanceId = "anotherInstanceId"; InstanceInfo anotherServiceInstance = mockServiceInstance(someServiceId, anotherInstanceId, anotherUri); when(eurekaClient.getApplication(someServiceId)).thenReturn(someApplication); when(someApplication.getInstances()) .thenReturn(Lists.newArrayList(someServiceInstance, anotherServiceInstance)); List serviceDTOList = defaultDiscoveryService.getServiceInstances(someServiceId); assertEquals(2, serviceDTOList.size()); check(someServiceInstance, serviceDTOList.get(0)); check(anotherServiceInstance, serviceDTOList.get(1)); } private void check(InstanceInfo serviceInstance, ServiceDTO serviceDTO) { assertEquals(serviceInstance.getAppName(), serviceDTO.getAppName()); assertEquals(serviceInstance.getInstanceId(), serviceDTO.getInstanceId()); assertEquals(serviceInstance.getHomePageUrl(), serviceDTO.getHomepageUrl()); } private InstanceInfo mockServiceInstance(String serviceId, String instanceId, String homePageUrl) { InstanceInfo serviceInstance = mock(InstanceInfo.class); when(serviceInstance.getAppName()).thenReturn(serviceId); when(serviceInstance.getInstanceId()).thenReturn(instanceId); when(serviceInstance.getHomePageUrl()).thenReturn(homePageUrl); return serviceInstance; } } ================================================ FILE: apollo-configservice/src/test/java/com/ctrip/framework/apollo/metaservice/service/KubernetesDiscoveryServiceTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.metaservice.service; import static org.junit.Assert.*; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import com.ctrip.framework.apollo.biz.config.BizConfig; import com.ctrip.framework.apollo.core.ServiceNameConsts; import com.ctrip.framework.apollo.core.dto.ServiceDTO; import java.util.List; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; @RunWith(MockitoJUnitRunner.class) public class KubernetesDiscoveryServiceTest { private String configServiceConfigName = "apollo.config-service.url"; private String adminServiceConfigName = "apollo.admin-service.url"; @Mock private BizConfig bizConfig; private KubernetesDiscoveryService kubernetesDiscoveryService; @Before public void setUp() throws Exception { kubernetesDiscoveryService = new KubernetesDiscoveryService(bizConfig); } @Test public void testGetServiceInstancesWithInvalidServiceId() { String someInvalidServiceId = "someInvalidServiceId"; assertTrue(kubernetesDiscoveryService.getServiceInstances(someInvalidServiceId).isEmpty()); } @Test public void testGetServiceInstancesWithNullConfig() { when(bizConfig.getValue(configServiceConfigName)).thenReturn(null); assertTrue( kubernetesDiscoveryService.getServiceInstances(ServiceNameConsts.APOLLO_CONFIGSERVICE) .isEmpty()); verify(bizConfig, times(1)).getValue(configServiceConfigName); } @Test public void testGetConfigServiceInstances() { String someUrl = "http://some-host/some-path"; when(bizConfig.getValue(configServiceConfigName)).thenReturn(someUrl); List serviceDTOList = kubernetesDiscoveryService.getServiceInstances(ServiceNameConsts.APOLLO_CONFIGSERVICE); assertEquals(1, serviceDTOList.size()); ServiceDTO serviceDTO = serviceDTOList.get(0); assertEquals(ServiceNameConsts.APOLLO_CONFIGSERVICE, serviceDTO.getAppName()); assertEquals(String.format("%s:%s", ServiceNameConsts.APOLLO_CONFIGSERVICE, someUrl), serviceDTO.getInstanceId()); assertEquals(someUrl, serviceDTO.getHomepageUrl()); } @Test public void testGetAdminServiceInstances() { String someUrl = "http://some-host/some-path"; String anotherUrl = "http://another-host/another-path"; when(bizConfig.getValue(adminServiceConfigName)) .thenReturn(String.format("%s,%s", someUrl, anotherUrl)); List serviceDTOList = kubernetesDiscoveryService.getServiceInstances(ServiceNameConsts.APOLLO_ADMINSERVICE); assertEquals(2, serviceDTOList.size()); ServiceDTO serviceDTO = serviceDTOList.get(0); assertEquals(ServiceNameConsts.APOLLO_ADMINSERVICE, serviceDTO.getAppName()); assertEquals(String.format("%s:%s", ServiceNameConsts.APOLLO_ADMINSERVICE, someUrl), serviceDTO.getInstanceId()); assertEquals(someUrl, serviceDTO.getHomepageUrl()); ServiceDTO anotherServiceDTO = serviceDTOList.get(1); assertEquals(ServiceNameConsts.APOLLO_ADMINSERVICE, anotherServiceDTO.getAppName()); assertEquals(String.format("%s:%s", ServiceNameConsts.APOLLO_ADMINSERVICE, anotherUrl), anotherServiceDTO.getInstanceId()); assertEquals(anotherUrl, anotherServiceDTO.getHomepageUrl()); } } ================================================ FILE: apollo-configservice/src/test/java/com/ctrip/framework/apollo/metaservice/service/NacosDiscoveryServiceTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.metaservice.service; import com.alibaba.nacos.api.naming.NamingService; import com.alibaba.nacos.api.naming.pojo.Instance; import com.ctrip.framework.apollo.core.dto.ServiceDTO; import com.google.common.collect.Lists; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import java.util.List; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; /** * @author kl (http://kailing.pub) * @since 2020/12/21 */ @RunWith(MockitoJUnitRunner.class) public class NacosDiscoveryServiceTest { private NacosDiscoveryService nacosDiscoveryService; @Mock private NamingService nacosNamingService; private String someServiceId; @Before public void setUp() throws Exception { nacosDiscoveryService = new NacosDiscoveryService(); nacosDiscoveryService.setNamingService(nacosNamingService); someServiceId = "someServiceId"; } @Test public void testGetServiceInstancesWithEmptyInstances() throws Exception { assertTrue(nacosNamingService.selectInstances(someServiceId, true).isEmpty()); } @Test public void testGetServiceInstancesWithInvalidServiceId() { assertTrue(nacosDiscoveryService.getServiceInstances(someServiceId).isEmpty()); } @Test public void testGetServiceInstances() throws Exception { String someIp = "1.2.3.4"; int somePort = 8080; String someInstanceId = "someInstanceId"; Instance someServiceInstance = mockServiceInstance(someInstanceId, someIp, somePort); when(nacosNamingService.selectInstances(someServiceId, true)) .thenReturn(Lists.newArrayList(someServiceInstance)); List serviceDTOList = nacosDiscoveryService.getServiceInstances(someServiceId); ServiceDTO serviceDTO = serviceDTOList.get(0); assertEquals(1, serviceDTOList.size()); assertEquals(someServiceId, serviceDTO.getAppName()); assertEquals("http://1.2.3.4:8080/", serviceDTO.getHomepageUrl()); } private Instance mockServiceInstance(String instanceId, String ip, int port) { Instance serviceInstance = mock(Instance.class); when(serviceInstance.getInstanceId()).thenReturn(instanceId); when(serviceInstance.getIp()).thenReturn(ip); when(serviceInstance.getPort()).thenReturn(port); return serviceInstance; } } ================================================ FILE: apollo-configservice/src/test/java/com/ctrip/framework/apollo/metaservice/service/ZookeeperDiscoveryServiceTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.metaservice.service; import java.util.List; import com.ctrip.framework.apollo.core.dto.ServiceDTO; import com.google.common.collect.Lists; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.zookeeper.discovery.ZookeeperDiscoveryClient; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @RunWith(MockitoJUnitRunner.class) public class ZookeeperDiscoveryServiceTest { @Mock private ZookeeperDiscoveryClient zookeeperDiscoveryClient; private SpringCloudInnerDiscoveryService zookeeperDiscoveryService; private String someServiceId; @Before public void setUp() throws Exception { zookeeperDiscoveryService = new SpringCloudInnerDiscoveryService(zookeeperDiscoveryClient); someServiceId = "someServiceId"; } @Test public void testGetServiceInstancesWithEmptyInstances() { when(zookeeperDiscoveryClient.getInstances(someServiceId)).thenReturn(null); assertTrue(zookeeperDiscoveryService.getServiceInstances(someServiceId).isEmpty()); } @Test public void testGetServiceInstances() { String someIp = "1.2.3.4"; int somePort = 8080; String someInstanceId = "someInstanceId"; ServiceInstance someServiceInstance = mockServiceInstance(someInstanceId, someIp, somePort); when(zookeeperDiscoveryClient.getInstances(someServiceId)) .thenReturn(Lists.newArrayList(someServiceInstance)); List serviceDTOList = zookeeperDiscoveryService.getServiceInstances(someServiceId); ServiceDTO serviceDTO = serviceDTOList.get(0); assertEquals(1, serviceDTOList.size()); assertEquals(someServiceId, serviceDTO.getAppName()); assertEquals("http://1.2.3.4:8080/", serviceDTO.getHomepageUrl()); } private ServiceInstance mockServiceInstance(String instanceId, String ip, int port) { ServiceInstance serviceInstance = mock(ServiceInstance.class); when(serviceInstance.getInstanceId()).thenReturn(instanceId); when(serviceInstance.getHost()).thenReturn(ip); when(serviceInstance.getPort()).thenReturn(port); return serviceInstance; } } ================================================ FILE: apollo-configservice/src/test/resources/application.properties ================================================ # # Copyright 2024 Apollo Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # spring.cloud.consul.enabled=false spring.cloud.zookeeper.enabled=false spring.datasource.url = jdbc:h2:mem:apolloconfigdb-${random.uuid};mode=mysql;DB_CLOSE_ON_EXIT=FALSE;DB_CLOSE_DELAY=-1;BUILTIN_ALIAS_OVERRIDE=TRUE;DATABASE_TO_UPPER=FALSE spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl spring.jpa.hibernate.globally_quoted_identifiers=false spring.jpa.properties.hibernate.globally_quoted_identifiers=false spring.jpa.properties.hibernate.show_sql=false spring.jpa.properties.hibernate.metadata_builder_contributor=com.ctrip.framework.apollo.common.jpa.SqlFunctionsMetadataBuilderContributor spring.jpa.defer-datasource-initialization=true spring.h2.console.enabled = true spring.h2.console.settings.web-allow-others=true spring.main.allow-bean-definition-overriding=true # for ReleaseMessageScanner test apollo.message-scan.interval=100 ================================================ FILE: apollo-configservice/src/test/resources/application.yml ================================================ # # Copyright 2024 Apollo Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # spring: application: name: apollo-configservice lifecycle: timeout-per-shutdown-phase: ${GRACEFUL_SHUTDOWN_TIMEOUT:10s} server: port: ${port:8080} shutdown: graceful eureka: instance: hostname: ${hostname:localhost} prefer-ip-address: true status-page-url-path: /info health-check-url-path: /health client: service-url: defaultZone: http://${eureka.instance.hostname}:8080/eureka/ healthcheck: enabled: true management: health: status: order: DOWN, OUT_OF_SERVICE, UNKNOWN, UP ================================================ FILE: apollo-configservice/src/test/resources/data.sql ================================================ -- -- Copyright 2024 Apollo Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. -- You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. -- INSERT INTO "App" (AppId, Name, OwnerName, OwnerEmail) VALUES ('100003171','apollo-config-service','刘一鸣','liuym@ctrip.com'); INSERT INTO "App" (AppId, Name, OwnerName, OwnerEmail) VALUES ('100003172','apollo-admin-service','宋顺','song_s@ctrip.com'); INSERT INTO "App" (AppId, Name, OwnerName, OwnerEmail) VALUES ('100003173','apollo-portal','张乐','zhanglea@ctrip.com'); INSERT INTO "App" (AppId, Name, OwnerName, OwnerEmail) VALUES ('fxhermesproducer','fx-hermes-producer','梁锦华','jhliang@ctrip.com'); INSERT INTO "Cluster" (AppId, Name) VALUES ('100003171', 'default'); INSERT INTO "Cluster" (AppId, Name) VALUES ('100003171', 'cluster1'); INSERT INTO "Cluster" (AppId, Name) VALUES ('100003172', 'default'); INSERT INTO "Cluster" (AppId, Name) VALUES ('100003172', 'cluster2'); INSERT INTO "Cluster" (AppId, Name) VALUES ('100003173', 'default'); INSERT INTO "Cluster" (AppId, Name) VALUES ('100003173', 'cluster3'); INSERT INTO "Cluster" (AppId, Name) VALUES ('fxhermesproducer', 'default'); INSERT INTO "AppNamespace" (AppId, Name) VALUES ('100003171', 'application'); INSERT INTO "AppNamespace" (AppId, Name) VALUES ('100003171', 'fx.apollo.config'); INSERT INTO "AppNamespace" (AppId, Name) VALUES ('100003172', 'application'); INSERT INTO "AppNamespace" (AppId, Name) VALUES ('100003172', 'fx.apollo.admin'); INSERT INTO "AppNamespace" (AppId, Name) VALUES ('100003173', 'application'); INSERT INTO "AppNamespace" (AppId, Name) VALUES ('100003173', 'fx.apollo.portal'); INSERT INTO "AppNamespace" (AppId, Name) VALUES ('fxhermesproducer', 'fx.hermes.producer'); INSERT INTO "Namespace" (Id, AppId, ClusterName, NamespaceName) VALUES (1, '100003171', 'default', 'application'); INSERT INTO "Namespace" (Id, AppId, ClusterName, NamespaceName) VALUES (2, 'fxhermesproducer', 'default', 'fx.hermes.producer'); INSERT INTO "Namespace" (Id, AppId, ClusterName, NamespaceName) VALUES (3, '100003172', 'default', 'application'); INSERT INTO "Namespace" (Id, AppId, ClusterName, NamespaceName) VALUES (4, '100003173', 'default', 'application'); INSERT INTO "Namespace" (Id, AppId, ClusterName, NamespaceName) VALUES (5, '100003171', 'default', 'application'); INSERT INTO "Item" (NamespaceId, "Key", "Value", Comment) VALUES (1, 'k1', 'v1', 'comment1'); INSERT INTO "Item" (NamespaceId, "Key", "Value", Comment) VALUES (1, 'k2', 'v2', 'comment2'); INSERT INTO "Item" (NamespaceId, "Key", "Value", Comment) VALUES (2, 'k3', 'v3', 'comment3'); INSERT INTO "Item" (NamespaceId, "Key", "Value", Comment) VALUES (5, 'k3', 'v4', 'comment4'); INSERT INTO "Release" (ReleaseKey, Name, Comment, AppId, ClusterName, NamespaceName, Configurations) VALUES ('TEST-RELEASE-KEY', 'REV1','First Release','100003171', 'default', 'application', '{"k1":"v1"}'); ================================================ FILE: apollo-configservice/src/test/resources/import.sql ================================================ -- -- Copyright 2024 Apollo Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. -- You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. -- ALTER TABLE "App" ALTER COLUMN DataChange_CreatedBy VARCHAR(255) NULL; ALTER TABLE "App" ALTER COLUMN DataChange_CreatedTime TIMESTAMP NULL; ALTER TABLE "App" ALTER COLUMN OrgName VARCHAR(255) NULL; ALTER TABLE "App" ALTER COLUMN OrgId VARCHAR(255) NULL; ALTER TABLE "Cluster" ALTER COLUMN DataChange_CreatedBy VARCHAR(255) NULL; ALTER TABLE "Cluster" ALTER COLUMN DataChange_CreatedTime TIMESTAMP NULL; ALTER TABLE "Cluster" ALTER COLUMN ParentClusterId BIGINT DEFAULT 0; ALTER TABLE "Namespace" ALTER COLUMN DataChange_CreatedBy VARCHAR(255) NULL; ALTER TABLE "Namespace" ALTER COLUMN DataChange_CreatedTime TIMESTAMP NULL; ALTER TABLE "Item" ALTER COLUMN DataChange_CreatedBy VARCHAR(255) NULL; ALTER TABLE "Item" ALTER COLUMN DataChange_CreatedTime TIMESTAMP NULL; ALTER TABLE "Release" ALTER COLUMN DataChange_CreatedBy VARCHAR(255) NULL; ALTER TABLE "Release" ALTER COLUMN DataChange_CreatedTime TIMESTAMP NULL; ALTER TABLE "ServerConfig" ALTER COLUMN DataChange_CreatedBy VARCHAR(255) NULL; ALTER TABLE "ServerConfig" ALTER COLUMN DataChange_CreatedTime TIMESTAMP NULL; ALTER TABLE "ServerConfig" ALTER COLUMN Comment VARCHAR(255) NULL; ALTER TABLE "Audit" ALTER COLUMN DataChange_CreatedBy VARCHAR(255) NULL; ALTER TABLE "Audit" ALTER COLUMN DataChange_CreatedTime TIMESTAMP NULL; ALTER TABLE "GrayReleaseRule" ALTER COLUMN DataChange_CreatedBy VARCHAR(255) NULL; ALTER TABLE "GrayReleaseRule" ALTER COLUMN DataChange_CreatedTime TIMESTAMP NULL; ALTER TABLE "Release" ALTER COLUMN DataChange_CreatedBy VARCHAR(255) NULL; ALTER TABLE "Release" ALTER COLUMN DataChange_CreatedTime TIMESTAMP NULL; ALTER TABLE "Item" ALTER COLUMN DataChange_CreatedBy VARCHAR(255) NULL; ALTER TABLE "Item" ALTER COLUMN DataChange_CreatedTime TIMESTAMP NULL; ALTER TABLE "Namespace" ALTER COLUMN DataChange_CreatedBy VARCHAR(255) NULL; ALTER TABLE "Namespace" ALTER COLUMN DataChange_CreatedTime TIMESTAMP NULL; ALTER TABLE "AppNamespace" ALTER COLUMN DataChange_CreatedBy VARCHAR(255) NULL; ALTER TABLE "AppNamespace" ALTER COLUMN DataChange_CreatedTime TIMESTAMP NULL; ALTER TABLE "AppNamespace" ALTER COLUMN Format VARCHAR(255) NULL; CREATE ALIAS IF NOT EXISTS UNIX_TIMESTAMP FOR "com.ctrip.framework.apollo.common.jpa.H2Function.unixTimestamp"; ================================================ FILE: apollo-configservice/src/test/resources/integration-test/cleanup.sql ================================================ -- -- Copyright 2024 Apollo Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. -- You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. -- DELETE FROM "Release"; DELETE FROM "Namespace"; DELETE FROM "AppNamespace"; DELETE FROM "Cluster"; DELETE FROM "App"; DELETE FROM "ReleaseMessage"; DELETE FROM "GrayReleaseRule"; ================================================ FILE: apollo-configservice/src/test/resources/integration-test/test-gray-release.sql ================================================ -- -- Copyright 2024 Apollo Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. -- You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. -- INSERT INTO "GrayReleaseRule" (`Id`, `AppId`, `ClusterName`, `NamespaceName`, `BranchName`, `Rules`, `ReleaseId`, `BranchStatus`) VALUES (1, 'someAppId', 'default', 'application', 'gray-branch-1', '[{"clientAppId":"someAppId","clientIpList":["1.1.1.1"],"clientLabelList":["myLabel"]}]', 986, 1); INSERT INTO "GrayReleaseRule" (`Id`, `AppId`, `ClusterName`, `NamespaceName`, `BranchName`, `Rules`, `ReleaseId`, `BranchStatus`) VALUES (2, 'somePublicAppId', 'default', 'somePublicNamespace', 'gray-branch-2', '[{"clientAppId":"someAppId","clientIpList":["1.1.1.1"],"clientLabelList":["myLabel"]}]', 985, 1); ================================================ FILE: apollo-configservice/src/test/resources/integration-test/test-release-message.sql ================================================ -- -- Copyright 2024 Apollo Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. -- You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. -- INSERT INTO "ReleaseMessage" (`Id`, `Message`) VALUES (10, 'someAppId+default+application'); INSERT INTO "ReleaseMessage" (`Id`, `Message`) VALUES (20, 'somePublicAppId+default+somePublicNamespace'); ================================================ FILE: apollo-configservice/src/test/resources/integration-test/test-release-public-dc-override.sql ================================================ -- -- Copyright 2024 Apollo Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. -- You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. -- INSERT INTO "Release" (Id, ReleaseKey, Name, Comment, AppId, ClusterName, NamespaceName, Configurations) VALUES (995, 'TEST-RELEASE-KEY6', 'INTEGRATION-TEST-DEFAULT-OVERRIDE-PUBLIC-DC','First Release','someAppId', 'someDC', 'somePublicNamespace', '{"k1":"override-someDC-v1"}'); ================================================ FILE: apollo-configservice/src/test/resources/integration-test/test-release-public-default-override.sql ================================================ -- -- Copyright 2024 Apollo Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. -- You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. -- INSERT INTO "Release" (Id, ReleaseKey, Name, Comment, AppId, ClusterName, NamespaceName, Configurations) VALUES (994, 'TEST-RELEASE-KEY5', 'INTEGRATION-TEST-DEFAULT-OVERRIDE-PUBLIC','First Release','someAppId', 'default', 'somePublicNamespace', '{"k1":"override-v1"}'); ================================================ FILE: apollo-configservice/src/test/resources/integration-test/test-release.sql ================================================ -- -- Copyright 2024 Apollo Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. -- You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. -- INSERT INTO "App" (AppId, Name, OwnerName, OwnerEmail) VALUES ('someAppId','someAppName','someOwnerName','someOwnerName@ctrip.com'); INSERT INTO "App" (AppId, Name, OwnerName, OwnerEmail) VALUES ('somePublicAppId','somePublicAppName','someOwnerName','someOwnerName@ctrip.com'); INSERT INTO "Cluster" (AppId, Name) VALUES ('someAppId', 'default'); INSERT INTO "Cluster" (AppId, Name) VALUES ('someAppId', 'someCluster'); INSERT INTO "Cluster" (AppId, Name) VALUES ('somePublicAppId', 'default'); INSERT INTO "Cluster" (AppId, Name) VALUES ('somePublicAppId', 'someDC'); INSERT INTO "AppNamespace" (AppId, Name, IsPublic) VALUES ('someAppId', 'application', false); INSERT INTO "AppNamespace" (AppId, Name, IsPublic) VALUES ('someAppId', 'someNamespace', true); INSERT INTO "AppNamespace" (AppId, Name, IsPublic) VALUES ('someAppId', 'someNamespace.xml', false); INSERT INTO "AppNamespace" (AppId, Name, IsPublic) VALUES ('someAppId', 'anotherNamespace', false); INSERT INTO "AppNamespace" (AppId, Name, IsPublic) VALUES ('somePublicAppId', 'application', false); INSERT INTO "AppNamespace" (AppId, Name, IsPublic) VALUES ('somePublicAppId', 'somePublicNamespace', true); INSERT INTO "AppNamespace" (AppId, Name, IsPublic) VALUES ('somePublicAppId', 'anotherNamespace', true); INSERT INTO "Namespace" (AppId, ClusterName, NamespaceName) VALUES ('someAppId', 'default', 'application'); INSERT INTO "Namespace" (AppId, ClusterName, NamespaceName) VALUES ('someAppId', 'default', 'someNamespace.xml'); INSERT INTO "Namespace" (AppId, ClusterName, NamespaceName) VALUES ('someAppId', 'default', 'anotherNamespace'); INSERT INTO "Namespace" (AppId, ClusterName, NamespaceName) VALUES ('someAppId', 'someCluster', 'someNamespace'); INSERT INTO "Namespace" (AppId, ClusterName, NamespaceName) VALUES ('somePublicAppId', 'default', 'application'); INSERT INTO "Namespace" (AppId, ClusterName, NamespaceName) VALUES ('somePublicAppId', 'someDC', 'somePublicNamespace'); INSERT INTO "Namespace" (AppId, ClusterName, NamespaceName) VALUES ('someAppId', 'default', 'somePublicNamespace'); INSERT INTO "Namespace" (AppId, ClusterName, NamespaceName) VALUES ('somePublicAppId', 'default', 'anotherNamespace'); INSERT INTO "Release" (Id, ReleaseKey, Name, Comment, AppId, ClusterName, NamespaceName, Configurations) VALUES (990, 'TEST-RELEASE-KEY1', 'INTEGRATION-TEST-DEFAULT','First Release','someAppId', 'default', 'application', '{"k1":"v1"}'); INSERT INTO "Release" (Id, ReleaseKey, Name, Comment, AppId, ClusterName, NamespaceName, Configurations) VALUES (991, 'TEST-RELEASE-KEY2', 'INTEGRATION-TEST-NAMESPACE','First Release','someAppId', 'someCluster', 'someNamespace', '{"k2":"v2"}'); INSERT INTO "Release" (Id, ReleaseKey, Name, Comment, AppId, ClusterName, NamespaceName, Configurations) VALUES (992, 'TEST-RELEASE-KEY3', 'INTEGRATION-TEST-PUBLIC-DEFAULT','First Release','somePublicAppId', 'default', 'somePublicNamespace', '{"k1":"default-v1", "k2":"default-v2"}'); INSERT INTO "Release" (Id, ReleaseKey, Name, Comment, AppId, ClusterName, NamespaceName, Configurations) VALUES (993, 'TEST-RELEASE-KEY4', 'INTEGRATION-TEST-PUBLIC-NAMESPACE','First Release','somePublicAppId', 'someDC', 'somePublicNamespace', '{"k1":"someDC-v1", "k2":"someDC-v2"}'); INSERT INTO "Release" (Id, ReleaseKey, Name, Comment, AppId, ClusterName, NamespaceName, Configurations) VALUES (989, 'TEST-RELEASE-KEY5', 'INTEGRATION-TEST-PRIVATE-CONFIG-FILE','First Release','someAppId', 'default', 'someNamespace.xml', '{"k1":"v1-file", "k2":"v2-file"}'); INSERT INTO "Release" (Id, ReleaseKey, Name, Comment, AppId, ClusterName, NamespaceName, Configurations) VALUES (988, 'TEST-RELEASE-KEY6', 'INTEGRATION-TEST-PRIVATE-CONFIG-FILE','First Release','someAppId', 'default', 'anotherNamespace', '{"k1":"v1-file"}'); INSERT INTO "Release" (Id, ReleaseKey, Name, Comment, AppId, ClusterName, NamespaceName, Configurations) VALUES (987, 'TEST-RELEASE-KEY7', 'INTEGRATION-TEST-PUBLIC-CONFIG-FILE','First Release','somePublicAppId', 'default', 'anotherNamespace', '{"k2":"v2-file"}'); INSERT INTO "Release" (Id, ReleaseKey, Name, Comment, AppId, ClusterName, NamespaceName, Configurations) VALUES (986, 'TEST-GRAY-RELEASE-KEY1', 'INTEGRATION-TEST-DEFAULT','Gray Release','someAppId', 'gray-branch-1', 'application', '{"k1":"v1-gray"}'); INSERT INTO "Release" (Id, ReleaseKey, Name, Comment, AppId, ClusterName, NamespaceName, Configurations) VALUES (985, 'TEST-GRAY-RELEASE-KEY2', 'INTEGRATION-TEST-NAMESPACE','Gray Release','somePublicAppId', 'gray-branch-2', 'somePublicNamespace', '{"k1":"gray-v1", "k2":"gray-v2"}'); ================================================ FILE: apollo-configservice/src/test/resources/logback-test.xml ================================================ utf-8 [%p] %c - %m%n ================================================ FILE: apollo-portal/pom.xml ================================================ com.ctrip.framework.apollo apollo ${revision} ../pom.xml 4.0.0 apollo-portal Apollo Portal https://raw.githubusercontent.com/apolloconfig/apollo-openapi/refs/tags/v0.1.0/apollo-openapi.yaml ${project.artifactId} yyyyMMddHHmmss org.springframework.security spring-security-ldap com.ctrip.framework.apollo apollo-common com.ctrip.framework.apollo apollo-openapi org.openapitools jackson-databind-nullable io.swagger.core.v3 swagger-annotations io.swagger.core.v3 swagger-models com.ctrip.framework.apollo apollo-audit-spring-boot-starter org.springframework.boot spring-boot-starter-oauth2-client org.springframework.boot spring-boot-starter-oauth2-resource-server org.springframework.session spring-session-core org.springframework.session spring-session-data-redis org.springframework.session spring-session-jdbc runtime org.springframework.boot spring-boot-configuration-processor true org.yaml snakeyaml jakarta.xml.bind jakarta.xml.bind-api org.glassfish.jaxb jaxb-runtime jakarta.activation jakarta.activation-api com.sun.mail jakarta.mail org.javassist javassist org.apache.httpcomponents.client5 httpclient5 org.springframework.security spring-security-test test org.eclipse.jetty jetty-server test org.openapitools openapi-generator-maven-plugin 6.6.0 generate-openapi-sources generate-sources generate ${apollo.openapi.spec.url} spring ${project.build.directory}/generated-sources/openapi com.ctrip.framework.apollo.openapi.api com.ctrip.framework.apollo.openapi.model com.ctrip.framework.apollo.openapi.invoker true true java8 true true org.springframework.boot spring-boot-maven-plugin maven-assembly-plugin package single ${project.artifactId}-${project.version}-${package.environment} false src/assembly/assembly-descriptor.xml com.spotify docker-maven-plugin 1.2.2 apolloconfig/${project.artifactId} ${project.version} latest ${project.basedir}/src/main/docker docker-hub ${project.version} / ${project.build.directory} *.zip com.google.code.maven-replacer-plugin replacer 1.5.3 prepare-package replace ${project.build.directory} classes/static/*.html classes/static/**/*.html \.css\" .css?v=${maven.build.timestamp}\" \.js\" .js?v=${maven.build.timestamp}\" org.openapitools openapi-generator-maven-plugin org.codehaus.mojo build-helper-maven-plugin 3.4.0 add-openapi-sources generate-sources add-source ${project.build.directory}/generated-sources/openapi/src/main/java ================================================ FILE: apollo-portal/src/assembly/assembly-descriptor.xml ================================================ apollo-assembly zip false src/main/scripts scripts *.sh 0755 unix target/classes / apollo-portal.conf unix target/classes /config application-github.properties apollo-env.properties application.properties target / ${project.artifactId}-*.jar 0755 ================================================ FILE: apollo-portal/src/main/docker/Dockerfile ================================================ # # Copyright 2024 Apollo Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # Dockerfile for apollo-portal # 1. ./scripts/build.sh # 2. Build with: mvn docker:build -pl apollo-portal # 3. Run with: docker run -p 8070:8070 -e SPRING_DATASOURCE_URL="jdbc:mysql://fill-in-the-correct-server:3306/ApolloPortalDB?characterEncoding=utf8" -e SPRING_DATASOURCE_USERNAME=FillInCorrectUser -e SPRING_DATASOURCE_PASSWORD=FillInCorrectPassword -e APOLLO_PORTAL_ENVS=dev,pro -e DEV_META=http://fill-in-dev-meta-server:8080 -e PRO_META=http://fill-in-pro-meta-server:8080 -d -v /tmp/logs:/opt/logs --name apollo-portal apolloconfig/apollo-portal FROM alpine:3.15.5 ARG VERSION ENV VERSION $VERSION COPY apollo-portal-${VERSION}-github.zip /apollo-portal/apollo-portal-${VERSION}-github.zip RUN unzip /apollo-portal/apollo-portal-${VERSION}-github.zip -d /apollo-portal \ && rm -rf /apollo-portal/apollo-portal-${VERSION}-github.zip \ && chmod +x /apollo-portal/scripts/startup.sh FROM eclipse-temurin:17-jre-jammy LABEL maintainer="g632104866@gmail.com;finchcn@gmail.com;ameizi" ENV APOLLO_RUN_MODE "Docker" ENV SERVER_PORT 8070 RUN DEBIAN_FRONTEND=noninteractive apt-get update \ && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends procps curl bash tzdata \ && rm -rf /var/lib/apt/lists/* \ && ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ && echo "Asia/Shanghai" > /etc/timezone COPY --from=0 /apollo-portal /apollo-portal EXPOSE $SERVER_PORT CMD ["/apollo-portal/scripts/startup.sh"] ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/PortalOpenApiConfig.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.openapi; import com.ctrip.framework.apollo.common.controller.WebMvcConfig; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.stereotype.Component; @EnableAutoConfiguration @Configuration @ComponentScan(basePackageClasses = PortalOpenApiConfig.class) public class PortalOpenApiConfig { @Component static class PortalWebMvcConfig extends WebMvcConfig { @Override public void customize(TomcatServletWebServerFactory factory) { final String relaxedChars = "<>[\\]^`{|}"; final String tomcatRelaxedPathCharsProperty = "relaxedPathChars"; final String tomcatRelaxedQueryCharsProperty = "relaxedQueryChars"; factory.addConnectorCustomizers(connector -> { connector.setProperty(tomcatRelaxedPathCharsProperty, relaxedChars); connector.setProperty(tomcatRelaxedQueryCharsProperty, relaxedChars); }); } } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/auth/ConsumerPermissionValidator.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.openapi.auth; import static com.ctrip.framework.apollo.portal.service.SystemRoleManagerService.SYSTEM_PERMISSION_TARGET_ID; import com.ctrip.framework.apollo.common.entity.AppNamespace; import com.ctrip.framework.apollo.openapi.service.ConsumerRolePermissionService; import com.ctrip.framework.apollo.openapi.util.ConsumerAuthUtil; import com.ctrip.framework.apollo.portal.component.AbstractPermissionValidator; import com.ctrip.framework.apollo.portal.component.PermissionValidator; import com.ctrip.framework.apollo.portal.constant.PermissionType; import com.ctrip.framework.apollo.portal.entity.po.Permission; import org.springframework.stereotype.Component; import java.util.List; @Component("consumerPermissionValidator") public class ConsumerPermissionValidator extends AbstractPermissionValidator implements PermissionValidator { private final ConsumerRolePermissionService permissionService; private final ConsumerAuthUtil consumerAuthUtil; public ConsumerPermissionValidator(final ConsumerRolePermissionService permissionService, final ConsumerAuthUtil consumerAuthUtil) { this.permissionService = permissionService; this.consumerAuthUtil = consumerAuthUtil; } @Override public boolean hasModifyNamespacePermission(String appId, String env, String clusterName, String namespaceName) { if (hasCreateNamespacePermission(appId)) { return true; } return super.hasModifyNamespacePermission(appId, env, clusterName, namespaceName); } @Override public boolean hasReleaseNamespacePermission(String appId, String env, String clusterName, String namespaceName) { if (hasCreateNamespacePermission(appId)) { return true; } return super.hasReleaseNamespacePermission(appId, env, clusterName, namespaceName); } @Override public boolean hasCreateAppNamespacePermission(String appId, AppNamespace appNamespace) { throw new UnsupportedOperationException("Not supported operation"); } @Override public boolean isSuperAdmin() { // openapi shouldn't be return false; } @Override public boolean shouldHideConfigToCurrentUser(String appId, String env, String clusterName, String namespaceName) { throw new UnsupportedOperationException("Not supported operation"); } @Override public boolean hasCreateApplicationPermission() { long consumerId = consumerAuthUtil.retrieveConsumerIdFromCtx(); return permissionService.consumerHasPermission(consumerId, PermissionType.CREATE_APPLICATION, SYSTEM_PERMISSION_TARGET_ID); } @Override public boolean hasCreateApplicationPermission(String userId) { return false; } @Override public boolean hasManageAppMasterPermission(String appId) { throw new UnsupportedOperationException("Not supported operation"); } @Override protected boolean hasPermissions(List requiredPerms) { if (requiredPerms == null || requiredPerms.isEmpty()) { return false; } long consumerId = consumerAuthUtil.retrieveConsumerIdFromCtx(); return permissionService.hasAnyPermission(consumerId, requiredPerms); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/entity/Consumer.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.openapi.entity; import com.ctrip.framework.apollo.common.entity.BaseEntity; import org.hibernate.annotations.SQLDelete; import org.hibernate.annotations.Where; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.Table; @Entity @Table(name = "`Consumer`") @SQLDelete( sql = "Update `Consumer` set IsDeleted = true, DeletedAt = ROUND(UNIX_TIMESTAMP(NOW(4))*1000) where Id = ?") @Where(clause = "`IsDeleted` = false") public class Consumer extends BaseEntity { @Column(name = "`Name`", nullable = false) private String name; @Column(name = "`AppId`", nullable = false) private String appId; @Column(name = "`OrgId`", nullable = false) private String orgId; @Column(name = "`OrgName`", nullable = false) private String orgName; @Column(name = "`OwnerName`", nullable = false) private String ownerName; @Column(name = "`OwnerEmail`", nullable = false) private String ownerEmail; public String getAppId() { return appId; } public String getName() { return name; } public String getOrgId() { return orgId; } public String getOrgName() { return orgName; } public String getOwnerEmail() { return ownerEmail; } public String getOwnerName() { return ownerName; } public void setAppId(String appId) { this.appId = appId; } public void setName(String name) { this.name = name; } public void setOrgId(String orgId) { this.orgId = orgId; } public void setOrgName(String orgName) { this.orgName = orgName; } public void setOwnerEmail(String ownerEmail) { this.ownerEmail = ownerEmail; } public void setOwnerName(String ownerName) { this.ownerName = ownerName; } @Override public String toString() { return toStringHelper().add("name", name).add("appId", appId).add("orgId", orgId) .add("orgName", orgName).add("ownerName", ownerName).add("ownerEmail", ownerEmail) .toString(); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/entity/ConsumerAudit.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.openapi.entity; import com.google.common.base.MoreObjects; import java.util.Date; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.PrePersist; import jakarta.persistence.Table; /** * @author Jason Song(song_s@ctrip.com) */ @Entity @Table(name = "`ConsumerAudit`") public class ConsumerAudit { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "`Id`") private long id; @Column(name = "`ConsumerId`", nullable = false) private long consumerId; @Column(name = "`Uri`", nullable = false) private String uri; @Column(name = "`Method`", nullable = false) private String method; @Column(name = "`DataChange_CreatedTime`") private Date dataChangeCreatedTime; @Column(name = "`DataChange_LastTime`") private Date dataChangeLastModifiedTime; @PrePersist protected void prePersist() { if (this.dataChangeCreatedTime == null) { this.dataChangeCreatedTime = new Date(); } if (this.dataChangeLastModifiedTime == null) { dataChangeLastModifiedTime = this.dataChangeCreatedTime; } } public long getId() { return id; } public void setId(long id) { this.id = id; } public long getConsumerId() { return consumerId; } public void setConsumerId(long consumerId) { this.consumerId = consumerId; } public String getUri() { return uri; } public void setUri(String uri) { this.uri = uri; } public String getMethod() { return method; } public void setMethod(String method) { this.method = method; } public Date getDataChangeCreatedTime() { return dataChangeCreatedTime; } public void setDataChangeCreatedTime(Date dataChangeCreatedTime) { this.dataChangeCreatedTime = dataChangeCreatedTime; } public Date getDataChangeLastModifiedTime() { return dataChangeLastModifiedTime; } public void setDataChangeLastModifiedTime(Date dataChangeLastModifiedTime) { this.dataChangeLastModifiedTime = dataChangeLastModifiedTime; } @Override public String toString() { return MoreObjects.toStringHelper(this).omitNullValues().add("id", id) .add("consumerId", consumerId).add("uri", uri).add("method", method) .add("dataChangeCreatedTime", dataChangeCreatedTime) .add("dataChangeLastModifiedTime", dataChangeLastModifiedTime).toString(); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/entity/ConsumerRole.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.openapi.entity; import com.ctrip.framework.apollo.common.entity.BaseEntity; import org.hibernate.annotations.SQLDelete; import org.hibernate.annotations.Where; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.Table; /** * @author Jason Song(song_s@ctrip.com) */ @Entity @Table(name = "`ConsumerRole`") @SQLDelete( sql = "Update `ConsumerRole` set IsDeleted = true, DeletedAt = ROUND(UNIX_TIMESTAMP(NOW(4))*1000) where Id = ?") @Where(clause = "`IsDeleted` = false") public class ConsumerRole extends BaseEntity { @Column(name = "`ConsumerId`", nullable = false) private long consumerId; @Column(name = "`RoleId`", nullable = false) private long roleId; public long getConsumerId() { return consumerId; } public void setConsumerId(long consumerId) { this.consumerId = consumerId; } public long getRoleId() { return roleId; } public void setRoleId(long roleId) { this.roleId = roleId; } @Override public String toString() { return toStringHelper().add("consumerId", consumerId).add("roleId", roleId).toString(); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/entity/ConsumerToken.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.openapi.entity; import com.ctrip.framework.apollo.common.entity.BaseEntity; import jakarta.validation.constraints.PositiveOrZero; import org.hibernate.annotations.SQLDelete; import org.hibernate.annotations.Where; import java.util.Date; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.Table; /** * @author Jason Song(song_s@ctrip.com) */ @Entity @Table(name = "`ConsumerToken`") @SQLDelete( sql = "Update `ConsumerToken` set IsDeleted = true, DeletedAt = ROUND(UNIX_TIMESTAMP(NOW(4))*1000) where Id = ?") @Where(clause = "`IsDeleted` = false") public class ConsumerToken extends BaseEntity { @Column(name = "`ConsumerId`", nullable = false) private long consumerId; @Column(name = "`Token`", nullable = false) private String token; @PositiveOrZero @Column(name = "`RateLimit`", nullable = false) private Integer rateLimit; @Column(name = "`Expires`", nullable = false) private Date expires; public long getConsumerId() { return consumerId; } public void setConsumerId(long consumerId) { this.consumerId = consumerId; } public String getToken() { return token; } public void setToken(String token) { this.token = token; } public Integer getRateLimit() { return rateLimit; } public void setRateLimit(Integer rateLimit) { this.rateLimit = rateLimit; } public Date getExpires() { return expires; } public void setExpires(Date expires) { this.expires = expires; } @Override public String toString() { return toStringHelper().add("consumerId", consumerId).add("token", token) .add("rateLimit", rateLimit).add("expires", expires).toString(); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/filter/ConsumerAuthenticationFilter.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.openapi.filter; import com.ctrip.framework.apollo.openapi.entity.ConsumerToken; import com.ctrip.framework.apollo.openapi.util.ConsumerAuditUtil; import com.ctrip.framework.apollo.openapi.util.ConsumerAuthUtil; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import com.google.common.util.concurrent.RateLimiter; import java.io.IOException; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import jakarta.servlet.Filter; import jakarta.servlet.FilterChain; import jakarta.servlet.FilterConfig; import jakarta.servlet.ServletException; import jakarta.servlet.ServletRequest; import jakarta.servlet.ServletResponse; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.apache.commons.lang3.tuple.ImmutablePair; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpHeaders; /** * @author Jason Song(song_s@ctrip.com) */ public class ConsumerAuthenticationFilter implements Filter { private static final Logger logger = LoggerFactory.getLogger(ConsumerAuthenticationFilter.class); private final ConsumerAuthUtil consumerAuthUtil; private final ConsumerAuditUtil consumerAuditUtil; private static final int WARMUP_MILLIS = 1000; // ms private static final int RATE_LIMITER_CACHE_MAX_SIZE = 20000; private static final int TOO_MANY_REQUESTS = 429; private static final Cache> LIMITER = CacheBuilder.newBuilder().expireAfterAccess(1, TimeUnit.HOURS) .maximumSize(RATE_LIMITER_CACHE_MAX_SIZE).build(); private static final String PORTAL_USER_AUTHENTICATED = "PORTAL_USER_AUTHENTICATED"; public ConsumerAuthenticationFilter(ConsumerAuthUtil consumerAuthUtil, ConsumerAuditUtil consumerAuditUtil) { this.consumerAuthUtil = consumerAuthUtil; this.consumerAuditUtil = consumerAuditUtil; } @Override public void init(FilterConfig filterConfig) throws ServletException { // nothing } @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; if (Boolean.TRUE.equals(request.getAttribute(PORTAL_USER_AUTHENTICATED))) { chain.doFilter(req, resp); return; } String token = request.getHeader(HttpHeaders.AUTHORIZATION); ConsumerToken consumerToken = consumerAuthUtil.getConsumerToken(token); if (null == consumerToken) { response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized"); return; } Integer rateLimit = consumerToken.getRateLimit(); if (null != rateLimit && rateLimit > 0) { try { ImmutablePair rateLimiterPair = getOrCreateRateLimiterPair(consumerToken.getToken(), rateLimit); long warmupToMillis = rateLimiterPair.getLeft() + WARMUP_MILLIS; if (System.currentTimeMillis() > warmupToMillis && !rateLimiterPair.getRight().tryAcquire()) { response.sendError(TOO_MANY_REQUESTS, "Too Many Requests, the flow is limited"); return; } } catch (Exception e) { logger.error("ConsumerAuthenticationFilter ratelimit error", e); response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Rate limiting failed"); return; } } long consumerId = consumerToken.getConsumerId(); consumerAuthUtil.storeConsumerId(request, consumerId); consumerAuditUtil.audit(request, consumerId); chain.doFilter(req, resp); } @Override public void destroy() { // nothing } private ImmutablePair getOrCreateRateLimiterPair(String key, Integer limitCount) { try { return LIMITER.get(key, () -> ImmutablePair.of(System.currentTimeMillis(), RateLimiter.create(limitCount))); } catch (ExecutionException e) { throw new RuntimeException("Failed to create rate limiter", e); } } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/repository/ConsumerAuditRepository.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.openapi.repository; import com.ctrip.framework.apollo.openapi.entity.ConsumerAudit; import org.springframework.data.jpa.repository.JpaRepository; /** * @author Jason Song(song_s@ctrip.com) */ public interface ConsumerAuditRepository extends JpaRepository { } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/repository/ConsumerRepository.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.openapi.repository; import com.ctrip.framework.apollo.openapi.entity.Consumer; import org.springframework.data.jpa.repository.JpaRepository; /** * @author Jason Song(song_s@ctrip.com) */ public interface ConsumerRepository extends JpaRepository { Consumer findByAppId(String appId); } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/repository/ConsumerRoleRepository.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.openapi.repository; import com.ctrip.framework.apollo.openapi.entity.ConsumerRole; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.repository.query.Param; import java.util.List; /** * @author Jason Song(song_s@ctrip.com) */ public interface ConsumerRoleRepository extends JpaRepository { /** * find consumer roles by userId * * @param consumerId consumer id */ List findByConsumerId(long consumerId); /** * find consumer roles by roleId */ List findByRoleId(long roleId); ConsumerRole findByConsumerIdAndRoleId(long consumerId, long roleId); @Modifying @Query("UPDATE ConsumerRole SET isDeleted = true, " + "deletedAt = :#{T(java.lang.System).currentTimeMillis()}, " + "dataChangeLastModifiedBy = :operator WHERE roleId in :roleIds and isDeleted = false") Integer batchDeleteByRoleIds(@Param("roleIds") List roleIds, @Param("operator") String operator); } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/repository/ConsumerTokenRepository.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.openapi.repository; import com.ctrip.framework.apollo.openapi.entity.ConsumerToken; import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; import java.util.Date; /** * @author Jason Song(song_s@ctrip.com) */ public interface ConsumerTokenRepository extends JpaRepository { /** * find consumer token by token * * @param token the token * @param validDate the date when the token is valid */ ConsumerToken findTopByTokenAndExpiresAfter(String token, Date validDate); ConsumerToken findByConsumerId(Long consumerId); List findByConsumerIdIn(List consumerIds); } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/server/service/AppOpenApiService.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.openapi.server.service; import com.ctrip.framework.apollo.openapi.model.MultiResponseEntity; import com.ctrip.framework.apollo.openapi.model.OpenAppDTO; import com.ctrip.framework.apollo.openapi.model.OpenCreateAppDTO; import com.ctrip.framework.apollo.openapi.model.OpenEnvClusterDTO; import org.springframework.lang.NonNull; import java.util.List; import java.util.Set; public interface AppOpenApiService { void createApp(@NonNull OpenCreateAppDTO req); List getEnvClusterInfo(String appId); List getAllApps(); List getAppsInfo(List appIds); List getAuthorizedApps(); void updateApp(OpenAppDTO openAppDTO); List getAppsBySelf(Set appIds, Integer page, Integer size); void createAppInEnv(String env, OpenAppDTO app, String operator); OpenAppDTO deleteApp(String appId); MultiResponseEntity findMissEnvs(String appId); MultiResponseEntity getAppNavTree(String appId); } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/server/service/ClusterOpenApiService.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.openapi.server.service; import com.ctrip.framework.apollo.openapi.model.OpenClusterDTO; public interface ClusterOpenApiService { OpenClusterDTO getCluster(String appId, String env, String clusterName); OpenClusterDTO createCluster(String env, OpenClusterDTO openClusterDTO); void deleteCluster(String env, String appId, String clusterName); } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/server/service/EnvOpenApiService.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.openapi.server.service; import java.util.List; public interface EnvOpenApiService { List getEnvs(); } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/server/service/OrganizationOpenApiService.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.openapi.server.service; import com.ctrip.framework.apollo.openapi.model.OpenOrganizationDto; import java.util.List; public interface OrganizationOpenApiService { List getOrganizations(); } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/server/service/ServerAppOpenApiService.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.openapi.server.service; import com.ctrip.framework.apollo.common.dto.ClusterDTO; import com.ctrip.framework.apollo.common.entity.App; import com.ctrip.framework.apollo.common.exception.BadRequestException; import com.ctrip.framework.apollo.common.utils.BeanUtils; import com.ctrip.framework.apollo.core.ConfigConsts; import com.ctrip.framework.apollo.openapi.model.OpenAppDTO; import com.ctrip.framework.apollo.openapi.model.OpenCreateAppDTO; import com.ctrip.framework.apollo.openapi.model.OpenEnvClusterDTO; import com.ctrip.framework.apollo.openapi.model.MultiResponseEntity; import com.ctrip.framework.apollo.openapi.model.OpenEnvClusterInfo; import com.ctrip.framework.apollo.openapi.model.RichResponseEntity; import com.ctrip.framework.apollo.openapi.util.OpenApiModelConverters; import com.ctrip.framework.apollo.portal.component.PortalSettings; import com.ctrip.framework.apollo.portal.entity.model.AppModel; import com.ctrip.framework.apollo.portal.environment.Env; import com.ctrip.framework.apollo.portal.listener.AppDeletionEvent; import com.ctrip.framework.apollo.portal.listener.AppInfoChangedEvent; import com.ctrip.framework.apollo.portal.service.AppService; import com.ctrip.framework.apollo.portal.service.ClusterService; import com.ctrip.framework.apollo.portal.service.RoleInitializationService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.ApplicationEventPublisher; import org.springframework.data.domain.Pageable; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; import org.springframework.web.client.HttpClientErrorException; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Set; /** * @author wxq */ @Service public class ServerAppOpenApiService implements AppOpenApiService { private final PortalSettings portalSettings; private final ClusterService clusterService; private final AppService appService; private final ApplicationEventPublisher publisher; private final RoleInitializationService roleInitializationService; private static final Logger logger = LoggerFactory.getLogger(ServerAppOpenApiService.class); public ServerAppOpenApiService(PortalSettings portalSettings, ClusterService clusterService, AppService appService, ApplicationEventPublisher publisher, RoleInitializationService roleInitializationService) { this.portalSettings = portalSettings; this.clusterService = clusterService; this.appService = appService; this.publisher = publisher; this.roleInitializationService = roleInitializationService; } private App convert(OpenAppDTO dto) { return App.builder().appId(dto.getAppId()).name(dto.getName()).ownerName(dto.getOwnerName()) .orgId(dto.getOrgId()).orgName(dto.getOrgName()).ownerEmail(dto.getOwnerEmail()).build(); } /** * @see com.ctrip.framework.apollo.portal.controller.AppController#create(AppModel) */ @Override public void createApp(OpenCreateAppDTO req) { App app = convert(req.getApp()); List admins = req.getAdmins(); if (admins == null) { admins = Collections.emptyList(); } appService.createAppAndAddRolePermission(app, new HashSet<>(admins)); } @Override public List getEnvClusterInfo(String appId) { List envClusters = new LinkedList<>(); List envs = portalSettings.getActiveEnvs(); for (Env env : envs) { OpenEnvClusterDTO envCluster = new OpenEnvClusterDTO(); envCluster.setEnv(env.getName()); List clusterDTOs = clusterService.findClusters(env, appId); Set clusterNames = clusterDTOs == null ? Collections.emptySet() : BeanUtils.toPropertySet("name", clusterDTOs); envCluster.setClusters(new ArrayList<>(clusterNames)); envClusters.add(envCluster); } return envClusters; } @Override public List getAllApps() { final List apps = this.appService.findAll(); return OpenApiModelConverters.fromApps(apps); } @Override public List getAppsInfo(List appIds) { if (appIds == null || appIds.isEmpty()) { return Collections.emptyList(); } final List apps = this.appService.findByAppIds(new HashSet<>(appIds)); return OpenApiModelConverters.fromApps(apps); } @Override public List getAuthorizedApps() { throw new UnsupportedOperationException(); } /** * Updating Application Information - Using OpenAPI DTOs * @param openAppDTO OpenAPI application DTO */ @Override public void updateApp(OpenAppDTO openAppDTO) { App app = convert(openAppDTO); App updatedApp = appService.updateAppInLocal(app); publisher.publishEvent(new AppInfoChangedEvent(updatedApp)); } /** * Get the current user's app list (paginated) * @param page Pagination parameter * @return App list */ @Override public List getAppsBySelf(Set appIds, Integer page, Integer size) { int pageIndex = page == null ? 0 : page; int pageSize = (size == null || size <= 0) ? 20 : size; Pageable pageable = Pageable.ofSize(pageSize).withPage(pageIndex); Set targetAppIds = appIds == null ? Collections.emptySet() : appIds; if (targetAppIds.isEmpty()) { return Collections.emptyList(); } List apps = appService.findByAppIds(targetAppIds, pageable); return OpenApiModelConverters.fromApps(apps); } /** * Create an application in a specified environment * @param env Environment * @param app Application information * @param operator Operator */ @Override public void createAppInEnv(String env, OpenAppDTO app, String operator) { if (env == null) { throw BadRequestException.invalidEnvFormat("null"); } Env envEnum; try { envEnum = Env.valueOf(env); } catch (IllegalArgumentException e) { throw BadRequestException.invalidEnvFormat(env); } App appEntity = convert(app); appService.createAppInRemote(envEnum, appEntity); roleInitializationService.initNamespaceSpecificEnvRoles(appEntity.getAppId(), ConfigConsts.NAMESPACE_APPLICATION, env, operator); } /** * Delete an application * @param appId application ID * @return the deleted application */ @Override public OpenAppDTO deleteApp(String appId) { App app = appService.deleteAppInLocal(appId); publisher.publishEvent(new AppDeletionEvent(app)); return OpenApiModelConverters.fromApp(app); } /** * Find missing environments * @param appId application ID * @return list of missing environments */ public MultiResponseEntity findMissEnvs(String appId) { List entities = new ArrayList<>(); MultiResponseEntity response = new MultiResponseEntity(HttpStatus.OK.value(), entities); for (Env env : portalSettings.getActiveEnvs()) { try { appService.load(env, appId); } catch (Exception e) { RichResponseEntity entity; if (e instanceof HttpClientErrorException && ((HttpClientErrorException) e).getStatusCode() == HttpStatus.NOT_FOUND) { entity = new RichResponseEntity(HttpStatus.OK.value(), HttpStatus.OK.getReasonPhrase()); entity.setBody(env.toString()); } else { entity = new RichResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR.value(), "load env:" + env.getName() + " cluster error." + e.getMessage()); } response.addEntitiesItem(entity); } } return response; } /** * Find AppNavTree * @param appId * @return list of EnvClusterInfos */ @Override public MultiResponseEntity getAppNavTree(String appId) { List entities = new ArrayList<>(); MultiResponseEntity response = new MultiResponseEntity(HttpStatus.OK.value(), entities); List envs = portalSettings.getActiveEnvs(); for (Env env : envs) { try { OpenEnvClusterInfo openEnvClusterInfo = OpenApiModelConverters.fromEnvClusterInfo(appService.createEnvNavNode(env, appId)); RichResponseEntity entity = new RichResponseEntity(HttpStatus.OK.value(), HttpStatus.OK.getReasonPhrase()); entity.setBody(openEnvClusterInfo); response.addEntitiesItem(entity); } catch (Exception e) { logger.warn("Failed to load env {} navigation for app {}", env, appId, e); RichResponseEntity entity = new RichResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR.value(), "load env:" + env.getName() + " cluster error." + e.getMessage()); response.addEntitiesItem(entity); } } return response; } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/server/service/ServerClusterOpenApiService.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.openapi.server.service; import com.ctrip.framework.apollo.common.dto.ClusterDTO; import com.ctrip.framework.apollo.openapi.model.OpenClusterDTO; import com.ctrip.framework.apollo.openapi.util.OpenApiModelConverters; import com.ctrip.framework.apollo.portal.environment.Env; import com.ctrip.framework.apollo.portal.service.ClusterService; import org.springframework.stereotype.Service; /** * @author wxq */ @Service public class ServerClusterOpenApiService implements ClusterOpenApiService { private final ClusterService clusterService; public ServerClusterOpenApiService(ClusterService clusterService) { this.clusterService = clusterService; } @Override public OpenClusterDTO getCluster(String appId, String env, String clusterName) { ClusterDTO clusterDTO = clusterService.loadCluster(appId, Env.valueOf(env), clusterName); return clusterDTO == null ? null : OpenApiModelConverters.fromClusterDTO(clusterDTO); } @Override public OpenClusterDTO createCluster(String env, OpenClusterDTO openClusterDTO) { ClusterDTO toCreate = OpenApiModelConverters.toClusterDTO(openClusterDTO); ClusterDTO createdClusterDTO = clusterService.createCluster(Env.valueOf(env), toCreate); return OpenApiModelConverters.fromClusterDTO(createdClusterDTO); } @Override public void deleteCluster(String env, String appId, String clusterName) { clusterService.deleteCluster(Env.valueOf(env), appId, clusterName); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/server/service/ServerEnvOpenApiService.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.openapi.server.service; import com.ctrip.framework.apollo.portal.component.PortalSettings; import com.ctrip.framework.apollo.portal.environment.Env; import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.List; @Service public class ServerEnvOpenApiService implements EnvOpenApiService { private final PortalSettings portalSettings; public ServerEnvOpenApiService(PortalSettings portalSettings) { this.portalSettings = portalSettings; } @Override public List getEnvs() { List environments = new ArrayList<>(); for (Env env : portalSettings.getActiveEnvs()) { environments.add(env.toString()); } return environments; } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/server/service/ServerInstanceOpenApiService.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.openapi.server.service; import com.ctrip.framework.apollo.openapi.api.InstanceOpenApiService; import com.ctrip.framework.apollo.portal.environment.Env; import com.ctrip.framework.apollo.portal.service.InstanceService; import org.springframework.stereotype.Service; @Service public class ServerInstanceOpenApiService implements InstanceOpenApiService { private final InstanceService instanceService; public ServerInstanceOpenApiService(InstanceService instanceService) { this.instanceService = instanceService; } @Override public int getInstanceCountByNamespace(String appId, String env, String clusterName, String namespaceName) { return instanceService.getInstanceCountByNamespace(appId, Env.valueOf(env), clusterName, namespaceName); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/server/service/ServerItemOpenApiService.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.openapi.server.service; import com.ctrip.framework.apollo.common.dto.ItemDTO; import com.ctrip.framework.apollo.common.dto.PageDTO; import com.ctrip.framework.apollo.openapi.api.ItemOpenApiService; import com.ctrip.framework.apollo.openapi.dto.OpenItemDTO; import com.ctrip.framework.apollo.openapi.dto.OpenPageDTO; import com.ctrip.framework.apollo.openapi.util.OpenApiBeanUtils; import com.ctrip.framework.apollo.portal.environment.Env; import com.ctrip.framework.apollo.portal.service.ItemService; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; import org.springframework.web.client.HttpStatusCodeException; /** * @author wxq */ @Service public class ServerItemOpenApiService implements ItemOpenApiService { private final ItemService itemService; public ServerItemOpenApiService(ItemService itemService) { this.itemService = itemService; } @Override public OpenItemDTO getItem(String appId, String env, String clusterName, String namespaceName, String key) { ItemDTO itemDTO = itemService.loadItem(Env.valueOf(env), appId, clusterName, namespaceName, key); return itemDTO == null ? null : OpenApiBeanUtils.transformFromItemDTO(itemDTO); } @Override public OpenItemDTO createItem(String appId, String env, String clusterName, String namespaceName, OpenItemDTO itemDTO) { ItemDTO toCreate = OpenApiBeanUtils.transformToItemDTO(itemDTO); // protect toCreate.setLineNum(0); toCreate.setId(0); toCreate.setDataChangeLastModifiedBy(toCreate.getDataChangeCreatedBy()); toCreate.setDataChangeLastModifiedTime(null); toCreate.setDataChangeCreatedTime(null); ItemDTO createdItem = itemService.createItem(appId, Env.valueOf(env), clusterName, namespaceName, toCreate); return OpenApiBeanUtils.transformFromItemDTO(createdItem); } @Override public void updateItem(String appId, String env, String clusterName, String namespaceName, OpenItemDTO itemDTO) { ItemDTO toUpdateItem = itemService.loadItem(Env.valueOf(env), appId, clusterName, namespaceName, itemDTO.getKey()); // protect. only value,type,comment,lastModifiedBy can be modified toUpdateItem.setComment(itemDTO.getComment()); toUpdateItem.setType(itemDTO.getType()); toUpdateItem.setValue(itemDTO.getValue()); toUpdateItem.setDataChangeLastModifiedBy(itemDTO.getDataChangeLastModifiedBy()); itemService.updateItem(appId, Env.valueOf(env), clusterName, namespaceName, toUpdateItem); } @Override public void createOrUpdateItem(String appId, String env, String clusterName, String namespaceName, OpenItemDTO itemDTO) { try { this.updateItem(appId, env, clusterName, namespaceName, itemDTO); } catch (Throwable ex) { if (ex instanceof HttpStatusCodeException) { // check createIfNotExists if (((HttpStatusCodeException) ex).getStatusCode().equals(HttpStatus.NOT_FOUND)) { this.createItem(appId, env, clusterName, namespaceName, itemDTO); return; } } throw ex; } } @Override public void removeItem(String appId, String env, String clusterName, String namespaceName, String key, String operator) { ItemDTO toDeleteItem = this.itemService.loadItem(Env.valueOf(env), appId, clusterName, namespaceName, key); this.itemService.deleteItem(Env.valueOf(env), toDeleteItem.getId(), operator); } @Override public OpenPageDTO findItemsByNamespace(String appId, String env, String clusterName, String namespaceName, int page, int size) { PageDTO commonOpenItemDTOPage = this.itemService.findItemsByNamespace(appId, Env.valueOf(env), clusterName, namespaceName, page, size); return new OpenPageDTO<>(commonOpenItemDTOPage.getPage(), commonOpenItemDTOPage.getSize(), commonOpenItemDTOPage.getTotal(), commonOpenItemDTOPage.getContent()); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/server/service/ServerNamespaceOpenApiService.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.openapi.server.service; import com.ctrip.framework.apollo.common.dto.NamespaceDTO; import com.ctrip.framework.apollo.common.dto.NamespaceLockDTO; import com.ctrip.framework.apollo.common.entity.AppNamespace; import com.ctrip.framework.apollo.openapi.api.NamespaceOpenApiService; import com.ctrip.framework.apollo.openapi.dto.OpenAppNamespaceDTO; import com.ctrip.framework.apollo.openapi.dto.OpenNamespaceDTO; import com.ctrip.framework.apollo.openapi.dto.OpenNamespaceLockDTO; import com.ctrip.framework.apollo.openapi.util.OpenApiBeanUtils; import com.ctrip.framework.apollo.portal.entity.bo.NamespaceBO; import com.ctrip.framework.apollo.portal.environment.Env; import com.ctrip.framework.apollo.portal.listener.AppNamespaceCreationEvent; import com.ctrip.framework.apollo.portal.service.AppNamespaceService; import com.ctrip.framework.apollo.portal.service.NamespaceLockService; import com.ctrip.framework.apollo.portal.service.NamespaceService; import java.util.List; import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; /** * @author wxq */ @Service public class ServerNamespaceOpenApiService implements NamespaceOpenApiService { private final AppNamespaceService appNamespaceService; private final ApplicationEventPublisher publisher; private final NamespaceService namespaceService; private final NamespaceLockService namespaceLockService; public ServerNamespaceOpenApiService(AppNamespaceService appNamespaceService, ApplicationEventPublisher publisher, NamespaceService namespaceService, NamespaceLockService namespaceLockService) { this.appNamespaceService = appNamespaceService; this.publisher = publisher; this.namespaceService = namespaceService; this.namespaceLockService = namespaceLockService; } @Override public OpenNamespaceDTO getNamespace(String appId, String env, String clusterName, String namespaceName, boolean fillItemDetail) { NamespaceBO namespaceBO = namespaceService.loadNamespaceBO(appId, Env.valueOf(env), clusterName, namespaceName, fillItemDetail, false); if (namespaceBO == null) { return null; } return OpenApiBeanUtils.transformFromNamespaceBO(namespaceBO); } @Override public List getNamespaces(String appId, String env, String clusterName, boolean fillItemDetail) { return OpenApiBeanUtils.batchTransformFromNamespaceBOs(namespaceService.findNamespaceBOs(appId, Env.valueOf(env), clusterName, fillItemDetail, false)); } @Override public OpenAppNamespaceDTO createAppNamespace(OpenAppNamespaceDTO appNamespaceDTO) { AppNamespace appNamespace = OpenApiBeanUtils.transformToAppNamespace(appNamespaceDTO); AppNamespace createdAppNamespace = appNamespaceService.createAppNamespaceInLocal(appNamespace, appNamespaceDTO.isAppendNamespacePrefix()); publisher.publishEvent(new AppNamespaceCreationEvent(createdAppNamespace)); return OpenApiBeanUtils.transformToOpenAppNamespaceDTO(createdAppNamespace); } @Override public OpenNamespaceLockDTO getNamespaceLock(String appId, String env, String clusterName, String namespaceName) { NamespaceDTO namespace = namespaceService.loadNamespaceBaseInfo(appId, Env.valueOf(env), clusterName, namespaceName); NamespaceLockDTO lockDTO = namespaceLockService.getNamespaceLock(appId, Env.valueOf(env), clusterName, namespaceName); return OpenApiBeanUtils.transformFromNamespaceLockDTO(namespace.getNamespaceName(), lockDTO); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/server/service/ServerOrganizationOpenApiService.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.openapi.server.service; import com.ctrip.framework.apollo.openapi.model.OpenOrganizationDto; import com.ctrip.framework.apollo.openapi.util.OpenApiModelConverters; import com.ctrip.framework.apollo.portal.component.config.PortalConfig; import org.springframework.stereotype.Service; import java.util.List; @Service public class ServerOrganizationOpenApiService implements OrganizationOpenApiService { private final PortalConfig portalConfig; public ServerOrganizationOpenApiService(PortalConfig portalConfig) { this.portalConfig = portalConfig; } @Override public List getOrganizations() { return OpenApiModelConverters.fromOrganizations(portalConfig.organizations()); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/server/service/ServerReleaseOpenApiService.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.openapi.server.service; import com.ctrip.framework.apollo.common.dto.ReleaseDTO; import com.ctrip.framework.apollo.common.utils.BeanUtils; import com.ctrip.framework.apollo.openapi.api.ReleaseOpenApiService; import com.ctrip.framework.apollo.openapi.dto.NamespaceReleaseDTO; import com.ctrip.framework.apollo.openapi.dto.OpenReleaseDTO; import com.ctrip.framework.apollo.openapi.util.OpenApiBeanUtils; import com.ctrip.framework.apollo.portal.entity.model.NamespaceReleaseModel; import com.ctrip.framework.apollo.portal.environment.Env; import com.ctrip.framework.apollo.portal.service.ReleaseService; import org.springframework.stereotype.Service; /** * @author wxq */ @Service public class ServerReleaseOpenApiService implements ReleaseOpenApiService { private final ReleaseService releaseService; public ServerReleaseOpenApiService(ReleaseService releaseService) { this.releaseService = releaseService; } @Override public OpenReleaseDTO publishNamespace(String appId, String env, String clusterName, String namespaceName, NamespaceReleaseDTO releaseDTO) { NamespaceReleaseModel releaseModel = BeanUtils.transform(NamespaceReleaseModel.class, releaseDTO); releaseModel.setAppId(appId); releaseModel.setEnv(Env.valueOf(env).toString()); releaseModel.setClusterName(clusterName); releaseModel.setNamespaceName(namespaceName); return OpenApiBeanUtils.transformFromReleaseDTO(releaseService.publish(releaseModel)); } @Override public OpenReleaseDTO getLatestActiveRelease(String appId, String env, String clusterName, String namespaceName) { ReleaseDTO releaseDTO = releaseService.loadLatestRelease(appId, Env.valueOf(env), clusterName, namespaceName); if (releaseDTO == null) { return null; } return OpenApiBeanUtils.transformFromReleaseDTO(releaseDTO); } @Override public void rollbackRelease(String env, long releaseId, String operator) { releaseService.rollback(Env.valueOf(env), releaseId, operator); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/service/ConsumerRolePermissionService.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.openapi.service; import com.ctrip.framework.apollo.openapi.entity.ConsumerRole; import com.ctrip.framework.apollo.openapi.repository.ConsumerRoleRepository; import com.ctrip.framework.apollo.portal.entity.po.Permission; import com.ctrip.framework.apollo.portal.entity.po.RolePermission; import com.ctrip.framework.apollo.portal.repository.PermissionRepository; import com.ctrip.framework.apollo.portal.repository.RolePermissionRepository; import com.google.common.collect.Sets; import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; import java.util.List; import java.util.Set; import java.util.stream.Collectors; /** * @author Jason Song(song_s@ctrip.com) */ @Service public class ConsumerRolePermissionService { private final PermissionRepository permissionRepository; private final ConsumerRoleRepository consumerRoleRepository; private final RolePermissionRepository rolePermissionRepository; public ConsumerRolePermissionService(final PermissionRepository permissionRepository, final ConsumerRoleRepository consumerRoleRepository, final RolePermissionRepository rolePermissionRepository) { this.permissionRepository = permissionRepository; this.consumerRoleRepository = consumerRoleRepository; this.rolePermissionRepository = rolePermissionRepository; } /** * Check whether user has the permission */ public boolean consumerHasPermission(long consumerId, String permissionType, String targetId) { Permission permission = permissionRepository.findTopByPermissionTypeAndTargetId(permissionType, targetId); if (permission == null) { return false; } List consumerRoles = consumerRoleRepository.findByConsumerId(consumerId); if (CollectionUtils.isEmpty(consumerRoles)) { return false; } Set roleIds = consumerRoles.stream().map(ConsumerRole::getRoleId).collect(Collectors.toSet()); List rolePermissions = rolePermissionRepository.findByRoleIdIn(roleIds); if (CollectionUtils.isEmpty(rolePermissions)) { return false; } for (RolePermission rolePermission : rolePermissions) { if (rolePermission.getPermissionId() == permission.getId()) { return true; } } return false; } public boolean hasAnyPermission(long consumerId, List permissions) { if (CollectionUtils.isEmpty(permissions)) { return false; } List consumerPermissions = permissionRepository.findConsumerPermissions(consumerId); if (CollectionUtils.isEmpty(consumerPermissions)) { return false; } Set userPermissionSet = Sets.newHashSet(consumerPermissions); return permissions.stream().anyMatch(userPermissionSet::contains); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/service/ConsumerService.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.openapi.service; import static com.ctrip.framework.apollo.portal.service.SystemRoleManagerService.CREATE_APPLICATION_ROLE_NAME; import com.ctrip.framework.apollo.common.exception.BadRequestException; import com.ctrip.framework.apollo.common.exception.NotFoundException; import com.ctrip.framework.apollo.openapi.entity.Consumer; import com.ctrip.framework.apollo.openapi.entity.ConsumerAudit; import com.ctrip.framework.apollo.openapi.entity.ConsumerRole; import com.ctrip.framework.apollo.openapi.entity.ConsumerToken; import com.ctrip.framework.apollo.openapi.repository.ConsumerAuditRepository; import com.ctrip.framework.apollo.openapi.repository.ConsumerRepository; import com.ctrip.framework.apollo.openapi.repository.ConsumerRoleRepository; import com.ctrip.framework.apollo.openapi.repository.ConsumerTokenRepository; import com.ctrip.framework.apollo.portal.component.config.PortalConfig; import com.ctrip.framework.apollo.portal.entity.bo.UserInfo; import com.ctrip.framework.apollo.portal.entity.po.Role; import com.ctrip.framework.apollo.portal.entity.vo.consumer.ConsumerInfo; import com.ctrip.framework.apollo.portal.repository.RoleRepository; import com.ctrip.framework.apollo.portal.service.RolePermissionService; import com.ctrip.framework.apollo.portal.spi.UserInfoHolder; import com.ctrip.framework.apollo.portal.spi.UserService; import com.ctrip.framework.apollo.portal.util.RoleUtils; import com.google.common.base.Charsets; import com.google.common.base.Joiner; import com.google.common.base.Preconditions; import com.google.common.base.Strings; import com.google.common.hash.Hashing; import java.util.ArrayList; import java.util.Collections; import java.util.Map; import java.util.Objects; import org.apache.commons.lang3.time.FastDateFormat; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.Arrays; import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.stream.Collectors; import org.springframework.util.CollectionUtils; /** * @author Jason Song(song_s@ctrip.com) */ @Service public class ConsumerService { private static final FastDateFormat TIMESTAMP_FORMAT = FastDateFormat.getInstance("yyyyMMddHHmmss"); private static final Joiner KEY_JOINER = Joiner.on("|"); private final UserInfoHolder userInfoHolder; private final ConsumerTokenRepository consumerTokenRepository; private final ConsumerRepository consumerRepository; private final ConsumerAuditRepository consumerAuditRepository; private final ConsumerRoleRepository consumerRoleRepository; private final PortalConfig portalConfig; private final RolePermissionService rolePermissionService; private final UserService userService; private final RoleRepository roleRepository; public ConsumerService(final UserInfoHolder userInfoHolder, final ConsumerTokenRepository consumerTokenRepository, final ConsumerRepository consumerRepository, final ConsumerAuditRepository consumerAuditRepository, final ConsumerRoleRepository consumerRoleRepository, final PortalConfig portalConfig, final RolePermissionService rolePermissionService, final UserService userService, final RoleRepository roleRepository) { this.userInfoHolder = userInfoHolder; this.consumerTokenRepository = consumerTokenRepository; this.consumerRepository = consumerRepository; this.consumerAuditRepository = consumerAuditRepository; this.consumerRoleRepository = consumerRoleRepository; this.portalConfig = portalConfig; this.rolePermissionService = rolePermissionService; this.userService = userService; this.roleRepository = roleRepository; } public Consumer createConsumer(Consumer consumer) { String appId = consumer.getAppId(); Consumer managedConsumer = consumerRepository.findByAppId(appId); if (managedConsumer != null) { throw new BadRequestException("Consumer already exist"); } String ownerName = consumer.getOwnerName(); UserInfo owner = userService.findByUserId(ownerName); if (owner == null) { throw BadRequestException.userNotExists(ownerName); } consumer.setOwnerEmail(owner.getEmail()); String operator = userInfoHolder.getUser().getUserId(); consumer.setDataChangeCreatedBy(operator); consumer.setDataChangeLastModifiedBy(operator); return consumerRepository.save(consumer); } public ConsumerToken generateAndSaveConsumerToken(Consumer consumer, Integer rateLimit, Date expires) { Preconditions.checkArgument(consumer != null, "Consumer can not be null"); ConsumerToken consumerToken = generateConsumerToken(consumer, rateLimit, expires); consumerToken.setId(0); return consumerTokenRepository.save(consumerToken); } public ConsumerToken getConsumerTokenByAppId(String appId) { Consumer consumer = consumerRepository.findByAppId(appId); if (consumer == null) { return null; } return consumerTokenRepository.findByConsumerId(consumer.getId()); } public ConsumerToken getConsumerTokenByToken(String token) { if (Strings.isNullOrEmpty(token)) { return null; } return consumerTokenRepository.findTopByTokenAndExpiresAfter(token, new Date()); } public Long getConsumerIdByToken(String token) { ConsumerToken consumerToken = getConsumerTokenByToken(token); return consumerToken == null ? null : consumerToken.getConsumerId(); } public Consumer getConsumerByConsumerId(long consumerId) { return consumerRepository.findById(consumerId).orElse(null); } @Transactional public List assignNamespaceRoleToConsumer(String token, String appId, String namespaceName) { return assignNamespaceRoleToConsumer(token, appId, namespaceName, null); } @Transactional public List assignNamespaceRoleToConsumer(String token, String appId, String namespaceName, String env) { Long consumerId = getConsumerIdByToken(token); if (consumerId == null) { throw new BadRequestException("Token is Illegal"); } Role namespaceModifyRole = rolePermissionService .findRoleByRoleName(RoleUtils.buildModifyNamespaceRoleName(appId, namespaceName, env)); Role namespaceReleaseRole = rolePermissionService .findRoleByRoleName(RoleUtils.buildReleaseNamespaceRoleName(appId, namespaceName, env)); if (namespaceModifyRole == null || namespaceReleaseRole == null) { throw new BadRequestException( "Namespace's role does not exist. Please check whether namespace has created."); } long namespaceModifyRoleId = namespaceModifyRole.getId(); long namespaceReleaseRoleId = namespaceReleaseRole.getId(); ConsumerRole managedModifyRole = consumerRoleRepository.findByConsumerIdAndRoleId(consumerId, namespaceModifyRoleId); ConsumerRole managedReleaseRole = consumerRoleRepository.findByConsumerIdAndRoleId(consumerId, namespaceReleaseRoleId); if (managedModifyRole != null && managedReleaseRole != null) { return Arrays.asList(managedModifyRole, managedReleaseRole); } String operator = userInfoHolder.getUser().getUserId(); ConsumerRole namespaceModifyConsumerRole = createConsumerRole(consumerId, namespaceModifyRoleId, operator); ConsumerRole namespaceReleaseConsumerRole = createConsumerRole(consumerId, namespaceReleaseRoleId, operator); ConsumerRole createdModifyConsumerRole = consumerRoleRepository.save(namespaceModifyConsumerRole); ConsumerRole createdReleaseConsumerRole = consumerRoleRepository.save(namespaceReleaseConsumerRole); return Arrays.asList(createdModifyConsumerRole, createdReleaseConsumerRole); } private ConsumerInfo convert(Consumer consumer, String token, boolean allowCreateApplication, Integer rateLimit) { ConsumerInfo consumerInfo = new ConsumerInfo(); consumerInfo.setConsumerId(consumer.getId()); consumerInfo.setAppId(consumer.getAppId()); consumerInfo.setName(consumer.getName()); consumerInfo.setOwnerName(consumer.getOwnerName()); consumerInfo.setOwnerEmail(consumer.getOwnerEmail()); consumerInfo.setOrgId(consumer.getOrgId()); consumerInfo.setOrgName(consumer.getOrgName()); consumerInfo.setRateLimit(rateLimit); consumerInfo.setToken(token); consumerInfo.setAllowCreateApplication(allowCreateApplication); return consumerInfo; } public ConsumerInfo getConsumerInfoByAppId(String appId) { ConsumerToken consumerToken = getConsumerTokenByAppId(appId); if (null == consumerToken) { return null; } Consumer consumer = consumerRepository.findByAppId(appId); if (consumer == null) { return null; } return convert(consumer, consumerToken.getToken(), isAllowCreateApplication(consumer.getId()), getRateLimit(consumer.getId())); } private boolean isAllowCreateApplication(Long consumerId) { return isAllowCreateApplication(Collections.singletonList(consumerId)).get(0); } private Integer getRateLimit(Long consumerId) { List list = getRateLimit(Collections.singletonList(consumerId)); if (CollectionUtils.isEmpty(list)) { return 0; } return list.get(0); } private List isAllowCreateApplication(List consumerIdList) { Role createAppRole = getCreateAppRole(); if (createAppRole == null) { List list = new ArrayList<>(consumerIdList.size()); for (Long ignored : consumerIdList) { list.add(false); } return list; } long roleId = createAppRole.getId(); List list = new ArrayList<>(consumerIdList.size()); for (Long consumerId : consumerIdList) { ConsumerRole createAppConsumerRole = consumerRoleRepository.findByConsumerIdAndRoleId(consumerId, roleId); list.add(createAppConsumerRole != null); } return list; } private List getRateLimit(List consumerIds) { List consumerTokens = consumerTokenRepository.findByConsumerIdIn(consumerIds); Map consumerRateLimits = consumerTokens.stream().collect(Collectors.toMap( ConsumerToken::getConsumerId, consumerToken -> consumerToken.getRateLimit() != null ? consumerToken.getRateLimit() : 0)); return consumerIds.stream().map(id -> consumerRateLimits.getOrDefault(id, 0)) .collect(Collectors.toList()); } private Role getCreateAppRole() { return rolePermissionService.findRoleByRoleName(CREATE_APPLICATION_ROLE_NAME); } public ConsumerRole assignCreateApplicationRoleToConsumer(String token) { Long consumerId = getConsumerIdByToken(token); if (consumerId == null) { throw new BadRequestException("Token is Illegal"); } Role createAppRole = getCreateAppRole(); if (createAppRole == null) { throw NotFoundException.roleNotFound(CREATE_APPLICATION_ROLE_NAME); } long roleId = createAppRole.getId(); ConsumerRole createAppConsumerRole = consumerRoleRepository.findByConsumerIdAndRoleId(consumerId, roleId); if (createAppConsumerRole != null) { return createAppConsumerRole; } String operator = userInfoHolder.getUser().getUserId(); ConsumerRole consumerRole = createConsumerRole(consumerId, roleId, operator); return consumerRoleRepository.save(consumerRole); } @Transactional public ConsumerRole assignAppRoleToConsumer(String token, String appId) { Long consumerId = getConsumerIdByToken(token); return assignAppRoleToConsumer(consumerId, appId); } @Transactional public ConsumerRole assignAppRoleToConsumer(Long consumerId, String appId) { if (consumerId == null) { throw new BadRequestException("Token is Illegal"); } Role masterRole = rolePermissionService.findRoleByRoleName(RoleUtils.buildAppMasterRoleName(appId)); if (masterRole == null) { throw new BadRequestException( "App's role does not exist. Please check whether app has created."); } long roleId = masterRole.getId(); ConsumerRole managedModifyRole = consumerRoleRepository.findByConsumerIdAndRoleId(consumerId, roleId); if (managedModifyRole != null) { return managedModifyRole; } String operator = userInfoHolder.getUser().getUserId(); ConsumerRole consumerRole = createConsumerRole(consumerId, roleId, operator); return consumerRoleRepository.save(consumerRole); } @Transactional public void createConsumerAudits(Iterable consumerAudits) { consumerAuditRepository.saveAll(consumerAudits); } @Transactional public ConsumerToken createConsumerToken(ConsumerToken entity) { entity.setId(0); // for protection return consumerTokenRepository.save(entity); } private ConsumerToken generateConsumerToken(Consumer consumer, Integer rateLimit, Date expires) { long consumerId = consumer.getId(); String createdBy = userInfoHolder.getUser().getUserId(); Date createdTime = new Date(); if (rateLimit == null || rateLimit < 0) { rateLimit = 0; } ConsumerToken consumerToken = new ConsumerToken(); consumerToken.setConsumerId(consumerId); consumerToken.setRateLimit(rateLimit); consumerToken.setExpires(expires); consumerToken.setDataChangeCreatedBy(createdBy); consumerToken.setDataChangeCreatedTime(createdTime); consumerToken.setDataChangeLastModifiedBy(createdBy); consumerToken.setDataChangeLastModifiedTime(createdTime); generateAndEnrichToken(consumer, consumerToken); return consumerToken; } void generateAndEnrichToken(Consumer consumer, ConsumerToken consumerToken) { Preconditions.checkArgument(consumer != null); if (consumerToken.getDataChangeCreatedTime() == null) { consumerToken.setDataChangeCreatedTime(new Date()); } consumerToken.setToken(generateToken(consumer.getAppId(), consumerToken.getDataChangeCreatedTime(), portalConfig.consumerTokenSalt())); } @SuppressWarnings("UnstableApiUsage") String generateToken(String consumerAppId, Date generationTime, String consumerTokenSalt) { return Hashing.sha256().hashString( KEY_JOINER.join(consumerAppId, TIMESTAMP_FORMAT.format(generationTime), consumerTokenSalt), Charsets.UTF_8).toString(); } ConsumerRole createConsumerRole(Long consumerId, Long roleId, String operator) { ConsumerRole consumerRole = new ConsumerRole(); consumerRole.setConsumerId(consumerId); consumerRole.setRoleId(roleId); consumerRole.setDataChangeCreatedBy(operator); consumerRole.setDataChangeLastModifiedBy(operator); return consumerRole; } public Set findAppIdsAuthorizedByConsumerId(long consumerId) { List consumerRoles = this.findConsumerRolesByConsumerId(consumerId); List roleIds = consumerRoles.stream().map(ConsumerRole::getRoleId).collect(Collectors.toList()); return this.findAppIdsByRoleIds(roleIds); } private List findConsumerRolesByConsumerId(long consumerId) { return this.consumerRoleRepository.findByConsumerId(consumerId); } private Set findAppIdsByRoleIds(List roleIds) { Iterable roleIterable = this.roleRepository.findAllById(roleIds); Set appIds = new HashSet<>(); roleIterable.forEach(role -> { if (!role.isDeleted()) { String roleName = role.getRoleName(); String appId = RoleUtils.extractAppIdFromRoleName(roleName); appIds.add(appId); } }); return appIds; } List findAllConsumer(Pageable page) { return this.consumerRepository.findAll(page).getContent(); } public List findConsumerInfoList(Pageable page) { List consumerList = findAllConsumer(page); List consumerIdList = consumerList.stream().map(Consumer::getId).collect(Collectors.toList()); List allowCreateApplicationList = isAllowCreateApplication(consumerIdList); List rateLimitList = getRateLimit(consumerIdList); List consumerInfoList = new ArrayList<>(consumerList.size()); for (int i = 0; i < consumerList.size(); i++) { Consumer consumer = consumerList.get(i); // without token ConsumerInfo consumerInfo = convert(consumer, null, allowCreateApplicationList.get(i), rateLimitList.get(i)); consumerInfoList.add(consumerInfo); } return consumerInfoList; } @Transactional public void deleteConsumer(String appId) { Consumer consumer = consumerRepository.findByAppId(appId); if (consumer == null) { throw new BadRequestException("ConsumerApp not exist"); } long consumerId = consumer.getId(); List consumerRoleList = consumerRoleRepository.findByConsumerId(consumerId); ConsumerToken consumerToken = consumerTokenRepository.findByConsumerId(consumerId); consumerRoleRepository.deleteAll(consumerRoleList); consumerRepository.delete(consumer); if (Objects.nonNull(consumerToken)) { consumerTokenRepository.delete(consumerToken); } } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/util/ConsumerAuditUtil.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.openapi.util; import com.ctrip.framework.apollo.core.utils.ApolloThreadFactory; import com.ctrip.framework.apollo.openapi.entity.ConsumerAudit; import com.ctrip.framework.apollo.openapi.service.ConsumerService; import com.ctrip.framework.apollo.tracer.Tracer; import com.google.common.base.Strings; import com.google.common.collect.Lists; import com.google.common.collect.Queues; import org.springframework.beans.factory.InitializingBean; import org.springframework.stereotype.Service; import jakarta.servlet.http.HttpServletRequest; import java.util.Date; import java.util.List; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; /** * @author Jason Song(song_s@ctrip.com) */ @Service public class ConsumerAuditUtil implements InitializingBean { private static final int CONSUMER_AUDIT_MAX_SIZE = 10000; private final BlockingQueue audits = Queues.newLinkedBlockingQueue(CONSUMER_AUDIT_MAX_SIZE); private final ExecutorService auditExecutorService; private final AtomicBoolean auditStopped; private static final int BATCH_SIZE = 100; // ConsumerAuditUtilTest used reflection to set BATCH_TIMEOUT and BATCH_TIMEUNIT, so without // `final` now private static long BATCH_TIMEOUT = 5; private static TimeUnit BATCH_TIMEUNIT = TimeUnit.SECONDS; private final ConsumerService consumerService; public ConsumerAuditUtil(final ConsumerService consumerService) { this.consumerService = consumerService; auditExecutorService = Executors.newSingleThreadExecutor(ApolloThreadFactory.create("ConsumerAuditUtil", true)); auditStopped = new AtomicBoolean(false); } public boolean audit(HttpServletRequest request, long consumerId) { // ignore GET request if ("GET".equalsIgnoreCase(request.getMethod())) { return true; } String uri = request.getRequestURI(); if (!Strings.isNullOrEmpty(request.getQueryString())) { uri += "?" + request.getQueryString(); } ConsumerAudit consumerAudit = new ConsumerAudit(); Date now = new Date(); consumerAudit.setConsumerId(consumerId); consumerAudit.setUri(uri); consumerAudit.setMethod(request.getMethod()); consumerAudit.setDataChangeCreatedTime(now); consumerAudit.setDataChangeLastModifiedTime(now); // throw away audits if exceeds the max size return this.audits.offer(consumerAudit); } @Override public void afterPropertiesSet() throws Exception { auditExecutorService.submit(() -> { while (!auditStopped.get() && !Thread.currentThread().isInterrupted()) { List toAudit = Lists.newArrayList(); try { Queues.drain(audits, toAudit, BATCH_SIZE, BATCH_TIMEOUT, BATCH_TIMEUNIT); if (!toAudit.isEmpty()) { consumerService.createConsumerAudits(toAudit); } } catch (Throwable ex) { Tracer.logError(ex); } } }); } public void stopAudit() { auditStopped.set(true); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/util/ConsumerAuthUtil.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.openapi.util; import com.ctrip.framework.apollo.openapi.entity.ConsumerToken; import com.ctrip.framework.apollo.openapi.service.ConsumerService; import org.springframework.stereotype.Service; import jakarta.servlet.http.HttpServletRequest; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; /** * @author Jason Song(song_s@ctrip.com) */ @Service public class ConsumerAuthUtil { public static final String CONSUMER_ID = "ApolloConsumerId"; private final ConsumerService consumerService; public ConsumerAuthUtil(final ConsumerService consumerService) { this.consumerService = consumerService; } public Long getConsumerId(String token) { return consumerService.getConsumerIdByToken(token); } public ConsumerToken getConsumerToken(String token) { return consumerService.getConsumerTokenByToken(token); } public void storeConsumerId(HttpServletRequest request, Long consumerId) { request.setAttribute(CONSUMER_ID, consumerId); } public long retrieveConsumerId(HttpServletRequest request) { Object value = request.getAttribute(CONSUMER_ID); try { return Long.parseLong(value.toString()); } catch (Throwable ex) { throw new IllegalStateException("No consumer id!", ex); } } // retrieve from RequestContextHolder public long retrieveConsumerIdFromCtx() { ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); if (attributes == null) { throw new IllegalStateException("No Request!"); } HttpServletRequest request = attributes.getRequest(); return retrieveConsumerId(request); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/util/OpenApiBeanUtils.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.openapi.util; import java.lang.reflect.Type; import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; import com.ctrip.framework.apollo.openapi.dto.OpenAppDTO; import com.ctrip.framework.apollo.openapi.dto.OpenAppNamespaceDTO; import com.ctrip.framework.apollo.openapi.dto.OpenClusterDTO; import com.ctrip.framework.apollo.openapi.dto.OpenGrayReleaseRuleDTO; import com.ctrip.framework.apollo.openapi.dto.OpenGrayReleaseRuleItemDTO; import com.ctrip.framework.apollo.openapi.dto.OpenItemDTO; import com.ctrip.framework.apollo.openapi.dto.OpenNamespaceDTO; import com.ctrip.framework.apollo.openapi.dto.OpenNamespaceLockDTO; import com.ctrip.framework.apollo.openapi.dto.OpenReleaseDTO; import com.ctrip.framework.apollo.openapi.dto.OpenOrganizationDto; import com.ctrip.framework.apollo.portal.entity.vo.Organization; import org.springframework.util.CollectionUtils; import com.ctrip.framework.apollo.common.dto.ClusterDTO; import com.ctrip.framework.apollo.common.dto.GrayReleaseRuleDTO; import com.ctrip.framework.apollo.common.dto.GrayReleaseRuleItemDTO; import com.ctrip.framework.apollo.common.dto.ItemDTO; import com.ctrip.framework.apollo.common.dto.NamespaceLockDTO; import com.ctrip.framework.apollo.common.dto.ReleaseDTO; import com.ctrip.framework.apollo.common.entity.App; import com.ctrip.framework.apollo.common.entity.AppNamespace; import com.ctrip.framework.apollo.common.utils.BeanUtils; import com.ctrip.framework.apollo.portal.entity.bo.ItemBO; import com.ctrip.framework.apollo.portal.entity.bo.NamespaceBO; import com.google.common.base.Preconditions; import com.google.common.reflect.TypeToken; import com.google.gson.Gson; public class OpenApiBeanUtils { private static final Gson GSON = new Gson(); private static final Type TYPE = new TypeToken>() {}.getType(); public static OpenItemDTO transformFromItemDTO(ItemDTO item) { Preconditions.checkArgument(item != null); return BeanUtils.transform(OpenItemDTO.class, item); } public static ItemDTO transformToItemDTO(OpenItemDTO openItemDTO) { Preconditions.checkArgument(openItemDTO != null); return BeanUtils.transform(ItemDTO.class, openItemDTO); } public static OpenAppNamespaceDTO transformToOpenAppNamespaceDTO(AppNamespace appNamespace) { Preconditions.checkArgument(appNamespace != null); return BeanUtils.transform(OpenAppNamespaceDTO.class, appNamespace); } public static AppNamespace transformToAppNamespace(OpenAppNamespaceDTO openAppNamespaceDTO) { Preconditions.checkArgument(openAppNamespaceDTO != null); return BeanUtils.transform(AppNamespace.class, openAppNamespaceDTO); } public static OpenReleaseDTO transformFromReleaseDTO(ReleaseDTO release) { Preconditions.checkArgument(release != null); OpenReleaseDTO openReleaseDTO = BeanUtils.transform(OpenReleaseDTO.class, release); Map configs = GSON.fromJson(release.getConfigurations(), TYPE); openReleaseDTO.setConfigurations(configs); return openReleaseDTO; } public static OpenNamespaceDTO transformFromNamespaceBO(NamespaceBO namespaceBO) { Preconditions.checkArgument(namespaceBO != null); OpenNamespaceDTO openNamespaceDTO = BeanUtils.transform(OpenNamespaceDTO.class, namespaceBO.getBaseInfo()); // app namespace info openNamespaceDTO.setFormat(namespaceBO.getFormat()); openNamespaceDTO.setComment(namespaceBO.getComment()); openNamespaceDTO.setPublic(namespaceBO.isPublic()); // items List items = new LinkedList<>(); List itemBOs = namespaceBO.getItems(); if (!CollectionUtils.isEmpty(itemBOs)) { items.addAll(itemBOs.stream().map(itemBO -> transformFromItemDTO(itemBO.getItem())) .collect(Collectors.toList())); } openNamespaceDTO.setItems(items); return openNamespaceDTO; } public static List batchTransformFromNamespaceBOs( List namespaceBOs) { if (CollectionUtils.isEmpty(namespaceBOs)) { return Collections.emptyList(); } return namespaceBOs.stream().map(OpenApiBeanUtils::transformFromNamespaceBO) .collect(Collectors.toCollection(LinkedList::new)); } public static OpenNamespaceLockDTO transformFromNamespaceLockDTO(String namespaceName, NamespaceLockDTO namespaceLock) { OpenNamespaceLockDTO lock = new OpenNamespaceLockDTO(); lock.setNamespaceName(namespaceName); if (namespaceLock == null) { lock.setLocked(false); } else { lock.setLocked(true); lock.setLockedBy(namespaceLock.getDataChangeCreatedBy()); } return lock; } public static OpenGrayReleaseRuleDTO transformFromGrayReleaseRuleDTO( GrayReleaseRuleDTO grayReleaseRuleDTO) { Preconditions.checkArgument(grayReleaseRuleDTO != null); return BeanUtils.transform(OpenGrayReleaseRuleDTO.class, grayReleaseRuleDTO); } public static GrayReleaseRuleDTO transformToGrayReleaseRuleDTO( OpenGrayReleaseRuleDTO openGrayReleaseRuleDTO) { Preconditions.checkArgument(openGrayReleaseRuleDTO != null); String appId = openGrayReleaseRuleDTO.getAppId(); String branchName = openGrayReleaseRuleDTO.getBranchName(); String clusterName = openGrayReleaseRuleDTO.getClusterName(); String namespaceName = openGrayReleaseRuleDTO.getNamespaceName(); GrayReleaseRuleDTO grayReleaseRuleDTO = new GrayReleaseRuleDTO(appId, clusterName, namespaceName, branchName); Set openGrayReleaseRuleItemDTOSet = openGrayReleaseRuleDTO.getRuleItems(); openGrayReleaseRuleItemDTOSet.forEach(openGrayReleaseRuleItemDTO -> { String clientAppId = openGrayReleaseRuleItemDTO.getClientAppId(); Set clientIpList = openGrayReleaseRuleItemDTO.getClientIpList(); Set clientLabelList = openGrayReleaseRuleItemDTO.getClientLabelList(); GrayReleaseRuleItemDTO ruleItem = new GrayReleaseRuleItemDTO(clientAppId, clientIpList, clientLabelList); grayReleaseRuleDTO.addRuleItem(ruleItem); }); return grayReleaseRuleDTO; } public static List transformFromApps(final List apps) { if (CollectionUtils.isEmpty(apps)) { return Collections.emptyList(); } return apps.stream().map(OpenApiBeanUtils::transformFromApp).collect(Collectors.toList()); } public static OpenAppDTO transformFromApp(final App app) { Preconditions.checkArgument(app != null); return BeanUtils.transform(OpenAppDTO.class, app); } public static OpenClusterDTO transformFromClusterDTO(ClusterDTO Cluster) { Preconditions.checkArgument(Cluster != null); return BeanUtils.transform(OpenClusterDTO.class, Cluster); } public static ClusterDTO transformToClusterDTO(OpenClusterDTO openClusterDTO) { Preconditions.checkArgument(openClusterDTO != null); return BeanUtils.transform(ClusterDTO.class, openClusterDTO); } public static OpenOrganizationDto transformFromOrganization(final Organization organization) { Preconditions.checkArgument(organization != null); return BeanUtils.transform(OpenOrganizationDto.class, organization); } public static List transformFromOrganizations( final List organizations) { if (CollectionUtils.isEmpty(organizations)) { return Collections.emptyList(); } return organizations.stream().map(OpenApiBeanUtils::transformFromOrganization) .collect(Collectors.toList()); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/util/OpenApiModelConverters.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.openapi.util; import com.ctrip.framework.apollo.common.dto.ClusterDTO; import com.ctrip.framework.apollo.common.dto.GrayReleaseRuleDTO; import com.ctrip.framework.apollo.common.dto.GrayReleaseRuleItemDTO; import com.ctrip.framework.apollo.common.dto.InstanceDTO; import com.ctrip.framework.apollo.common.dto.ItemChangeSets; import com.ctrip.framework.apollo.common.dto.ItemDTO; import com.ctrip.framework.apollo.common.dto.NamespaceDTO; import com.ctrip.framework.apollo.common.dto.NamespaceLockDTO; import com.ctrip.framework.apollo.common.dto.ReleaseDTO; import com.ctrip.framework.apollo.common.entity.App; import com.ctrip.framework.apollo.common.entity.AppNamespace; import com.ctrip.framework.apollo.common.utils.BeanUtils; import com.ctrip.framework.apollo.openapi.model.KVEntity; import com.ctrip.framework.apollo.openapi.model.OpenAppDTO; import com.ctrip.framework.apollo.openapi.model.OpenAppNamespaceDTO; import com.ctrip.framework.apollo.openapi.model.OpenClusterDTO; import com.ctrip.framework.apollo.openapi.model.OpenEnvClusterInfo; import com.ctrip.framework.apollo.openapi.model.OpenGrayReleaseRuleDTO; import com.ctrip.framework.apollo.openapi.model.OpenGrayReleaseRuleItemDTO; import com.ctrip.framework.apollo.openapi.model.OpenInstanceDTO; import com.ctrip.framework.apollo.openapi.model.OpenItemChangeSets; import com.ctrip.framework.apollo.openapi.model.OpenItemDTO; import com.ctrip.framework.apollo.openapi.model.OpenItemDiffs; import com.ctrip.framework.apollo.openapi.model.OpenNamespaceDTO; import com.ctrip.framework.apollo.openapi.model.OpenNamespaceIdentifier; import com.ctrip.framework.apollo.openapi.model.OpenNamespaceLockDTO; import com.ctrip.framework.apollo.openapi.model.OpenNamespaceSyncModel; import com.ctrip.framework.apollo.openapi.model.OpenNamespaceTextModel; import com.ctrip.framework.apollo.openapi.model.OpenOrganizationDto; import com.ctrip.framework.apollo.openapi.model.OpenReleaseBO; import com.ctrip.framework.apollo.openapi.model.OpenReleaseDTO; import com.ctrip.framework.apollo.portal.entity.bo.ItemBO; import com.ctrip.framework.apollo.portal.entity.bo.NamespaceBO; import com.ctrip.framework.apollo.portal.entity.bo.ReleaseBO; import com.ctrip.framework.apollo.portal.entity.model.NamespaceSyncModel; import com.ctrip.framework.apollo.portal.entity.model.NamespaceTextModel; import com.ctrip.framework.apollo.portal.entity.vo.EnvClusterInfo; import com.ctrip.framework.apollo.portal.entity.vo.ItemDiffs; import com.ctrip.framework.apollo.portal.entity.vo.NamespaceIdentifier; import com.ctrip.framework.apollo.portal.entity.vo.Organization; import com.google.common.base.Preconditions; import com.google.common.reflect.TypeToken; import com.google.gson.Gson; import org.springframework.util.CollectionUtils; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; /** * Non-invasive converters for OpenAPI generated model classes. * This class mirrors/OpenApiBeanUtils functions but targets com.ctrip.framework.apollo.openapi.model.* types. */ public final class OpenApiModelConverters { private static final Gson GSON = new Gson(); private static final Type TYPE = new TypeToken>() {}.getType(); private OpenApiModelConverters() {} // region Item conversions // originally defined in OpenApiBeanUtils not new added public static OpenItemDTO fromItemDTO(ItemDTO item) { Preconditions.checkArgument(item != null); return BeanUtils.transform(OpenItemDTO.class, item); } // originally defined in OpenApiBeanUtils public static ItemDTO toItemDTO(OpenItemDTO openItemDTO) { Preconditions.checkArgument(openItemDTO != null); return BeanUtils.transform(ItemDTO.class, openItemDTO); } // newly added public static List toItemDTOs(List openItemDTOs) { if (CollectionUtils.isEmpty(openItemDTOs)) { return Collections.emptyList(); } return openItemDTOs.stream().map(OpenApiModelConverters::toItemDTO) .collect(Collectors.toList()); } // newly added public static List fromItemDTOs(List items) { if (CollectionUtils.isEmpty(items)) { return Collections.emptyList(); } return items.stream().map(OpenApiModelConverters::fromItemDTO).collect(Collectors.toList()); } // endregion // region App/AppNamespace conversions // originally defined in OpenApiBeanUtils public static OpenAppNamespaceDTO fromAppNamespace(AppNamespace appNamespace) { Preconditions.checkArgument(appNamespace != null); return BeanUtils.transform(OpenAppNamespaceDTO.class, appNamespace); } // originally defined in OpenApiBeanUtils public static AppNamespace toAppNamespace(OpenAppNamespaceDTO openAppNamespaceDTO) { Preconditions.checkArgument(openAppNamespaceDTO != null); return BeanUtils.transform(AppNamespace.class, openAppNamespaceDTO); } // originally defined in OpenApiBeanUtils public static List fromApps(final List apps) { if (CollectionUtils.isEmpty(apps)) { return Collections.emptyList(); } return apps.stream().map(OpenApiModelConverters::fromApp).collect(Collectors.toList()); } // originally defined in OpenApiBeanUtils public static OpenAppDTO fromApp(final App app) { Preconditions.checkArgument(app != null); return BeanUtils.transform(OpenAppDTO.class, app); } // endregion // region Release conversions // originally defined in OpenApiBeanUtils public static OpenReleaseDTO fromReleaseDTO(ReleaseDTO release) { Preconditions.checkArgument(release != null); OpenReleaseDTO openReleaseDTO = BeanUtils.transform(OpenReleaseDTO.class, release); Map configs = GSON.fromJson(release.getConfigurations(), TYPE); openReleaseDTO.setConfigurations(configs); return openReleaseDTO; } // newly added public static OpenReleaseBO fromReleaseBO(final ReleaseBO releaseBO) { Preconditions.checkArgument(releaseBO != null); OpenReleaseBO openReleaseBO = new OpenReleaseBO(); openReleaseBO.setBaseInfo(fromReleaseDTO(releaseBO.getBaseInfo())); Set items = releaseBO.getItems(); List itemsList = new ArrayList<>(); if (!CollectionUtils.isEmpty(items)) { for (com.ctrip.framework.apollo.portal.entity.bo.KVEntity item : items) { KVEntity kvEntity = new KVEntity(); kvEntity.setKey(item.getKey()); kvEntity.setValue(item.getValue()); itemsList.add(kvEntity); } } openReleaseBO.setItems(itemsList); return openReleaseBO; } // newly added public static List fromReleaseBOs(final List releaseBOs) { if (CollectionUtils.isEmpty(releaseBOs)) { return Collections.emptyList(); } return releaseBOs.stream().map(OpenApiModelConverters::fromReleaseBO) .collect(Collectors.toList()); } // endregion // region Namespace conversions // originally defined in OpenApiBeanUtils public static OpenNamespaceDTO fromNamespaceBO(NamespaceBO namespaceBO) { Preconditions.checkArgument(namespaceBO != null); OpenNamespaceDTO openNamespaceDTO = BeanUtils.transform(OpenNamespaceDTO.class, namespaceBO.getBaseInfo()); openNamespaceDTO.setFormat(namespaceBO.getFormat()); openNamespaceDTO.setComment(namespaceBO.getComment()); openNamespaceDTO.setIsPublic(namespaceBO.isPublic()); List items = new LinkedList<>(); List itemBOs = namespaceBO.getItems(); if (!CollectionUtils.isEmpty(itemBOs)) { items.addAll(itemBOs.stream().map(itemBO -> fromItemDTO(itemBO.getItem())) .collect(Collectors.toList())); } openNamespaceDTO.setItems(items); return openNamespaceDTO; } // originally defined in OpenApiBeanUtils public static List fromNamespaceBOs(List namespaceBOs) { if (CollectionUtils.isEmpty(namespaceBOs)) { return Collections.emptyList(); } return namespaceBOs.stream().map(OpenApiModelConverters::fromNamespaceBO) .collect(Collectors.toCollection(LinkedList::new)); } // originally defined in OpenApiBeanUtils public static OpenNamespaceLockDTO fromNamespaceLockDTO(String namespaceName, NamespaceLockDTO namespaceLock) { OpenNamespaceLockDTO lock = new OpenNamespaceLockDTO(); lock.setNamespaceName(namespaceName); if (namespaceLock == null) { lock.setIsLocked(false); } else { lock.setIsLocked(true); lock.setLockedBy(namespaceLock.getDataChangeCreatedBy()); } return lock; } // newly added public static OpenNamespaceDTO fromNamespaceDTO(NamespaceDTO namespaceDTO) { Preconditions.checkArgument(namespaceDTO != null); return BeanUtils.transform(OpenNamespaceDTO.class, namespaceDTO); } // newly added public static NamespaceTextModel toNamespaceTextModel( final OpenNamespaceTextModel openNamespaceTextModel) { Preconditions.checkArgument(openNamespaceTextModel != null); return BeanUtils.transform(NamespaceTextModel.class, openNamespaceTextModel); } // newly added public static List toNamespaceTextModels( final List openNamespaceTextModels) { if (CollectionUtils.isEmpty(openNamespaceTextModels)) { return Collections.emptyList(); } return openNamespaceTextModels.stream().map(OpenApiModelConverters::toNamespaceTextModel) .collect(Collectors.toList()); } // newly added public static NamespaceIdentifier toNamespaceIdentifier( final OpenNamespaceIdentifier openNamespaceIdentifier) { Preconditions.checkArgument(openNamespaceIdentifier != null); NamespaceIdentifier namespaceIdentifier = new NamespaceIdentifier(); namespaceIdentifier.setAppId(openNamespaceIdentifier.getAppId()); namespaceIdentifier.setEnv(openNamespaceIdentifier.getEnv()); namespaceIdentifier.setClusterName(openNamespaceIdentifier.getClusterName()); namespaceIdentifier.setNamespaceName(openNamespaceIdentifier.getNamespaceName()); return namespaceIdentifier; } // newly added public static List toNamespaceIdentifiers( final List openNamespaceIdentifiers) { if (CollectionUtils.isEmpty(openNamespaceIdentifiers)) { return Collections.emptyList(); } return openNamespaceIdentifiers.stream().map(OpenApiModelConverters::toNamespaceIdentifier) .collect(Collectors.toList()); } // newly added public static OpenNamespaceIdentifier fromNamespaceIdentifier( final NamespaceIdentifier namespaceIdentifier) { Preconditions.checkArgument(namespaceIdentifier != null); OpenNamespaceIdentifier openNamespaceIdentifier = new OpenNamespaceIdentifier(); openNamespaceIdentifier.setAppId(namespaceIdentifier.getAppId()); openNamespaceIdentifier.setEnv(namespaceIdentifier.getEnv().toString()); openNamespaceIdentifier.setClusterName(namespaceIdentifier.getClusterName()); openNamespaceIdentifier.setNamespaceName(namespaceIdentifier.getNamespaceName()); return openNamespaceIdentifier; } // newly added public static NamespaceSyncModel toNamespaceSyncModel( final OpenNamespaceSyncModel openNamespaceSyncModel) { Preconditions.checkArgument(openNamespaceSyncModel != null); NamespaceSyncModel model = BeanUtils.transform(NamespaceSyncModel.class, openNamespaceSyncModel); if (openNamespaceSyncModel.getSyncToNamespaces() != null) { model.setSyncToNamespaces( toNamespaceIdentifiers(openNamespaceSyncModel.getSyncToNamespaces())); } if (openNamespaceSyncModel.getSyncItems() != null) { model.setSyncItems(toItemDTOs(openNamespaceSyncModel.getSyncItems())); } return model; } // newly added public static List toNamespaceSyncModels( final List openNamespaceSyncModels) { if (CollectionUtils.isEmpty(openNamespaceSyncModels)) { return Collections.emptyList(); } return openNamespaceSyncModels.stream().map(OpenApiModelConverters::toNamespaceSyncModel) .collect(Collectors.toList()); } // endregion // region Gray release rule conversions // originally defined in OpenApiBeanUtils public static OpenGrayReleaseRuleDTO fromGrayReleaseRuleDTO( GrayReleaseRuleDTO grayReleaseRuleDTO) { Preconditions.checkArgument(grayReleaseRuleDTO != null); return BeanUtils.transform(OpenGrayReleaseRuleDTO.class, grayReleaseRuleDTO); } // originally defined in OpenApiBeanUtils public static GrayReleaseRuleDTO toGrayReleaseRuleDTO( OpenGrayReleaseRuleDTO openGrayReleaseRuleDTO) { Preconditions.checkArgument(openGrayReleaseRuleDTO != null); String appId = openGrayReleaseRuleDTO.getAppId(); String branchName = openGrayReleaseRuleDTO.getBranchName(); String clusterName = openGrayReleaseRuleDTO.getClusterName(); String namespaceName = openGrayReleaseRuleDTO.getNamespaceName(); GrayReleaseRuleDTO grayReleaseRuleDTO = new GrayReleaseRuleDTO(appId, clusterName, namespaceName, branchName); List openGrayReleaseRuleItemDTOSet = openGrayReleaseRuleDTO.getRuleItems(); if (!CollectionUtils.isEmpty(openGrayReleaseRuleItemDTOSet)) { openGrayReleaseRuleItemDTOSet.forEach(openGrayReleaseRuleItemDTO -> { String clientAppId = openGrayReleaseRuleItemDTO.getClientAppId(); Set clientIpList = openGrayReleaseRuleItemDTO.getClientIpList() != null ? new HashSet<>(openGrayReleaseRuleItemDTO.getClientIpList()) : new HashSet<>(); Set clientLabelList = openGrayReleaseRuleItemDTO.getClientLabelList() != null ? new HashSet<>(openGrayReleaseRuleItemDTO.getClientLabelList()) : new HashSet<>(); GrayReleaseRuleItemDTO ruleItem = new GrayReleaseRuleItemDTO(clientAppId, clientIpList, clientLabelList); grayReleaseRuleDTO.addRuleItem(ruleItem); }); } return grayReleaseRuleDTO; } // endregion // region Cluster conversions // originally defined in OpenApiBeanUtils public static OpenClusterDTO fromClusterDTO(ClusterDTO cluster) { Preconditions.checkArgument(cluster != null); return BeanUtils.transform(OpenClusterDTO.class, cluster); } // originally defined in OpenApiBeanUtils public static ClusterDTO toClusterDTO(OpenClusterDTO openClusterDTO) { Preconditions.checkArgument(openClusterDTO != null); return BeanUtils.transform(ClusterDTO.class, openClusterDTO); } // endregion // region Organization conversions // originally defined in OpenApiBeanUtils public static OpenOrganizationDto fromOrganization(final Organization organization) { Preconditions.checkArgument(organization != null); return BeanUtils.transform(OpenOrganizationDto.class, organization); } // originally defined in OpenApiBeanUtils public static List fromOrganizations( final List organizations) { if (CollectionUtils.isEmpty(organizations)) { return Collections.emptyList(); } return organizations.stream().map(OpenApiModelConverters::fromOrganization) .collect(Collectors.toList()); } // endregion // region Instance conversions // newly added public static OpenInstanceDTO fromInstanceDTO(final InstanceDTO instanceDTO) { Preconditions.checkArgument(instanceDTO != null); return BeanUtils.transform(OpenInstanceDTO.class, instanceDTO); } // newly added public static List fromInstanceDTOs(final List instanceDTOs) { if (CollectionUtils.isEmpty(instanceDTOs)) { return Collections.emptyList(); } return instanceDTOs.stream().map(OpenApiModelConverters::fromInstanceDTO) .collect(Collectors.toList()); } // endregion // region Env/Cluster info conversions // newly added public static OpenEnvClusterInfo fromEnvClusterInfo(final EnvClusterInfo envClusterInfo) { Preconditions.checkArgument(envClusterInfo != null); return BeanUtils.transform(OpenEnvClusterInfo.class, envClusterInfo); } // newly added public static List fromEnvClusterInfos( final List envClusterInfos) { if (CollectionUtils.isEmpty(envClusterInfos)) { return Collections.emptyList(); } return envClusterInfos.stream().map(OpenApiModelConverters::fromEnvClusterInfo) .collect(Collectors.toList()); } // endregion // region Item diffs/change sets // newly added public static OpenItemChangeSets fromItemChangeSets(final ItemChangeSets itemChangeSets) { Preconditions.checkArgument(itemChangeSets != null); OpenItemChangeSets openItemChangeSets = new OpenItemChangeSets(); if (itemChangeSets.getCreateItems() != null) { openItemChangeSets.setCreateItems(fromItemDTOs(itemChangeSets.getCreateItems())); } if (itemChangeSets.getUpdateItems() != null) { openItemChangeSets.setUpdateItems(fromItemDTOs(itemChangeSets.getUpdateItems())); } if (itemChangeSets.getDeleteItems() != null) { openItemChangeSets.setDeleteItems(fromItemDTOs(itemChangeSets.getDeleteItems())); } return openItemChangeSets; } // newly added public static OpenItemDiffs fromItemDiffs(final ItemDiffs itemDiffs) { Preconditions.checkArgument(itemDiffs != null); OpenItemDiffs openItemDiffs = new OpenItemDiffs(); if (itemDiffs.getNamespace() != null) { openItemDiffs.setNamespace(fromNamespaceIdentifier(itemDiffs.getNamespace())); } if (itemDiffs.getDiffs() != null) { openItemDiffs.setDiffs(fromItemChangeSets(itemDiffs.getDiffs())); } openItemDiffs.setExtInfo(itemDiffs.getExtInfo()); return openItemDiffs; } // newly added public static List fromItemDiffsList(final List itemDiffsList) { if (CollectionUtils.isEmpty(itemDiffsList)) { return Collections.emptyList(); } return itemDiffsList.stream().map(OpenApiModelConverters::fromItemDiffs) .collect(Collectors.toList()); } // endregion } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/v1/controller/AppController.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.openapi.v1.controller; import com.ctrip.framework.apollo.audit.annotation.ApolloAuditLog; import com.ctrip.framework.apollo.audit.annotation.OpType; import com.ctrip.framework.apollo.common.exception.BadRequestException; import com.ctrip.framework.apollo.openapi.api.AppManagementApi; import com.ctrip.framework.apollo.openapi.model.MultiResponseEntity; import com.ctrip.framework.apollo.openapi.model.OpenAppDTO; import com.ctrip.framework.apollo.openapi.model.OpenCreateAppDTO; import com.ctrip.framework.apollo.openapi.model.OpenEnvClusterDTO; import com.ctrip.framework.apollo.openapi.server.service.AppOpenApiService; import com.ctrip.framework.apollo.openapi.service.ConsumerService; import com.ctrip.framework.apollo.openapi.util.ConsumerAuthUtil; import com.ctrip.framework.apollo.portal.entity.model.AppModel; import com.ctrip.framework.apollo.portal.spi.UserService; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.RestController; import jakarta.transaction.Transactional; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.Set; @RestController("openapiAppController") public class AppController implements AppManagementApi { private final ConsumerAuthUtil consumerAuthUtil; private final ConsumerService consumerService; private final AppOpenApiService appOpenApiService; private final UserService userService; public AppController(final ConsumerAuthUtil consumerAuthUtil, final ConsumerService consumerService, final AppOpenApiService appOpenApiService, final UserService userService) { this.consumerAuthUtil = consumerAuthUtil; this.consumerService = consumerService; this.appOpenApiService = appOpenApiService; this.userService = userService; } /** * @see com.ctrip.framework.apollo.portal.controller.AppController#create(AppModel) */ @Transactional @PreAuthorize(value = "@unifiedPermissionValidator.hasCreateApplicationPermission()") @Override public ResponseEntity createApp(OpenCreateAppDTO req) { if (null == req.getApp()) { throw new BadRequestException("App is null"); } final OpenAppDTO app = req.getApp(); if (null == app.getAppId()) { throw new BadRequestException("AppId is null"); } // create app this.appOpenApiService.createApp(req); if (Boolean.TRUE.equals(req.getAssignAppRoleToSelf())) { long consumerId = this.consumerAuthUtil.retrieveConsumerIdFromCtx(); consumerService.assignAppRoleToConsumer(consumerId, app.getAppId()); } return ResponseEntity.ok().build(); } @Override public ResponseEntity> getEnvClusterInfo(String appId) { return ResponseEntity.ok(appOpenApiService.getEnvClusterInfo(appId)); } @Override public ResponseEntity> findApps(String appIds) { if (StringUtils.hasText(appIds)) { return ResponseEntity .ok(this.appOpenApiService.getAppsInfo(Arrays.asList(appIds.split(",")))); } else { return ResponseEntity.ok(this.appOpenApiService.getAllApps()); } } /** * @return which apps can be operated by open api */ @Override public ResponseEntity> findAppsAuthorized() { long consumerId = this.consumerAuthUtil.retrieveConsumerIdFromCtx(); Set appIds = this.consumerService.findAppIdsAuthorizedByConsumerId(consumerId); return ResponseEntity.ok(appOpenApiService.getAppsInfo(new ArrayList<>(appIds))); } /** * get single app info (new added) */ @Override public ResponseEntity getApp(String appId) { List apps = appOpenApiService.getAppsInfo(Collections.singletonList(appId)); if (null == apps || apps.isEmpty()) { throw new BadRequestException("App not found: " + appId); } return ResponseEntity.ok(apps.get(0)); } /** * update app (new added) */ @Override @PreAuthorize(value = "@unifiedPermissionValidator.isAppAdmin(#appId)") @ApolloAuditLog(type = OpType.UPDATE, name = "App.update") public ResponseEntity updateApp(String appId, String operator, OpenAppDTO dto) { if (!Objects.equals(appId, dto.getAppId())) { throw new BadRequestException("The App Id of path variable and request body is different"); } if (userService.findByUserId(operator) == null) { throw BadRequestException.userNotExists(operator); } appOpenApiService.updateApp(dto); return ResponseEntity.ok(dto); } /** * Get the current Consumer's application list (paginated) (new added) */ @Override public ResponseEntity> getAppsBySelf(Integer page, Integer size) { long consumerId = this.consumerAuthUtil.retrieveConsumerIdFromCtx(); Set authorizedAppIds = this.consumerService.findAppIdsAuthorizedByConsumerId(consumerId); List apps = appOpenApiService.getAppsBySelf(authorizedAppIds, page, size); return ResponseEntity.ok(apps); } /** * Create an application in a specified environment (new added) * POST /openapi/v1/apps/envs/{env} */ @Override @PreAuthorize(value = "@unifiedPermissionValidator.hasCreateApplicationPermission()") @ApolloAuditLog(type = OpType.CREATE, name = "App.create.forEnv") public ResponseEntity createAppInEnv(String env, String operator, OpenAppDTO app) { if (userService.findByUserId(operator) == null) { throw BadRequestException.userNotExists(operator); } appOpenApiService.createAppInEnv(env, app, operator); return ResponseEntity.ok().build(); } /** * Delete App (new added) */ @Override @PreAuthorize(value = "@unifiedPermissionValidator.isAppAdmin(#appId)") @ApolloAuditLog(type = OpType.DELETE, name = "App.delete") public ResponseEntity deleteApp(String appId, String operator) { if (userService.findByUserId(operator) == null) { throw BadRequestException.userNotExists(operator); } appOpenApiService.deleteApp(appId); return ResponseEntity.ok().build(); } /** * Find miss env (new added) */ @Override public ResponseEntity findMissEnvs(String appId) { return ResponseEntity.ok(appOpenApiService.findMissEnvs(appId)); } /** * Find appNavTree (new added) */ @Override public ResponseEntity getAppNavTree(String appId) { return ResponseEntity.ok(appOpenApiService.getAppNavTree(appId)); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/v1/controller/ClusterController.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.openapi.v1.controller; import com.ctrip.framework.apollo.audit.annotation.ApolloAuditLog; import com.ctrip.framework.apollo.audit.annotation.OpType; import com.ctrip.framework.apollo.openapi.server.service.ClusterOpenApiService; import com.ctrip.framework.apollo.portal.component.UserIdentityContextHolder; import com.ctrip.framework.apollo.portal.constant.UserIdentityConstants; import com.ctrip.framework.apollo.portal.spi.UserInfoHolder; import com.ctrip.framework.apollo.portal.spi.UserService; import java.util.Objects; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.RestController; import com.ctrip.framework.apollo.common.exception.BadRequestException; import com.ctrip.framework.apollo.common.utils.InputValidator; import com.ctrip.framework.apollo.common.utils.RequestPrecondition; import com.ctrip.framework.apollo.core.utils.StringUtils; import com.ctrip.framework.apollo.openapi.api.ClusterManagementApi; import com.ctrip.framework.apollo.openapi.model.OpenClusterDTO; import org.springframework.http.ResponseEntity; @RestController("openapiClusterController") public class ClusterController implements ClusterManagementApi { private final UserService userService; private final ClusterOpenApiService clusterOpenApiService; private final UserInfoHolder userInfoHolder; public ClusterController(UserService userService, ClusterOpenApiService clusterOpenApiService, UserInfoHolder userInfoHolder) { this.userService = userService; this.clusterOpenApiService = clusterOpenApiService; this.userInfoHolder = userInfoHolder; } @Override public ResponseEntity getCluster(String appId, String clusterName, String env) { return ResponseEntity.ok(this.clusterOpenApiService.getCluster(appId, env, clusterName)); } @PreAuthorize(value = "@unifiedPermissionValidator.hasCreateClusterPermission(#appId)") @Override public ResponseEntity createCluster(String appId, String env, OpenClusterDTO cluster) { if (!Objects.equals(appId, cluster.getAppId())) { throw new BadRequestException("AppId not equal. AppId in path = %s, AppId in payload = %s", appId, cluster.getAppId()); } String clusterName = cluster.getName(); String operator; if (UserIdentityConstants.USER.equals(UserIdentityContextHolder.getAuthType())) { operator = userInfoHolder.getUser().getUserId(); cluster.setDataChangeLastModifiedBy(operator); cluster.setDataChangeCreatedBy(operator); } else { operator = cluster.getDataChangeCreatedBy(); } RequestPrecondition.checkArguments(!StringUtils.isContainEmpty(clusterName, operator), "name and dataChangeCreatedBy should not be null or empty"); if (!InputValidator.isValidClusterNamespace(clusterName)) { throw BadRequestException .invalidClusterNameFormat(InputValidator.INVALID_CLUSTER_NAMESPACE_MESSAGE); } if (userService.findByUserId(operator) == null) { throw BadRequestException.userNotExists(operator); } return ResponseEntity.ok(this.clusterOpenApiService.createCluster(env, cluster)); } /** * Delete Clusters */ @PreAuthorize(value = "@unifiedPermissionValidator.isAppAdmin(#appId)") @ApolloAuditLog(type = OpType.DELETE, name = "Cluster.delete") @Override public ResponseEntity deleteCluster(String env, String appId, String clusterName, String operator) { if (UserIdentityConstants.CONSUMER.equals(UserIdentityContextHolder.getAuthType())) { RequestPrecondition.checkArguments(!StringUtils.isContainEmpty(operator), "operator should not be null or empty"); if (userService.findByUserId(operator) == null) { throw BadRequestException.userNotExists(operator); } } clusterOpenApiService.deleteCluster(env, appId, clusterName); return ResponseEntity.ok().build(); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/v1/controller/EnvController.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.openapi.v1.controller; import com.ctrip.framework.apollo.openapi.api.EnvironmentManagementApi; import com.ctrip.framework.apollo.openapi.server.service.EnvOpenApiService; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RestController; import java.util.List; @RestController("openapiEnvController") public class EnvController implements EnvironmentManagementApi { private final EnvOpenApiService envOpenApiService; public EnvController(EnvOpenApiService envOpenApiService) { this.envOpenApiService = envOpenApiService; } @Override public ResponseEntity> getEnvs() { return ResponseEntity.ok(envOpenApiService.getEnvs()); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/v1/controller/InstanceController.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.openapi.v1.controller; import com.ctrip.framework.apollo.openapi.api.InstanceOpenApiService; import org.springframework.web.bind.annotation.*; @RestController("openapiInstanceController") @RequestMapping("/openapi/v1/envs/{env}") public class InstanceController { private final InstanceOpenApiService instanceOpenApiService; public InstanceController(InstanceOpenApiService instanceOpenApiService) { this.instanceOpenApiService = instanceOpenApiService; } @GetMapping(value = "/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/instances") public int getInstanceCountByNamespace(@PathVariable String appId, @PathVariable String env, @PathVariable String clusterName, @PathVariable String namespaceName) { return this.instanceOpenApiService.getInstanceCountByNamespace(appId, env, clusterName, namespaceName); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/v1/controller/ItemController.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.openapi.v1.controller; import com.ctrip.framework.apollo.common.dto.ItemDTO; import com.ctrip.framework.apollo.common.exception.BadRequestException; import com.ctrip.framework.apollo.common.exception.NotFoundException; import com.ctrip.framework.apollo.common.utils.RequestPrecondition; import com.ctrip.framework.apollo.core.utils.StringUtils; import com.ctrip.framework.apollo.openapi.api.ItemOpenApiService; import com.ctrip.framework.apollo.openapi.dto.OpenItemDTO; import com.ctrip.framework.apollo.openapi.dto.OpenPageDTO; import com.ctrip.framework.apollo.portal.environment.Env; import com.ctrip.framework.apollo.portal.service.ItemService; import com.ctrip.framework.apollo.portal.spi.UserService; import java.nio.charset.StandardCharsets; import java.util.Base64; import java.util.Objects; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import jakarta.validation.Valid; import jakarta.validation.constraints.Positive; import jakarta.validation.constraints.PositiveOrZero; @Validated @RestController("openapiItemController") @RequestMapping("/openapi/v1/envs/{env}") public class ItemController { private final ItemService itemService; private final UserService userService; private final ItemOpenApiService itemOpenApiService; private static final int ITEM_COMMENT_MAX_LENGTH = 256; public ItemController(final ItemService itemService, final UserService userService, ItemOpenApiService itemOpenApiService) { this.itemService = itemService; this.userService = userService; this.itemOpenApiService = itemOpenApiService; } @GetMapping( value = "/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/items/{key:.+}") public OpenItemDTO getItem(@PathVariable String appId, @PathVariable String env, @PathVariable String clusterName, @PathVariable String namespaceName, @PathVariable String key) { return this.itemOpenApiService.getItem(appId, env, clusterName, namespaceName, key); } @GetMapping( value = "/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/encodedItems/{key:.+}") public OpenItemDTO getItemByEncodedKey(@PathVariable String appId, @PathVariable String env, @PathVariable String clusterName, @PathVariable String namespaceName, @PathVariable String key) { return this.getItem(appId, env, clusterName, namespaceName, new String(Base64.getDecoder().decode(key.getBytes(StandardCharsets.UTF_8)))); } @PreAuthorize( value = "@unifiedPermissionValidator.hasModifyNamespacePermission(#appId, #env, #clusterName, #namespaceName)") @PostMapping(value = "/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/items") public OpenItemDTO createItem(@PathVariable String appId, @PathVariable String env, @PathVariable String clusterName, @PathVariable String namespaceName, @RequestBody OpenItemDTO item) { RequestPrecondition.checkArguments( !StringUtils.isContainEmpty(item.getKey(), item.getDataChangeCreatedBy()), "key and dataChangeCreatedBy should not be null or empty"); RequestPrecondition.checkArguments(!Objects.isNull(item.getValue()), "value should not be null"); if (userService.findByUserId(item.getDataChangeCreatedBy()) == null) { throw BadRequestException.userNotExists(item.getDataChangeCreatedBy()); } if (!StringUtils.isEmpty(item.getComment()) && item.getComment().length() > ITEM_COMMENT_MAX_LENGTH) { throw new BadRequestException("Comment length should not exceed %s characters", ITEM_COMMENT_MAX_LENGTH); } return this.itemOpenApiService.createItem(appId, env, clusterName, namespaceName, item); } @PreAuthorize( value = "@unifiedPermissionValidator.hasModifyNamespacePermission(#appId, #env, #clusterName, #namespaceName)") @PutMapping( value = "/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/items/{key:.+}") public void updateItem(@PathVariable String appId, @PathVariable String env, @PathVariable String clusterName, @PathVariable String namespaceName, @PathVariable String key, @RequestBody OpenItemDTO item, @RequestParam(defaultValue = "false") boolean createIfNotExists) { RequestPrecondition.checkArguments(item != null, "item payload can not be empty"); RequestPrecondition.checkArguments( !StringUtils.isContainEmpty(item.getKey(), item.getDataChangeLastModifiedBy()), "key and dataChangeLastModifiedBy can not be empty"); RequestPrecondition.checkArguments(item.getKey().equals(key), "Key in path and payload is not consistent"); RequestPrecondition.checkArguments(!Objects.isNull(item.getValue()), "value should not be null"); if (userService.findByUserId(item.getDataChangeLastModifiedBy()) == null) { throw BadRequestException.userNotExists(item.getDataChangeLastModifiedBy()); } if (!StringUtils.isEmpty(item.getComment()) && item.getComment().length() > ITEM_COMMENT_MAX_LENGTH) { throw new BadRequestException("Comment length should not exceed %s characters", ITEM_COMMENT_MAX_LENGTH); } if (createIfNotExists) { if (StringUtils.isEmpty(item.getDataChangeCreatedBy())) { throw new BadRequestException( "dataChangeCreatedBy is required when createIfNotExists is true"); } this.itemOpenApiService.createOrUpdateItem(appId, env, clusterName, namespaceName, item); } else { this.itemOpenApiService.updateItem(appId, env, clusterName, namespaceName, item); } } @PreAuthorize( value = "@unifiedPermissionValidator.hasModifyNamespacePermission(#appId, #env, #clusterName, #namespaceName)") @PutMapping( value = "/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/encodedItems/{key:.+}") public void updateItemByEncodedKey(@PathVariable String appId, @PathVariable String env, @PathVariable String clusterName, @PathVariable String namespaceName, @PathVariable String key, @RequestBody OpenItemDTO item, @RequestParam(defaultValue = "false") boolean createIfNotExists) { this.updateItem(appId, env, clusterName, namespaceName, new String(Base64.getDecoder().decode(key.getBytes(StandardCharsets.UTF_8))), item, createIfNotExists); } @PreAuthorize( value = "@unifiedPermissionValidator.hasModifyNamespacePermission(#appId, #env, #clusterName, #namespaceName)") @DeleteMapping( value = "/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/items/{key:.+}") public void deleteItem(@PathVariable String appId, @PathVariable String env, @PathVariable String clusterName, @PathVariable String namespaceName, @PathVariable String key, @RequestParam String operator) { if (userService.findByUserId(operator) == null) { throw BadRequestException.userNotExists(operator); } ItemDTO toDeleteItem = itemService.loadItem(Env.valueOf(env), appId, clusterName, namespaceName, key); if (toDeleteItem == null) { throw NotFoundException.itemNotFound(appId, clusterName, namespaceName, key); } this.itemOpenApiService.removeItem(appId, env, clusterName, namespaceName, key, operator); } @PreAuthorize( value = "@unifiedPermissionValidator.hasModifyNamespacePermission(#appId, #env, #clusterName, #namespaceName)") @DeleteMapping( value = "/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/encodedItems/{key:.+}") public void deleteItemByEncodedKey(@PathVariable String appId, @PathVariable String env, @PathVariable String clusterName, @PathVariable String namespaceName, @PathVariable String key, @RequestParam String operator) { this.deleteItem(appId, env, clusterName, namespaceName, new String(Base64.getDecoder().decode(key.getBytes(StandardCharsets.UTF_8))), operator); } @GetMapping(value = "/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/items") public OpenPageDTO findItemsByNamespace(@PathVariable String appId, @PathVariable String env, @PathVariable String clusterName, @PathVariable String namespaceName, @Valid @PositiveOrZero(message = "page should be positive or 0") @RequestParam(defaultValue = "0") int page, @Valid @Positive(message = "size should be positive number") @RequestParam(defaultValue = "50") int size) { return this.itemOpenApiService.findItemsByNamespace(appId, env, clusterName, namespaceName, page, size); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/v1/controller/NamespaceBranchController.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.openapi.v1.controller; import com.ctrip.framework.apollo.common.dto.GrayReleaseRuleDTO; import com.ctrip.framework.apollo.common.dto.NamespaceDTO; import com.ctrip.framework.apollo.common.exception.BadRequestException; import com.ctrip.framework.apollo.common.utils.BeanUtils; import com.ctrip.framework.apollo.common.utils.RequestPrecondition; import com.ctrip.framework.apollo.portal.component.UnifiedPermissionValidator; import com.ctrip.framework.apollo.portal.environment.Env; import com.ctrip.framework.apollo.core.utils.StringUtils; import com.ctrip.framework.apollo.openapi.dto.OpenGrayReleaseRuleDTO; import com.ctrip.framework.apollo.openapi.dto.OpenNamespaceDTO; import com.ctrip.framework.apollo.openapi.util.OpenApiBeanUtils; import com.ctrip.framework.apollo.portal.entity.bo.NamespaceBO; import com.ctrip.framework.apollo.portal.service.NamespaceBranchService; import com.ctrip.framework.apollo.portal.service.ReleaseService; import com.ctrip.framework.apollo.portal.spi.UserService; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController("openapiNamespaceBranchController") @RequestMapping("/openapi/v1/envs/{env}") public class NamespaceBranchController { private final UnifiedPermissionValidator unifiedPermissionValidator; private final ReleaseService releaseService; private final NamespaceBranchService namespaceBranchService; private final UserService userService; public NamespaceBranchController(final UnifiedPermissionValidator unifiedPermissionValidator, final ReleaseService releaseService, final NamespaceBranchService namespaceBranchService, final UserService userService) { this.unifiedPermissionValidator = unifiedPermissionValidator; this.releaseService = releaseService; this.namespaceBranchService = namespaceBranchService; this.userService = userService; } @GetMapping(value = "/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/branches") public OpenNamespaceDTO findBranch(@PathVariable String appId, @PathVariable String env, @PathVariable String clusterName, @PathVariable String namespaceName) { NamespaceBO namespaceBO = namespaceBranchService.findBranch(appId, Env.valueOf(env.toUpperCase()), clusterName, namespaceName); if (namespaceBO == null) { return null; } return OpenApiBeanUtils.transformFromNamespaceBO(namespaceBO); } @PreAuthorize(value = "@unifiedPermissionValidator.hasCreateNamespacePermission(#appId)") @PostMapping(value = "/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/branches") public OpenNamespaceDTO createBranch(@PathVariable String appId, @PathVariable String env, @PathVariable String clusterName, @PathVariable String namespaceName, @RequestParam("operator") String operator) { RequestPrecondition.checkArguments(!StringUtils.isContainEmpty(operator), "operator can not be empty"); if (userService.findByUserId(operator) == null) { throw BadRequestException.userNotExists(operator); } NamespaceDTO namespaceDTO = namespaceBranchService.createBranch(appId, Env.valueOf(env.toUpperCase()), clusterName, namespaceName, operator); if (namespaceDTO == null) { return null; } return BeanUtils.transform(OpenNamespaceDTO.class, namespaceDTO); } @PreAuthorize( value = "@unifiedPermissionValidator.hasModifyNamespacePermission(#appId, #env, #clusterName, #namespaceName)") @DeleteMapping( value = "/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/branches/{branchName}") public void deleteBranch(@PathVariable String appId, @PathVariable String env, @PathVariable String clusterName, @PathVariable String namespaceName, @PathVariable String branchName, @RequestParam("operator") String operator) { RequestPrecondition.checkArguments(!StringUtils.isContainEmpty(operator), "operator can not be empty"); if (userService.findByUserId(operator) == null) { throw BadRequestException.userNotExists(operator); } boolean canDelete = unifiedPermissionValidator.hasReleaseNamespacePermission(appId, env, clusterName, namespaceName) || (unifiedPermissionValidator.hasModifyNamespacePermission(appId, env, clusterName, namespaceName) && releaseService.loadLatestRelease(appId, Env.valueOf(env), branchName, namespaceName) == null); if (!canDelete) { throw new AccessDeniedException( "Forbidden operation. " + "Caused by: 1.you don't have release permission " + "or 2. you don't have modification permission " + "or 3. you have modification permission but branch has been released"); } namespaceBranchService.deleteBranch(appId, Env.valueOf(env.toUpperCase()), clusterName, namespaceName, branchName, operator); } @GetMapping( value = "/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/branches/{branchName}/rules") public OpenGrayReleaseRuleDTO getBranchGrayRules(@PathVariable String appId, @PathVariable String env, @PathVariable String clusterName, @PathVariable String namespaceName, @PathVariable String branchName) { GrayReleaseRuleDTO grayReleaseRuleDTO = namespaceBranchService.findBranchGrayRules(appId, Env.valueOf(env.toUpperCase()), clusterName, namespaceName, branchName); if (grayReleaseRuleDTO == null) { return null; } return OpenApiBeanUtils.transformFromGrayReleaseRuleDTO(grayReleaseRuleDTO); } @PreAuthorize( value = "@unifiedPermissionValidator.hasModifyNamespacePermission(#appId, #env, #clusterName, #namespaceName)") @PutMapping( value = "/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/branches/{branchName}/rules") public void updateBranchRules(@PathVariable String appId, @PathVariable String env, @PathVariable String clusterName, @PathVariable String namespaceName, @PathVariable String branchName, @RequestBody OpenGrayReleaseRuleDTO rules, @RequestParam("operator") String operator) { RequestPrecondition.checkArguments(!StringUtils.isContainEmpty(operator), "operator can not be empty"); if (userService.findByUserId(operator) == null) { throw BadRequestException.userNotExists(operator); } rules.setAppId(appId); rules.setClusterName(clusterName); rules.setNamespaceName(namespaceName); rules.setBranchName(branchName); GrayReleaseRuleDTO grayReleaseRuleDTO = OpenApiBeanUtils.transformToGrayReleaseRuleDTO(rules); namespaceBranchService.updateBranchGrayRules(appId, Env.valueOf(env.toUpperCase()), clusterName, namespaceName, branchName, grayReleaseRuleDTO, operator); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/v1/controller/NamespaceController.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.openapi.v1.controller; import com.ctrip.framework.apollo.common.exception.BadRequestException; import com.ctrip.framework.apollo.common.utils.InputValidator; import com.ctrip.framework.apollo.common.utils.RequestPrecondition; import com.ctrip.framework.apollo.core.enums.ConfigFileFormat; import com.ctrip.framework.apollo.openapi.api.NamespaceOpenApiService; import com.ctrip.framework.apollo.openapi.dto.OpenAppNamespaceDTO; import com.ctrip.framework.apollo.openapi.dto.OpenNamespaceDTO; import com.ctrip.framework.apollo.openapi.dto.OpenNamespaceLockDTO; import com.ctrip.framework.apollo.portal.spi.UserService; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.util.List; import java.util.Objects; @RestController("openapiNamespaceController") public class NamespaceController { private final UserService userService; private final NamespaceOpenApiService namespaceOpenApiService; public NamespaceController(final UserService userService, NamespaceOpenApiService namespaceOpenApiService) { this.userService = userService; this.namespaceOpenApiService = namespaceOpenApiService; } @PreAuthorize(value = "@unifiedPermissionValidator.hasCreateNamespacePermission(#appId)") @PostMapping(value = "/openapi/v1/apps/{appId}/appnamespaces") public OpenAppNamespaceDTO createNamespace(@PathVariable String appId, @RequestBody OpenAppNamespaceDTO appNamespaceDTO) { if (!Objects.equals(appId, appNamespaceDTO.getAppId())) { throw new BadRequestException("AppId not equal. AppId in path = %s, AppId in payload = %s", appId, appNamespaceDTO.getAppId()); } RequestPrecondition.checkArgumentsNotEmpty(appNamespaceDTO.getAppId(), appNamespaceDTO.getName(), appNamespaceDTO.getFormat(), appNamespaceDTO.getDataChangeCreatedBy()); if (!InputValidator.isValidAppNamespace(appNamespaceDTO.getName())) { throw BadRequestException .invalidNamespaceFormat(InputValidator.INVALID_CLUSTER_NAMESPACE_MESSAGE + " & " + InputValidator.INVALID_NAMESPACE_NAMESPACE_MESSAGE); } if (!ConfigFileFormat.isValidFormat(appNamespaceDTO.getFormat())) { throw BadRequestException.invalidNamespaceFormat(appNamespaceDTO.getFormat()); } String operator = appNamespaceDTO.getDataChangeCreatedBy(); if (userService.findByUserId(operator) == null) { throw BadRequestException.userNotExists(operator); } return this.namespaceOpenApiService.createAppNamespace(appNamespaceDTO); } @GetMapping(value = "/openapi/v1/envs/{env}/apps/{appId}/clusters/{clusterName}/namespaces") public List findNamespaces(@PathVariable String appId, @PathVariable String env, @PathVariable String clusterName, @RequestParam(defaultValue = "true") boolean fillItemDetail) { return this.namespaceOpenApiService.getNamespaces(appId, env, clusterName, fillItemDetail); } @GetMapping( value = "/openapi/v1/envs/{env}/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName:.+}") public OpenNamespaceDTO loadNamespace(@PathVariable String appId, @PathVariable String env, @PathVariable String clusterName, @PathVariable String namespaceName, @RequestParam(defaultValue = "true") boolean fillItemDetail) { return this.namespaceOpenApiService.getNamespace(appId, env, clusterName, namespaceName, fillItemDetail); } @GetMapping( value = "/openapi/v1/envs/{env}/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/lock") public OpenNamespaceLockDTO getNamespaceLock(@PathVariable String appId, @PathVariable String env, @PathVariable String clusterName, @PathVariable String namespaceName) { return this.namespaceOpenApiService.getNamespaceLock(appId, env, clusterName, namespaceName); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/v1/controller/OrganizationController.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.openapi.v1.controller; import com.ctrip.framework.apollo.openapi.api.OrganizationManagementApi; import com.ctrip.framework.apollo.openapi.server.service.OrganizationOpenApiService; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RestController; import com.ctrip.framework.apollo.openapi.model.OpenOrganizationDto; import java.util.List; @RestController("openapiOrganizationController") public class OrganizationController implements OrganizationManagementApi { private final OrganizationOpenApiService organizationOpenApiService; public OrganizationController(OrganizationOpenApiService organizationOpenApiService) { this.organizationOpenApiService = organizationOpenApiService; } @Override public ResponseEntity> getOrganization() { return ResponseEntity.ok(organizationOpenApiService.getOrganizations()); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/v1/controller/ReleaseController.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.openapi.v1.controller; import com.ctrip.framework.apollo.common.dto.ReleaseDTO; import com.ctrip.framework.apollo.common.exception.BadRequestException; import com.ctrip.framework.apollo.common.utils.BeanUtils; import com.ctrip.framework.apollo.common.utils.RequestPrecondition; import com.ctrip.framework.apollo.openapi.api.ReleaseOpenApiService; import com.ctrip.framework.apollo.portal.component.UnifiedPermissionValidator; import com.ctrip.framework.apollo.portal.environment.Env; import com.ctrip.framework.apollo.core.utils.StringUtils; import com.ctrip.framework.apollo.openapi.dto.NamespaceGrayDelReleaseDTO; import com.ctrip.framework.apollo.openapi.dto.NamespaceReleaseDTO; import com.ctrip.framework.apollo.openapi.dto.OpenReleaseDTO; import com.ctrip.framework.apollo.openapi.util.OpenApiBeanUtils; import com.ctrip.framework.apollo.portal.entity.model.NamespaceGrayDelReleaseModel; import com.ctrip.framework.apollo.portal.entity.model.NamespaceReleaseModel; import com.ctrip.framework.apollo.portal.listener.ConfigPublishEvent; import com.ctrip.framework.apollo.portal.service.NamespaceBranchService; import com.ctrip.framework.apollo.portal.service.ReleaseService; import com.ctrip.framework.apollo.portal.spi.UserService; import org.springframework.context.ApplicationEventPublisher; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController("openapiReleaseController") @RequestMapping("/openapi/v1/envs/{env}") public class ReleaseController { private final ReleaseService releaseService; private final UserService userService; private final NamespaceBranchService namespaceBranchService; private final ReleaseOpenApiService releaseOpenApiService; private final ApplicationEventPublisher publisher; private final UnifiedPermissionValidator unifiedPermissionValidator; public ReleaseController(final ReleaseService releaseService, final UserService userService, final NamespaceBranchService namespaceBranchService, final UnifiedPermissionValidator unifiedPermissionValidator, ReleaseOpenApiService releaseOpenApiService, ApplicationEventPublisher publisher) { this.releaseService = releaseService; this.userService = userService; this.namespaceBranchService = namespaceBranchService; this.unifiedPermissionValidator = unifiedPermissionValidator; this.releaseOpenApiService = releaseOpenApiService; this.publisher = publisher; } @PreAuthorize( value = "@unifiedPermissionValidator.hasReleaseNamespacePermission(#appId, #env, #clusterName, #namespaceName)") @PostMapping(value = "/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/releases") public OpenReleaseDTO createRelease(@PathVariable String appId, @PathVariable String env, @PathVariable String clusterName, @PathVariable String namespaceName, @RequestBody NamespaceReleaseDTO model) { RequestPrecondition.checkArguments( !StringUtils.isContainEmpty(model.getReleasedBy(), model.getReleaseTitle()), "Params(releaseTitle and releasedBy) can not be empty"); if (userService.findByUserId(model.getReleasedBy()) == null) { throw BadRequestException.userNotExists(model.getReleasedBy()); } OpenReleaseDTO releaseDTO = this.releaseOpenApiService.publishNamespace(appId, env, clusterName, namespaceName, model); ConfigPublishEvent event = ConfigPublishEvent.instance(); event.withAppId(appId).withCluster(clusterName).withNamespace(namespaceName) .withReleaseId(releaseDTO.getId()).setNormalPublishEvent(true).setEnv(Env.valueOf(env)); publisher.publishEvent(event); return releaseDTO; } @GetMapping( value = "/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/releases/latest") public OpenReleaseDTO loadLatestActiveRelease(@PathVariable String appId, @PathVariable String env, @PathVariable String clusterName, @PathVariable String namespaceName) { return this.releaseOpenApiService.getLatestActiveRelease(appId, env, clusterName, namespaceName); } @PreAuthorize( value = "@unifiedPermissionValidator.hasReleaseNamespacePermission(#appId, #env, #clusterName, #namespaceName)") @PostMapping( value = "/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/branches/{branchName}/merge") public OpenReleaseDTO merge(@PathVariable String appId, @PathVariable String env, @PathVariable String clusterName, @PathVariable String namespaceName, @PathVariable String branchName, @RequestParam(value = "deleteBranch", defaultValue = "true") boolean deleteBranch, @RequestBody NamespaceReleaseDTO model) { RequestPrecondition.checkArguments( !StringUtils.isContainEmpty(model.getReleasedBy(), model.getReleaseTitle()), "Params(releaseTitle and releasedBy) can not be empty"); if (userService.findByUserId(model.getReleasedBy()) == null) { throw BadRequestException.userNotExists(model.getReleasedBy()); } ReleaseDTO mergedRelease = namespaceBranchService.merge(appId, Env.valueOf(env.toUpperCase()), clusterName, namespaceName, branchName, model.getReleaseTitle(), model.getReleaseComment(), model.isEmergencyPublish(), deleteBranch, model.getReleasedBy()); ConfigPublishEvent event = ConfigPublishEvent.instance(); event.withAppId(appId).withCluster(clusterName).withNamespace(namespaceName) .withReleaseId(mergedRelease.getId()).setMergeEvent(true).setEnv(Env.valueOf(env)); publisher.publishEvent(event); return OpenApiBeanUtils.transformFromReleaseDTO(mergedRelease); } @PreAuthorize( value = "@unifiedPermissionValidator.hasReleaseNamespacePermission(#appId, #env, #clusterName, #namespaceName)") @PostMapping( value = "/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/branches/{branchName}/releases") public OpenReleaseDTO createGrayRelease(@PathVariable String appId, @PathVariable String env, @PathVariable String clusterName, @PathVariable String namespaceName, @PathVariable String branchName, @RequestBody NamespaceReleaseDTO model) { RequestPrecondition.checkArguments( !StringUtils.isContainEmpty(model.getReleasedBy(), model.getReleaseTitle()), "Params(releaseTitle and releasedBy) can not be empty"); if (userService.findByUserId(model.getReleasedBy()) == null) { throw BadRequestException.userNotExists(model.getReleasedBy()); } NamespaceReleaseModel releaseModel = BeanUtils.transform(NamespaceReleaseModel.class, model); releaseModel.setAppId(appId); releaseModel.setEnv(Env.valueOf(env).toString()); releaseModel.setClusterName(branchName); releaseModel.setNamespaceName(namespaceName); ReleaseDTO releaseDTO = releaseService.publish(releaseModel); ConfigPublishEvent event = ConfigPublishEvent.instance(); event.withAppId(appId).withCluster(clusterName).withNamespace(namespaceName) .withReleaseId(releaseDTO.getId()).setGrayPublishEvent(true).setEnv(Env.valueOf(env)); publisher.publishEvent(event); return OpenApiBeanUtils.transformFromReleaseDTO(releaseDTO); } @PreAuthorize( value = "@unifiedPermissionValidator.hasReleaseNamespacePermission(#appId, #env, #clusterName, #namespaceName)") @PostMapping( value = "/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/branches/{branchName}/gray-del-releases") public OpenReleaseDTO createGrayDelRelease(@PathVariable String appId, @PathVariable String env, @PathVariable String clusterName, @PathVariable String namespaceName, @PathVariable String branchName, @RequestBody NamespaceGrayDelReleaseDTO model) { RequestPrecondition.checkArguments( !StringUtils.isContainEmpty(model.getReleasedBy(), model.getReleaseTitle()), "Params(releaseTitle and releasedBy) can not be empty"); RequestPrecondition.checkArguments(model.getGrayDelKeys() != null, "Params(grayDelKeys) can not be null"); if (userService.findByUserId(model.getReleasedBy()) == null) { throw BadRequestException.userNotExists(model.getReleasedBy()); } NamespaceGrayDelReleaseModel releaseModel = BeanUtils.transform(NamespaceGrayDelReleaseModel.class, model); releaseModel.setAppId(appId); releaseModel.setEnv(env.toUpperCase()); releaseModel.setClusterName(branchName); releaseModel.setNamespaceName(namespaceName); return OpenApiBeanUtils.transformFromReleaseDTO( releaseService.publish(releaseModel, releaseModel.getReleasedBy())); } @PutMapping(path = "/releases/{releaseId}/rollback") public void rollback(@PathVariable String env, @PathVariable long releaseId, @RequestParam String operator) { RequestPrecondition.checkArguments(!StringUtils.isContainEmpty(operator), "Param operator can not be empty"); if (userService.findByUserId(operator) == null) { throw BadRequestException.userNotExists(operator); } ReleaseDTO release = releaseService.findReleaseById(Env.valueOf(env), releaseId); if (release == null) { throw new BadRequestException("release not found"); } if (!unifiedPermissionValidator.hasReleaseNamespacePermission(release.getAppId(), env, release.getClusterName(), release.getNamespaceName())) { throw new AccessDeniedException("Forbidden operation. you don't have release permission"); } ConfigPublishEvent event = ConfigPublishEvent.instance(); event.withAppId(release.getAppId()).withCluster(release.getClusterName()) .withNamespace(release.getNamespaceName()).withReleaseId(release.getId()) .setRollbackEvent(true).setEnv(Env.valueOf(env)); publisher.publishEvent(event); this.releaseOpenApiService.rollbackRelease(env, releaseId, operator); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/PortalApplication.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal; import com.ctrip.framework.apollo.common.ApolloCommonConfig; import com.ctrip.framework.apollo.openapi.PortalOpenApiConfig; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.ldap.LdapAutoConfiguration; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.EnableAspectJAutoProxy; import org.springframework.context.annotation.PropertySource; import org.springframework.transaction.annotation.EnableTransactionManagement; @EnableAspectJAutoProxy @Configuration @PropertySource(value = {"classpath:portal.properties"}) @EnableAutoConfiguration(exclude = {LdapAutoConfiguration.class}) @EnableTransactionManagement @ComponentScan(basePackageClasses = {ApolloCommonConfig.class, PortalApplication.class, PortalOpenApiConfig.class}) public class PortalApplication { public static void main(String[] args) throws Exception { SpringApplication.run(PortalApplication.class, args); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/PortalAssemblyConfiguration.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal; import com.ctrip.framework.apollo.common.datasource.ApolloDataSourceScriptDatabaseInitializer; import com.ctrip.framework.apollo.common.datasource.ApolloDataSourceScriptDatabaseInitializerFactory; import com.ctrip.framework.apollo.common.datasource.ApolloSqlInitializationProperties; import javax.sql.DataSource; import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.context.annotation.Profile; @Profile("assembly") @Configuration public class PortalAssemblyConfiguration { @Primary @ConfigurationProperties(prefix = "spring.portal-datasource") @Bean public static DataSourceProperties dataSourceProperties() { return new DataSourceProperties(); } @ConfigurationProperties(prefix = "spring.sql.portal-init") @Bean public static ApolloSqlInitializationProperties apolloSqlInitializationProperties() { return new ApolloSqlInitializationProperties(); } @Bean public static ApolloDataSourceScriptDatabaseInitializer apolloDataSourceScriptDatabaseInitializer( DataSource dataSource, ApolloSqlInitializationProperties properties) { return ApolloDataSourceScriptDatabaseInitializerFactory.create(dataSource, properties); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/ServletInitializer.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; /** * Entry point for traditional web app * * @author Jason Song(song_s@ctrip.com) */ public class ServletInitializer extends SpringBootServletInitializer { @Override protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { return application.sources(PortalApplication.class); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/api/API.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.api; import com.ctrip.framework.apollo.portal.component.RetryableRestTemplate; import org.springframework.beans.factory.annotation.Autowired; public abstract class API { @Autowired protected RetryableRestTemplate restTemplate; } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/api/AdminServiceAPI.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.api; import com.ctrip.framework.apollo.audit.annotation.ApolloAuditLog; import com.ctrip.framework.apollo.audit.annotation.OpType; import com.ctrip.framework.apollo.common.dto.*; import com.ctrip.framework.apollo.openapi.dto.OpenItemDTO; import com.ctrip.framework.apollo.portal.entity.po.ServerConfig; import com.ctrip.framework.apollo.portal.environment.Env; import com.google.common.base.Joiner; import java.nio.charset.StandardCharsets; import java.util.Base64; import org.springframework.boot.actuate.health.Health; import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; @Service public class AdminServiceAPI { @Service public static class HealthAPI extends API { public Health health(Env env) { return restTemplate.get(env, "/health", Health.class); } } @Service public static class AppAPI extends API { public AppDTO loadApp(Env env, String appId) { return restTemplate.get(env, "apps/{appId}", AppDTO.class, appId); } @ApolloAuditLog(type = OpType.RPC, name = "App.createInRemote") public AppDTO createApp(Env env, AppDTO app) { return restTemplate.post(env, "apps", app, AppDTO.class); } @ApolloAuditLog(type = OpType.RPC, name = "App.updateInRemote") public void updateApp(Env env, AppDTO app) { restTemplate.put(env, "apps/{appId}", app, app.getAppId()); } @ApolloAuditLog(type = OpType.RPC, name = "App.deleteInRemote") public void deleteApp(Env env, String appId, String operator) { restTemplate.delete(env, "/apps/{appId}?operator={operator}", appId, operator); } } @Service public static class NamespaceAPI extends API { private ParameterizedTypeReference> namespacePageDTO = new ParameterizedTypeReference>() {}; private ParameterizedTypeReference> typeReference = new ParameterizedTypeReference>() {}; public List findNamespaceByCluster(String appId, Env env, String clusterName) { NamespaceDTO[] namespaceDTOs = restTemplate.get(env, "apps/{appId}/clusters/{clusterName}/namespaces", NamespaceDTO[].class, appId, clusterName); return Arrays.asList(namespaceDTOs); } public PageDTO findByItem(Env env, String itemKey, int page, int size) { ResponseEntity> entity = restTemplate.get(env, "/namespaces/find-by-item?itemKey={itemKey}&page={page}&size={size}", namespacePageDTO, itemKey, page, size); return entity.getBody(); } public NamespaceDTO loadNamespace(String appId, Env env, String clusterName, String namespaceName) { return restTemplate.get(env, "apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}", NamespaceDTO.class, appId, clusterName, namespaceName); } public NamespaceDTO findPublicNamespaceForAssociatedNamespace(Env env, String appId, String clusterName, String namespaceName) { return restTemplate.get(env, "apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/associated-public-namespace", NamespaceDTO.class, appId, clusterName, namespaceName); } @ApolloAuditLog(type = OpType.RPC, name = "Namespace.createInRemote") public NamespaceDTO createNamespace(Env env, NamespaceDTO namespace) { return restTemplate.post(env, "apps/{appId}/clusters/{clusterName}/namespaces", namespace, NamespaceDTO.class, namespace.getAppId(), namespace.getClusterName()); } @ApolloAuditLog(type = OpType.RPC, name = "AppNamespace.createInRemote") public AppNamespaceDTO createAppNamespace(Env env, AppNamespaceDTO appNamespace) { return restTemplate.post(env, "apps/{appId}/appnamespaces", appNamespace, AppNamespaceDTO.class, appNamespace.getAppId()); } @ApolloAuditLog(type = OpType.RPC, name = "AppNamespace.createMissingAppNamespaceInRemote") public AppNamespaceDTO createMissingAppNamespace(Env env, AppNamespaceDTO appNamespace) { return restTemplate.post(env, "apps/{appId}/appnamespaces?silentCreation=true", appNamespace, AppNamespaceDTO.class, appNamespace.getAppId()); } public List getAppNamespaces(String appId, Env env) { AppNamespaceDTO[] appNamespaceDTOs = restTemplate.get(env, "apps/{appId}/appnamespaces", AppNamespaceDTO[].class, appId); return Arrays.asList(appNamespaceDTOs); } @ApolloAuditLog(type = OpType.RPC, name = "Namespace.deleteInRemote") public void deleteNamespace(Env env, String appId, String clusterName, String namespaceName, String operator) { restTemplate.delete(env, "apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}?operator={operator}", appId, clusterName, namespaceName, operator); } public Map getNamespacePublishInfo(Env env, String appId) { return restTemplate.get(env, "apps/{appId}/namespaces/publish_info", typeReference, appId) .getBody(); } public List getPublicAppNamespaceAllNamespaces(Env env, String publicNamespaceName, int page, int size) { NamespaceDTO[] namespaceDTOs = restTemplate.get(env, "/appnamespaces/{publicNamespaceName}/namespaces?page={page}&size={size}", NamespaceDTO[].class, publicNamespaceName, page, size); return Arrays.asList(namespaceDTOs); } public int countPublicAppNamespaceAssociatedNamespaces(Env env, String publicNamesapceName) { Integer count = restTemplate.get(env, "/appnamespaces/{publicNamespaceName}/associated-namespaces/count", Integer.class, publicNamesapceName); return count == null ? 0 : count; } @ApolloAuditLog(type = OpType.RPC, name = "AppNamespace.deleteInRemote") public void deleteAppNamespace(Env env, String appId, String namespaceName, String operator) { restTemplate.delete(env, "/apps/{appId}/appnamespaces/{namespaceName}?operator={operator}", appId, namespaceName, operator); } } @Service public static class ItemAPI extends API { private final ParameterizedTypeReference> openItemPageDTO = new ParameterizedTypeReference>() {}; private final ParameterizedTypeReference> pageItemInfoDTO = new ParameterizedTypeReference>() {}; public List findItems(String appId, Env env, String clusterName, String namespaceName) { ItemDTO[] itemDTOs = restTemplate.get(env, "apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/items", ItemDTO[].class, appId, clusterName, namespaceName); return Arrays.asList(itemDTOs); } public List findDeletedItems(String appId, Env env, String clusterName, String namespaceName) { ItemDTO[] itemDTOs = restTemplate.get(env, "apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/items/deleted", ItemDTO[].class, appId, clusterName, namespaceName); return Arrays.asList(itemDTOs); } public PageDTO getPerEnvItemInfoBySearch(Env env, String key, String value, int page, int size) { ResponseEntity> entity = restTemplate.get(env, "items-search/key-and-value?key={key}&value={value}&page={page}&size={size}", pageItemInfoDTO, key, value, page, size); return entity.getBody(); } public ItemDTO loadItem(Env env, String appId, String clusterName, String namespaceName, String key) { return restTemplate.get(env, "apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/items/{key}", ItemDTO.class, appId, clusterName, namespaceName, key); } public ItemDTO loadItemByEncodeKey(Env env, String appId, String clusterName, String namespaceName, String key) { return restTemplate.get(env, "apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/encodedItems/{key}", ItemDTO.class, appId, clusterName, namespaceName, new String(Base64.getEncoder().encode(key.getBytes(StandardCharsets.UTF_8)))); } public ItemDTO loadItemById(Env env, long itemId) { return restTemplate.get(env, "items/{itemId}", ItemDTO.class, itemId); } public void updateItemsByChangeSet(String appId, Env env, String clusterName, String namespace, ItemChangeSets changeSets) { restTemplate.post(env, "apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/itemset", changeSets, Void.class, appId, clusterName, namespace); } public void updateItem(String appId, Env env, String clusterName, String namespace, long itemId, ItemDTO item) { restTemplate.put(env, "apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/items/{itemId}", item, appId, clusterName, namespace, itemId); } public ItemDTO createItem(String appId, Env env, String clusterName, String namespace, ItemDTO item) { return restTemplate.post(env, "apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/items", item, ItemDTO.class, appId, clusterName, namespace); } public ItemDTO createCommentItem(String appId, Env env, String clusterName, String namespace, ItemDTO item) { return restTemplate.post(env, "apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/comment_items", item, ItemDTO.class, appId, clusterName, namespace); } public void deleteItem(Env env, long itemId, String operator) { restTemplate.delete(env, "items/{itemId}?operator={operator}", itemId, operator); } public PageDTO findItemsByNamespace(String appId, Env env, String clusterName, String namespaceName, int page, int size) { ResponseEntity> entity = restTemplate.get(env, "/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/items-with-page?page={page}&size={size}", openItemPageDTO, appId, clusterName, namespaceName, page, size); return entity.getBody(); } } @Service public static class ClusterAPI extends API { public List findClustersByApp(String appId, Env env) { ClusterDTO[] clusterDTOs = restTemplate.get(env, "apps/{appId}/clusters", ClusterDTO[].class, appId); return Arrays.asList(clusterDTOs); } public ClusterDTO loadCluster(String appId, Env env, String clusterName) { return restTemplate.get(env, "apps/{appId}/clusters/{clusterName}", ClusterDTO.class, appId, clusterName); } public boolean isClusterUnique(String appId, Env env, String clusterName) { return restTemplate.get(env, "apps/{appId}/cluster/{clusterName}/unique", Boolean.class, appId, clusterName); } @ApolloAuditLog(type = OpType.RPC, name = "Cluster.createInRemote") public ClusterDTO create(Env env, ClusterDTO cluster) { return restTemplate.post(env, "apps/{appId}/clusters", cluster, ClusterDTO.class, cluster.getAppId()); } @ApolloAuditLog(type = OpType.RPC, name = "Cluster.deleteInRemote") public void delete(Env env, String appId, String clusterName, String operator) { restTemplate.delete(env, "apps/{appId}/clusters/{clusterName}?operator={operator}", appId, clusterName, operator); } } @Service public static class AccessKeyAPI extends API { @ApolloAuditLog(type = OpType.RPC, name = "AccessKey.createInRemote") public AccessKeyDTO create(Env env, AccessKeyDTO accessKey) { return restTemplate.post(env, "apps/{appId}/accesskeys", accessKey, AccessKeyDTO.class, accessKey.getAppId()); } public List findByAppId(Env env, String appId) { AccessKeyDTO[] accessKeys = restTemplate.get(env, "apps/{appId}/accesskeys", AccessKeyDTO[].class, appId); return Arrays.asList(accessKeys); } @ApolloAuditLog(type = OpType.RPC, name = "AccessKey.deleteInRemote") public void delete(Env env, String appId, long id, String operator) { restTemplate.delete(env, "apps/{appId}/accesskeys/{id}?operator={operator}", appId, id, operator); } @ApolloAuditLog(type = OpType.RPC, name = "AccessKey.enableInRemote") public void enable(Env env, String appId, long id, int mode, String operator) { restTemplate.put(env, "apps/{appId}/accesskeys/{id}/enable?mode={mode}&operator={operator}", null, appId, id, mode, operator); } @ApolloAuditLog(type = OpType.RPC, name = "AccessKey.disableInRemote") public void disable(Env env, String appId, long id, String operator) { restTemplate.put(env, "apps/{appId}/accesskeys/{id}/disable?operator={operator}", null, appId, id, operator); } } @Service public static class ReleaseAPI extends API { private static final Joiner JOINER = Joiner.on(","); public ReleaseDTO loadRelease(Env env, long releaseId) { return restTemplate.get(env, "releases/{releaseId}", ReleaseDTO.class, releaseId); } public List findReleaseByIds(Env env, Set releaseIds) { if (CollectionUtils.isEmpty(releaseIds)) { return Collections.emptyList(); } ReleaseDTO[] releases = restTemplate.get(env, "/releases?releaseIds={releaseIds}", ReleaseDTO[].class, JOINER.join(releaseIds)); return Arrays.asList(releases); } public List findAllReleases(String appId, Env env, String clusterName, String namespaceName, int page, int size) { ReleaseDTO[] releaseDTOs = restTemplate.get(env, "apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/releases/all?page={page}&size={size}", ReleaseDTO[].class, appId, clusterName, namespaceName, page, size); return Arrays.asList(releaseDTOs); } public List findActiveReleases(String appId, Env env, String clusterName, String namespaceName, int page, int size) { ReleaseDTO[] releaseDTOs = restTemplate.get(env, "apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/releases/active?page={page}&size={size}", ReleaseDTO[].class, appId, clusterName, namespaceName, page, size); return Arrays.asList(releaseDTOs); } public ReleaseDTO loadLatestRelease(String appId, Env env, String clusterName, String namespace) { return restTemplate.get(env, "apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/releases/latest", ReleaseDTO.class, appId, clusterName, namespace); } public ReleaseDTO createRelease(String appId, Env env, String clusterName, String namespace, String releaseName, String releaseComment, String operator, boolean isEmergencyPublish) { HttpHeaders headers = new HttpHeaders(); headers.setContentType( MediaType.parseMediaType(MediaType.APPLICATION_FORM_URLENCODED_VALUE + ";charset=UTF-8")); MultiValueMap parameters = new LinkedMultiValueMap<>(); parameters.add("name", releaseName); parameters.add("comment", releaseComment); parameters.add("operator", operator); parameters.add("isEmergencyPublish", String.valueOf(isEmergencyPublish)); HttpEntity> entity = new HttpEntity<>(parameters, headers); return restTemplate.post(env, "apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/releases", entity, ReleaseDTO.class, appId, clusterName, namespace); } public ReleaseDTO createGrayDeletionRelease(String appId, Env env, String clusterName, String namespace, String releaseName, String releaseComment, String operator, boolean isEmergencyPublish, Set grayDelKeys) { HttpHeaders headers = new HttpHeaders(); headers.setContentType( MediaType.parseMediaType(MediaType.APPLICATION_FORM_URLENCODED_VALUE + ";charset=UTF-8")); MultiValueMap parameters = new LinkedMultiValueMap<>(); parameters.add("releaseName", releaseName); parameters.add("comment", releaseComment); parameters.add("operator", operator); parameters.add("isEmergencyPublish", String.valueOf(isEmergencyPublish)); grayDelKeys.forEach(key -> parameters.add("grayDelKeys", key)); HttpEntity> entity = new HttpEntity<>(parameters, headers); return restTemplate.post(env, "apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/gray-del-releases", entity, ReleaseDTO.class, appId, clusterName, namespace); } public ReleaseDTO updateAndPublish(String appId, Env env, String clusterName, String namespace, String releaseName, String releaseComment, String branchName, boolean isEmergencyPublish, boolean deleteBranch, ItemChangeSets changeSets) { return restTemplate.post(env, "apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/updateAndPublish?" + "releaseName={releaseName}&releaseComment={releaseComment}&branchName={branchName}" + "&deleteBranch={deleteBranch}&isEmergencyPublish={isEmergencyPublish}", changeSets, ReleaseDTO.class, appId, clusterName, namespace, releaseName, releaseComment, branchName, deleteBranch, isEmergencyPublish); } public void rollback(Env env, long releaseId, String operator) { restTemplate.put(env, "releases/{releaseId}/rollback?operator={operator}", null, releaseId, operator); } public void rollbackTo(Env env, long releaseId, long toReleaseId, String operator) { restTemplate.put(env, "releases/{releaseId}/rollback?toReleaseId={toReleaseId}&operator={operator}", null, releaseId, toReleaseId, operator); } } @Service public static class CommitAPI extends API { public List find(String appId, Env env, String clusterName, String namespaceName, int page, int size) { CommitDTO[] commitDTOs = restTemplate.get(env, "apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/commit?page={page}&size={size}", CommitDTO[].class, appId, clusterName, namespaceName, page, size); return Arrays.asList(commitDTOs); } public List findByKey(String appId, Env env, String clusterName, String namespaceName, String key, int page, int size) { CommitDTO[] commitDTOs = restTemplate.get(env, "apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/commit?key={key}&page={page}&size={size}", CommitDTO[].class, appId, clusterName, namespaceName, key, page, size); return Arrays.asList(commitDTOs); } } @Service public static class NamespaceLockAPI extends API { public NamespaceLockDTO getNamespaceLockOwner(String appId, Env env, String clusterName, String namespaceName) { return restTemplate.get(env, "apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/lock", NamespaceLockDTO.class, appId, clusterName, namespaceName); } } @Service public static class InstanceAPI extends API { private Joiner joiner = Joiner.on(","); private ParameterizedTypeReference> pageInstanceDtoType = new ParameterizedTypeReference>() {}; public PageDTO getByRelease(Env env, long releaseId, int page, int size) { ResponseEntity> entity = restTemplate.get(env, "/instances/by-release?releaseId={releaseId}&page={page}&size={size}", pageInstanceDtoType, releaseId, page, size); return entity.getBody(); } public List getByReleasesNotIn(String appId, Env env, String clusterName, String namespaceName, Set releaseIds) { InstanceDTO[] instanceDTOs = restTemplate.get(env, "/instances/by-namespace-and-releases-not-in?appId={appId}&clusterName={clusterName}&namespaceName={namespaceName}&releaseIds={releaseIds}", InstanceDTO[].class, appId, clusterName, namespaceName, joiner.join(releaseIds)); return Arrays.asList(instanceDTOs); } public PageDTO getByNamespace(String appId, Env env, String clusterName, String namespaceName, String instanceAppId, int page, int size) { ResponseEntity> entity = restTemplate.get(env, "/instances/by-namespace?appId={appId}" + "&clusterName={clusterName}&namespaceName={namespaceName}&instanceAppId={instanceAppId}" + "&page={page}&size={size}", pageInstanceDtoType, appId, clusterName, namespaceName, instanceAppId, page, size); return entity.getBody(); } public int getInstanceCountByNamespace(String appId, Env env, String clusterName, String namespaceName) { Integer count = restTemplate.get(env, "/instances/by-namespace/count?appId={appId}&clusterName={clusterName}&namespaceName={namespaceName}", Integer.class, appId, clusterName, namespaceName); if (count == null) { return 0; } return count; } } @Service public static class NamespaceBranchAPI extends API { @ApolloAuditLog(type = OpType.RPC, name = "NamespaceBranch.createInRemote") public NamespaceDTO createBranch(String appId, Env env, String clusterName, String namespaceName, String operator) { return restTemplate.post(env, "/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/branches?operator={operator}", null, NamespaceDTO.class, appId, clusterName, namespaceName, operator); } public NamespaceDTO findBranch(String appId, Env env, String clusterName, String namespaceName) { return restTemplate.get(env, "/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/branches", NamespaceDTO.class, appId, clusterName, namespaceName); } public GrayReleaseRuleDTO findBranchGrayRules(String appId, Env env, String clusterName, String namespaceName, String branchName) { return restTemplate.get(env, "/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/branches/{branchName}/rules", GrayReleaseRuleDTO.class, appId, clusterName, namespaceName, branchName); } @ApolloAuditLog(type = OpType.RPC, name = "NamespaceBranch.updateInRemote") public void updateBranchGrayRules(String appId, Env env, String clusterName, String namespaceName, String branchName, GrayReleaseRuleDTO rules) { restTemplate.put(env, "/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/branches/{branchName}/rules", rules, appId, clusterName, namespaceName, branchName); } @ApolloAuditLog(type = OpType.RPC, name = "NamespaceBranch.deleteInRemote") public void deleteBranch(String appId, Env env, String clusterName, String namespaceName, String branchName, String operator) { restTemplate.delete(env, "/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/branches/{branchName}?operator={operator}", appId, clusterName, namespaceName, branchName, operator); } } @Service public static class ReleaseHistoryAPI extends API { private ParameterizedTypeReference> type = new ParameterizedTypeReference>() {}; public PageDTO findReleaseHistoriesByNamespace(String appId, Env env, String clusterName, String namespaceName, int page, int size) { return restTemplate.get(env, "/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/releases/histories?page={page}&size={size}", type, appId, clusterName, namespaceName, page, size).getBody(); } public PageDTO findByReleaseIdAndOperation(Env env, long releaseId, int operation, int page, int size) { return restTemplate.get(env, "/releases/histories/by_release_id_and_operation?releaseId={releaseId}&operation={operation}&page={page}&size={size}", type, releaseId, operation, page, size).getBody(); } public PageDTO findByPreviousReleaseIdAndOperation(Env env, long previousReleaseId, int operation, int page, int size) { return restTemplate.get(env, "/releases/histories/by_previous_release_id_and_operation?previousReleaseId={releaseId}&operation={operation}&page={page}&size={size}", type, previousReleaseId, operation, page, size).getBody(); } } @Service public static class ServerConfigAPI extends API { public List findAllConfigDBConfig(Env env) { return restTemplate.get(env, "/server/config/find-all-config", new ParameterizedTypeReference>() {}).getBody(); } @ApolloAuditLog(type = OpType.RPC, name = "ServerConfig.createOrUpdateConfigDBConfigInRemote") public ServerConfig createOrUpdateConfigDBConfig(Env env, ServerConfig serverConfig) { return restTemplate.post(env, "/server/config", serverConfig, ServerConfig.class); } } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/audit/ApolloAuditLogQueryApiPortalPreAuthorizer.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.audit; import com.ctrip.framework.apollo.audit.spi.ApolloAuditLogQueryApiPreAuthorizer; import com.ctrip.framework.apollo.portal.component.UnifiedPermissionValidator; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Component; @Component("apolloAuditLogQueryApiPreAuthorizer") @ConditionalOnProperty(prefix = "apollo.audit.log", name = "enabled", havingValue = "true") public class ApolloAuditLogQueryApiPortalPreAuthorizer implements ApolloAuditLogQueryApiPreAuthorizer { private final UnifiedPermissionValidator unifiedPermissionValidator; public ApolloAuditLogQueryApiPortalPreAuthorizer( UnifiedPermissionValidator unifiedPermissionValidator) { this.unifiedPermissionValidator = unifiedPermissionValidator; } @Override public boolean hasQueryPermission() { return unifiedPermissionValidator.isSuperAdmin(); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/audit/ApolloAuditOperatorPortalSupplier.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.audit; import com.ctrip.framework.apollo.audit.spi.ApolloAuditOperatorSupplier; import com.ctrip.framework.apollo.portal.spi.UserInfoHolder; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Component; @Component @ConditionalOnProperty(prefix = "apollo.audit.log", name = "enabled", havingValue = "true") public class ApolloAuditOperatorPortalSupplier implements ApolloAuditOperatorSupplier { private final ObjectProvider userInfoHolderProvider; public ApolloAuditOperatorPortalSupplier(ObjectProvider userInfoHolderProvider) { this.userInfoHolderProvider = userInfoHolderProvider; } @Override public String getOperator() { UserInfoHolder userInfoHolder = userInfoHolderProvider.getIfAvailable(); return userInfoHolder == null ? null : userInfoHolder.getUser().getName(); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/component/AbstractPermissionValidator.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.component; import com.ctrip.framework.apollo.common.entity.AppNamespace; import com.ctrip.framework.apollo.common.exception.BadRequestException; import com.ctrip.framework.apollo.portal.constant.PermissionType; import com.ctrip.framework.apollo.portal.entity.po.Permission; import com.ctrip.framework.apollo.portal.environment.Env; import com.ctrip.framework.apollo.portal.util.RoleUtils; import java.util.Arrays; import java.util.Collections; import java.util.List; public abstract class AbstractPermissionValidator implements PermissionValidator { @Override public boolean hasModifyNamespacePermission(String appId, String env, String clusterName, String namespaceName) { // Normalize env to ensure consistent permission target ID construction String normalizedEnv = normalizeEnv(env); List requiredPermissions = Arrays.asList( new Permission(PermissionType.MODIFY_NAMESPACE, RoleUtils.buildNamespaceTargetId(appId, namespaceName)), new Permission(PermissionType.MODIFY_NAMESPACE, RoleUtils.buildNamespaceTargetId(appId, namespaceName, normalizedEnv)), new Permission(PermissionType.MODIFY_NAMESPACES_IN_CLUSTER, RoleUtils.buildClusterTargetId(appId, normalizedEnv, clusterName))); return hasPermissions(requiredPermissions); } @Override public boolean hasReleaseNamespacePermission(String appId, String env, String clusterName, String namespaceName) { // Normalize env to ensure consistent permission target ID construction String normalizedEnv = normalizeEnv(env); List requiredPermissions = Arrays.asList( new Permission(PermissionType.RELEASE_NAMESPACE, RoleUtils.buildNamespaceTargetId(appId, namespaceName)), new Permission(PermissionType.RELEASE_NAMESPACE, RoleUtils.buildNamespaceTargetId(appId, namespaceName, normalizedEnv)), new Permission(PermissionType.RELEASE_NAMESPACES_IN_CLUSTER, RoleUtils.buildClusterTargetId(appId, normalizedEnv, clusterName))); return hasPermissions(requiredPermissions); } @Override public boolean hasAssignRolePermission(String appId) { List requiredPermissions = Collections.singletonList(new Permission(PermissionType.ASSIGN_ROLE, appId)); return hasPermissions(requiredPermissions); } @Override public boolean hasCreateNamespacePermission(String appId) { List requiredPermissions = Collections.singletonList(new Permission(PermissionType.CREATE_NAMESPACE, appId)); return hasPermissions(requiredPermissions); } @Override public abstract boolean hasCreateAppNamespacePermission(String appId, AppNamespace appNamespace); @Override public boolean hasCreateClusterPermission(String appId) { List requiredPermissions = Collections.singletonList(new Permission(PermissionType.CREATE_CLUSTER, appId)); return hasPermissions(requiredPermissions); } @Override public abstract boolean isSuperAdmin(); @Override public boolean shouldHideConfigToCurrentUser(String appId, String env, String clusterName, String namespaceName) { return false; } @Override public abstract boolean hasCreateApplicationPermission(); @Override public abstract boolean hasManageAppMasterPermission(String appId); protected abstract boolean hasPermissions(List requiredPerms); /** * Normalize the env name to ensure consistency between UI display and permission control. * For example, "prod" -> "PROD" -> "PRO" via {@link Env#transformEnv(String)}. * * @param env the raw environment name * @return the normalized environment name * @throws BadRequestException if the env name is invalid * @see #5442 */ protected String normalizeEnv(String env) { Env transformedEnv = Env.transformEnv(env); if (Env.UNKNOWN == transformedEnv) { throw BadRequestException.invalidEnvFormat(env); } return transformedEnv.getName(); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/component/AdminServiceAddressLocator.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.component; import com.ctrip.framework.apollo.portal.component.config.PortalConfig; import com.ctrip.framework.apollo.portal.environment.PortalMetaDomainService; import com.ctrip.framework.apollo.core.dto.ServiceDTO; import com.ctrip.framework.apollo.portal.environment.Env; import com.ctrip.framework.apollo.core.utils.ApolloThreadFactory; import com.ctrip.framework.apollo.tracer.Tracer; import com.google.common.collect.Lists; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.autoconfigure.http.HttpMessageConverters; import org.springframework.stereotype.Component; import org.springframework.util.CollectionUtils; import org.springframework.web.client.RestTemplate; import jakarta.annotation.PostConstruct; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; @Component public class AdminServiceAddressLocator { private static final int RETRY_TIMES = 3; private static final String ADMIN_SERVICE_URL_PATH = "/services/admin"; private static final Logger logger = LoggerFactory.getLogger(AdminServiceAddressLocator.class); private ScheduledExecutorService refreshServiceAddressService; private RestTemplate restTemplate; private List allEnvs; private Map> cache = new ConcurrentHashMap<>(); private final PortalSettings portalSettings; private final RestTemplateFactory restTemplateFactory; private final PortalMetaDomainService portalMetaDomainService; private final PortalConfig portalConfig; public AdminServiceAddressLocator(final HttpMessageConverters httpMessageConverters, final PortalSettings portalSettings, final RestTemplateFactory restTemplateFactory, final PortalMetaDomainService portalMetaDomainService, final PortalConfig portalConfig) { this.portalSettings = portalSettings; this.restTemplateFactory = restTemplateFactory; this.portalMetaDomainService = portalMetaDomainService; this.portalConfig = portalConfig; } @PostConstruct public void init() { allEnvs = portalSettings.getAllEnvs(); // init restTemplate restTemplate = restTemplateFactory.getObject(); refreshServiceAddressService = Executors.newScheduledThreadPool(1, ApolloThreadFactory.create("ServiceLocator", true)); refreshServiceAddressService.schedule(new RefreshAdminServerAddressTask(), 1, TimeUnit.MILLISECONDS); } public List getServiceList(Env env) { List services = cache.get(env); if (CollectionUtils.isEmpty(services)) { return Collections.emptyList(); } List randomConfigServices = Lists.newArrayList(services); Collections.shuffle(randomConfigServices); return randomConfigServices; } // maintain admin server address private class RefreshAdminServerAddressTask implements Runnable { @Override public void run() { boolean refreshSuccess = true; // refresh fail if get any env address fail for (Env env : allEnvs) { boolean currentEnvRefreshResult = refreshServerAddressCache(env); refreshSuccess = refreshSuccess && currentEnvRefreshResult; } if (refreshSuccess) { refreshServiceAddressService.schedule(new RefreshAdminServerAddressTask(), portalConfig.refreshAdminServerAddressTaskNormalIntervalSecond(), TimeUnit.SECONDS); } else { refreshServiceAddressService.schedule(new RefreshAdminServerAddressTask(), portalConfig.refreshAdminServerAddressTaskOfflineIntervalSecond(), TimeUnit.SECONDS); } } } private boolean refreshServerAddressCache(Env env) { for (int i = 0; i < RETRY_TIMES; i++) { try { ServiceDTO[] services = getAdminServerAddress(env); if (services == null || services.length == 0) { continue; } cache.put(env, Arrays.asList(services)); return true; } catch (Throwable e) { logger.error( "Get admin server address from meta server failed. env: {}, meta server address:{}", env, portalMetaDomainService.getDomain(env), e); Tracer.logError(String.format( "Get admin server address from meta server failed. env: %s, meta server address:%s", env, portalMetaDomainService.getDomain(env)), e); } } return false; } private ServiceDTO[] getAdminServerAddress(Env env) { String domainName = portalMetaDomainService.getDomain(env); String url = domainName + ADMIN_SERVICE_URL_PATH; return restTemplate.getForObject(url, ServiceDTO[].class); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/component/ConfigReleaseWebhookNotifier.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.component; import jakarta.annotation.PostConstruct; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.stereotype.Component; import org.springframework.web.client.RestTemplate; import com.ctrip.framework.apollo.portal.entity.bo.ReleaseHistoryBO; import com.ctrip.framework.apollo.portal.environment.Env; /** * publish webHook * * @author HuangSheng */ @Component public class ConfigReleaseWebhookNotifier { private static final Logger logger = LoggerFactory.getLogger(ConfigReleaseWebhookNotifier.class); private final RestTemplateFactory restTemplateFactory; private RestTemplate restTemplate; public ConfigReleaseWebhookNotifier(RestTemplateFactory restTemplateFactory) { this.restTemplateFactory = restTemplateFactory; } @PostConstruct public void init() { // init restTemplate restTemplate = restTemplateFactory.getObject(); } public void notify(String[] webHookUrls, Env env, ReleaseHistoryBO releaseHistory) { if (webHookUrls == null) { return; } for (String webHookUrl : webHookUrls) { HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON_UTF8); HttpEntity entity = new HttpEntity(releaseHistory, headers); String url = webHookUrl + "?env={env}"; try { restTemplate.postForObject(url, entity, String.class, env); } catch (Exception e) { logger.error("Notify webHook server failed, env: {}, webHook server url:{}", env, url, e); } } } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/component/ItemsComparator.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.component; import com.ctrip.framework.apollo.common.dto.ItemChangeSets; import com.ctrip.framework.apollo.common.dto.ItemDTO; import com.ctrip.framework.apollo.common.utils.BeanUtils; import com.ctrip.framework.apollo.core.utils.StringUtils; import org.springframework.stereotype.Component; import org.springframework.util.CollectionUtils; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Objects; @Component public class ItemsComparator { public ItemChangeSets compareIgnoreBlankAndCommentItem(long baseNamespaceId, List baseItems, List targetItems) { List filteredSourceItems = filterBlankAndCommentItem(baseItems); List filteredTargetItems = filterBlankAndCommentItem(targetItems); Map sourceItemMap = BeanUtils.mapByKey("key", filteredSourceItems); Map targetItemMap = BeanUtils.mapByKey("key", filteredTargetItems); ItemChangeSets changeSets = new ItemChangeSets(); for (ItemDTO item : targetItems) { String key = item.getKey(); ItemDTO sourceItem = sourceItemMap.get(key); if (sourceItem == null) {// add ItemDTO copiedItem = copyItem(item); copiedItem.setNamespaceId(baseNamespaceId); changeSets.addCreateItem(copiedItem); } else if (!Objects.equals(sourceItem.getValue(), item.getValue())) {// update // only type & value & comment can be update sourceItem.setType(item.getType()); sourceItem.setValue(item.getValue()); sourceItem.setComment(item.getComment()); changeSets.addUpdateItem(sourceItem); } } for (ItemDTO item : baseItems) { String key = item.getKey(); ItemDTO targetItem = targetItemMap.get(key); if (targetItem == null) {// delete changeSets.addDeleteItem(item); } } return changeSets; } private List filterBlankAndCommentItem(List items) { List result = new LinkedList<>(); if (CollectionUtils.isEmpty(items)) { return result; } for (ItemDTO item : items) { if (!StringUtils.isEmpty(item.getKey())) { result.add(item); } } return result; } private ItemDTO copyItem(ItemDTO sourceItem) { ItemDTO copiedItem = new ItemDTO(); copiedItem.setKey(sourceItem.getKey()); copiedItem.setType(sourceItem.getType()); copiedItem.setValue(sourceItem.getValue()); copiedItem.setComment(sourceItem.getComment()); return copiedItem; } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/component/PermissionValidator.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.component; import com.ctrip.framework.apollo.common.entity.AppNamespace; public interface PermissionValidator { boolean hasModifyNamespacePermission(String appId, String env, String clusterName, String namespaceName); boolean hasReleaseNamespacePermission(String appId, String env, String clusterName, String namespaceName); default boolean hasDeleteNamespacePermission(String appId) { return hasAssignRolePermission(appId) || isSuperAdmin(); } default boolean hasOperateNamespacePermission(String appId, String env, String clusterName, String namespaceName) { return hasModifyNamespacePermission(appId, env, clusterName, namespaceName) || hasReleaseNamespacePermission(appId, env, clusterName, namespaceName); } boolean hasAssignRolePermission(String appId); boolean hasCreateNamespacePermission(String appId); boolean hasCreateAppNamespacePermission(String appId, AppNamespace appNamespace); boolean hasCreateClusterPermission(String appId); default boolean isAppAdmin(String appId) { return isSuperAdmin() || hasAssignRolePermission(appId); } boolean isSuperAdmin(); boolean shouldHideConfigToCurrentUser(String appId, String env, String clusterName, String namespaceName); boolean hasCreateApplicationPermission(); boolean hasCreateApplicationPermission(String userId); boolean hasManageAppMasterPermission(String appId); } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/component/PortalSettings.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.component; import com.ctrip.framework.apollo.portal.environment.PortalMetaDomainService; import com.ctrip.framework.apollo.portal.environment.Env; import com.ctrip.framework.apollo.core.utils.ApolloThreadFactory; import com.ctrip.framework.apollo.portal.api.AdminServiceAPI; import com.ctrip.framework.apollo.portal.component.config.PortalConfig; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.actuate.health.Health; import org.springframework.context.ApplicationContext; import org.springframework.stereotype.Component; import jakarta.annotation.PostConstruct; import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; @Component public class PortalSettings { private static final Logger logger = LoggerFactory.getLogger(PortalSettings.class); private static final int HEALTH_CHECK_INTERVAL = 10 * 1000; private final ApplicationContext applicationContext; private final PortalConfig portalConfig; private final PortalMetaDomainService portalMetaDomainService; private List allEnvs = new ArrayList<>(); // mark env up or down private Map envStatusMark = new ConcurrentHashMap<>(); public PortalSettings(final ApplicationContext applicationContext, final PortalConfig portalConfig, final PortalMetaDomainService portalMetaDomainService) { this.applicationContext = applicationContext; this.portalConfig = portalConfig; this.portalMetaDomainService = portalMetaDomainService; } @PostConstruct private void postConstruct() { allEnvs = portalConfig.portalSupportedEnvs(); for (Env env : allEnvs) { envStatusMark.put(env, true); } ScheduledExecutorService healthCheckService = Executors.newScheduledThreadPool(1, ApolloThreadFactory.create("EnvHealthChecker", true)); healthCheckService.scheduleWithFixedDelay(new HealthCheckTask(applicationContext), 1000, HEALTH_CHECK_INTERVAL, TimeUnit.MILLISECONDS); } public List getAllEnvs() { return allEnvs; } public List getActiveEnvs() { List activeEnvs = new LinkedList<>(); for (Env env : allEnvs) { if (envStatusMark.get(env)) { activeEnvs.add(env); } } return activeEnvs; } public boolean isEnvActive(Env env) { Boolean mark = envStatusMark.get(env); return mark != null && mark; } private class HealthCheckTask implements Runnable { private static final int ENV_DOWN_THRESHOLD = 2; private Map healthCheckFailedCounter = new HashMap<>(); private AdminServiceAPI.HealthAPI healthAPI; public HealthCheckTask(ApplicationContext context) { healthAPI = context.getBean(AdminServiceAPI.HealthAPI.class); for (Env env : allEnvs) { healthCheckFailedCounter.put(env, 0); } } @Override public void run() { for (Env env : allEnvs) { try { if (isUp(env)) { // revive if (!envStatusMark.get(env)) { envStatusMark.put(env, true); healthCheckFailedCounter.put(env, 0); logger.info("Env revived because env health check success. env: {}", env); } } else { logger.error( "Env health check failed, maybe because of admin server down. env: {}, meta server address: {}", env, portalMetaDomainService.getDomain(env)); handleEnvDown(env); } } catch (Exception e) { logger.error( "Env health check failed, maybe because of meta server down " + "or configure wrong meta server address. env: {}, meta server address: {}", env, portalMetaDomainService.getDomain(env), e); handleEnvDown(env); } } } private boolean isUp(Env env) { Health health = healthAPI.health(env); return "UP".equals(health.getStatus().getCode()); } private void handleEnvDown(Env env) { int failedTimes = healthCheckFailedCounter.get(env); healthCheckFailedCounter.put(env, ++failedTimes); if (!envStatusMark.get(env)) { logger.error("Env is down. env: {}, failed times: {}, meta server address: {}", env, failedTimes, portalMetaDomainService.getDomain(env)); } else { if (failedTimes >= ENV_DOWN_THRESHOLD) { envStatusMark.put(env, false); logger.error( "Env is down because health check failed for {} times, " + "which equals to down threshold. env: {}, meta server address: {}", ENV_DOWN_THRESHOLD, env, portalMetaDomainService.getDomain(env)); } else { logger.error( "Env health check failed for {} times which less than down threshold. down threshold:{}, env: {}, meta server address: {}", failedTimes, ENV_DOWN_THRESHOLD, env, portalMetaDomainService.getDomain(env)); } } } } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/component/RestTemplateFactory.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.component; import com.ctrip.framework.apollo.audit.component.ApolloAuditHttpInterceptor; import com.ctrip.framework.apollo.portal.component.config.PortalConfig; import java.util.concurrent.TimeUnit; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.hc.client5.http.impl.classic.HttpClients; import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager; import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder; import org.apache.hc.core5.util.TimeValue; import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.InitializingBean; import org.springframework.boot.autoconfigure.http.HttpMessageConverters; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.stereotype.Component; import org.springframework.web.client.RestTemplate; @Component public class RestTemplateFactory implements FactoryBean, InitializingBean { private final HttpMessageConverters httpMessageConverters; private final PortalConfig portalConfig; private final ApolloAuditHttpInterceptor apolloAuditHttpInterceptor; private RestTemplate restTemplate; public RestTemplateFactory(final HttpMessageConverters httpMessageConverters, final PortalConfig portalConfig, final ApolloAuditHttpInterceptor apolloAuditHttpInterceptor) { this.httpMessageConverters = httpMessageConverters; this.portalConfig = portalConfig; this.apolloAuditHttpInterceptor = apolloAuditHttpInterceptor; } @Override public RestTemplate getObject() { return restTemplate; } @Override public Class getObjectType() { return RestTemplate.class; } @Override public boolean isSingleton() { return true; } @Override public void afterPropertiesSet() { PoolingHttpClientConnectionManager connectionManager = PoolingHttpClientConnectionManagerBuilder .create().setMaxConnTotal(portalConfig.connectPoolMaxTotal()) .setMaxConnPerRoute(portalConfig.connectPoolMaxPerRoute()).setConnectionTimeToLive( TimeValue.of(portalConfig.connectionTimeToLive(), TimeUnit.MILLISECONDS)) .build(); CloseableHttpClient httpClient = HttpClients.custom().setConnectionManager(connectionManager) .evictExpiredConnections().build(); restTemplate = new RestTemplate(httpMessageConverters.getConverters()); HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(httpClient); requestFactory.setConnectTimeout(portalConfig.connectTimeout()); requestFactory.setReadTimeout(portalConfig.readTimeout()); restTemplate.setRequestFactory(requestFactory); restTemplate.getInterceptors().add(apolloAuditHttpInterceptor); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/component/RetryableRestTemplate.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.component; import com.ctrip.framework.apollo.common.exception.ServiceException; import com.ctrip.framework.apollo.core.dto.ServiceDTO; import com.ctrip.framework.apollo.portal.component.config.PortalConfig; import com.ctrip.framework.apollo.portal.constant.TracerEventType; import com.ctrip.framework.apollo.portal.environment.Env; import com.ctrip.framework.apollo.portal.environment.PortalMetaDomainService; import com.ctrip.framework.apollo.tracer.Tracer; import com.ctrip.framework.apollo.tracer.spi.Transaction; import com.google.common.base.Strings; import com.google.common.collect.Maps; import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; import java.lang.reflect.Type; import java.net.SocketTimeoutException; import java.util.List; import java.util.Map; import java.util.Objects; import jakarta.annotation.PostConstruct; import org.apache.hc.client5.http.ConnectTimeoutException; import org.apache.hc.client5.http.HttpHostConnectException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Lazy; import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Component; import org.springframework.util.CollectionUtils; import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestTemplate; import org.springframework.web.util.DefaultUriBuilderFactory; import org.springframework.web.util.UriTemplateHandler; /** * 封装RestTemplate. admin server集群在某些机器宕机或者超时的情况下轮询重试 */ @Component public class RetryableRestTemplate { private Logger logger = LoggerFactory.getLogger(RetryableRestTemplate.class); private UriTemplateHandler uriTemplateHandler = new DefaultUriBuilderFactory(); private static final Gson GSON = new Gson(); /** * Admin service access tokens in "PortalDB.ServerConfig" */ private static final Type ACCESS_TOKENS = new TypeToken>() {}.getType(); private RestTemplate restTemplate; private final RestTemplateFactory restTemplateFactory; private final AdminServiceAddressLocator adminServiceAddressLocator; private final PortalMetaDomainService portalMetaDomainService; private final PortalConfig portalConfig; private volatile String lastAdminServiceAccessTokens; private volatile Map adminServiceAccessTokenMap; public RetryableRestTemplate(final @Lazy RestTemplateFactory restTemplateFactory, final @Lazy AdminServiceAddressLocator adminServiceAddressLocator, final PortalMetaDomainService portalMetaDomainService, final PortalConfig portalConfig) { this.restTemplateFactory = restTemplateFactory; this.adminServiceAddressLocator = adminServiceAddressLocator; this.portalMetaDomainService = portalMetaDomainService; this.portalConfig = portalConfig; } @PostConstruct private void postConstruct() { restTemplate = restTemplateFactory.getObject(); } public T get(Env env, String path, Class responseType, Object... urlVariables) throws RestClientException { return execute(HttpMethod.GET, env, path, null, responseType, urlVariables); } public ResponseEntity get(Env env, String path, ParameterizedTypeReference reference, Object... uriVariables) throws RestClientException { return exchangeGet(env, path, reference, uriVariables); } public T post(Env env, String path, Object request, Class responseType, Object... uriVariables) throws RestClientException { return execute(HttpMethod.POST, env, path, request, responseType, uriVariables); } public void put(Env env, String path, Object request, Object... urlVariables) throws RestClientException { execute(HttpMethod.PUT, env, path, request, null, urlVariables); } public void delete(Env env, String path, Object... urlVariables) throws RestClientException { execute(HttpMethod.DELETE, env, path, null, null, urlVariables); } private T execute(HttpMethod method, Env env, String path, Object request, Class responseType, Object... uriVariables) { if (path.startsWith("/")) { path = path.substring(1); } String uri = uriTemplateHandler.expand(path, uriVariables).getPath(); Transaction ct = Tracer.newTransaction("AdminAPI", uri); ct.addData("Env", env); List services = getAdminServices(env, ct); HttpHeaders extraHeaders = assembleExtraHeaders(env); for (ServiceDTO serviceDTO : services) { try { T result = doExecute(method, extraHeaders, serviceDTO, path, request, responseType, uriVariables); ct.setStatus(Transaction.SUCCESS); ct.complete(); return result; } catch (Throwable t) { logger.error("Http request failed, uri: {}, method: {}", uri, method, t); Tracer.logError(t); if (canRetry(t, method)) { Tracer.logEvent(TracerEventType.API_RETRY, uri); } else {// biz exception rethrow ct.setStatus(t); ct.complete(); throw t; } } } // all admin server down ServiceException e = new ServiceException( String.format("Admin servers are unresponsive. meta server address: %s, admin servers: %s", portalMetaDomainService.getDomain(env), services)); ct.setStatus(e); ct.complete(); throw e; } private ResponseEntity exchangeGet(Env env, String path, ParameterizedTypeReference reference, Object... uriVariables) { if (path.startsWith("/")) { path = path.substring(1); } String uri = uriTemplateHandler.expand(path, uriVariables).getPath(); Transaction ct = Tracer.newTransaction("AdminAPI", uri); ct.addData("Env", env); List services = getAdminServices(env, ct); HttpEntity entity = new HttpEntity<>(assembleExtraHeaders(env)); for (ServiceDTO serviceDTO : services) { try { ResponseEntity result = restTemplate.exchange(parseHost(serviceDTO) + path, HttpMethod.GET, entity, reference, uriVariables); ct.setStatus(Transaction.SUCCESS); ct.complete(); return result; } catch (Throwable t) { logger.error("Http request failed, uri: {}, method: {}", uri, HttpMethod.GET, t); Tracer.logError(t); if (canRetry(t, HttpMethod.GET)) { Tracer.logEvent(TracerEventType.API_RETRY, uri); } else {// biz exception rethrow ct.setStatus(t); ct.complete(); throw t; } } } // all admin server down ServiceException e = new ServiceException( String.format("Admin servers are unresponsive. meta server address: %s, admin servers: %s", portalMetaDomainService.getDomain(env), services)); ct.setStatus(e); ct.complete(); throw e; } private HttpHeaders assembleExtraHeaders(Env env) { String adminServiceAccessToken = getAdminServiceAccessToken(env); if (!Strings.isNullOrEmpty(adminServiceAccessToken)) { HttpHeaders headers = new HttpHeaders(); headers.add(HttpHeaders.AUTHORIZATION, adminServiceAccessToken); return headers; } return null; } private List getAdminServices(Env env, Transaction ct) { List services = adminServiceAddressLocator.getServiceList(env); if (CollectionUtils.isEmpty(services)) { ServiceException e = new ServiceException(String.format("No available admin server." + " Maybe because of meta server down or all admin server down. " + "Meta server address: %s", portalMetaDomainService.getDomain(env))); ct.setStatus(e); ct.complete(); throw e; } return services; } private String getAdminServiceAccessToken(Env env) { String accessTokens = portalConfig.getAdminServiceAccessTokens(); if (Strings.isNullOrEmpty(accessTokens)) { return null; } if (!accessTokens.equals(lastAdminServiceAccessTokens)) { synchronized (this) { adminServiceAccessTokenMap = parseAdminServiceAccessTokens(accessTokens); lastAdminServiceAccessTokens = accessTokens; } } return adminServiceAccessTokenMap.get(env); } private Map parseAdminServiceAccessTokens(String accessTokens) { Map tokenMap = Maps.newHashMap(); try { // try to parse Map map = GSON.fromJson(accessTokens, ACCESS_TOKENS); map.forEach((env, token) -> { if (Env.exists(env)) { tokenMap.put(Env.valueOf(env), token); } }); } catch (Exception e) { logger.error("Wrong format of admin service access tokens: {}", accessTokens, e); } return tokenMap; } private T doExecute(HttpMethod method, HttpHeaders extraHeaders, ServiceDTO service, String path, Object request, Class responseType, Object... uriVariables) { T result = null; if (HttpMethod.GET.equals(method) || HttpMethod.POST.equals(method) || HttpMethod.PUT.equals(method) || HttpMethod.DELETE.equals(method)) { HttpEntity entity; if (request instanceof HttpEntity) { entity = (HttpEntity) request; if (!CollectionUtils.isEmpty(extraHeaders)) { HttpHeaders headers = new HttpHeaders(); headers.addAll(entity.getHeaders()); headers.addAll(extraHeaders); entity = new HttpEntity<>(entity.getBody(), headers); } } else { entity = new HttpEntity<>(request, extraHeaders); } result = restTemplate .exchange(parseHost(service) + path, method, entity, responseType, uriVariables) .getBody(); } else { throw new UnsupportedOperationException( String.format("unsupported http method(method=%s)", method)); } return result; } private String parseHost(ServiceDTO serviceAddress) { String homepageUrl = serviceAddress.getHomepageUrl(); Objects.requireNonNull(homepageUrl, "homepageUrl"); return homepageUrl.endsWith("/") ? homepageUrl : homepageUrl + "/"; } // post,delete,put请求在admin server处理超时情况下不重试 private boolean canRetry(Throwable e, HttpMethod method) { Throwable nestedException = e.getCause(); if (method == HttpMethod.GET) { return nestedException instanceof SocketTimeoutException || nestedException instanceof HttpHostConnectException || nestedException instanceof ConnectTimeoutException; } return nestedException instanceof HttpHostConnectException || nestedException instanceof ConnectTimeoutException; } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/component/UnifiedPermissionValidator.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.component; import com.ctrip.framework.apollo.common.entity.AppNamespace; import com.ctrip.framework.apollo.openapi.auth.ConsumerPermissionValidator; import com.ctrip.framework.apollo.portal.constant.UserIdentityConstants; import org.springframework.stereotype.Component; @Component("unifiedPermissionValidator") public class UnifiedPermissionValidator implements PermissionValidator { private final UserPermissionValidator userPermissionValidator; private final ConsumerPermissionValidator consumerPermissionValidator; public UnifiedPermissionValidator(UserPermissionValidator userPermissionValidator, ConsumerPermissionValidator consumerPermissionValidator) { this.userPermissionValidator = userPermissionValidator; this.consumerPermissionValidator = consumerPermissionValidator; } private PermissionValidator getDelegate() { String type = UserIdentityContextHolder.getAuthType(); if (UserIdentityConstants.USER.equals(type)) { return userPermissionValidator; } if (UserIdentityConstants.CONSUMER.equals(type)) { return consumerPermissionValidator; } throw new IllegalStateException("Unknown authentication type"); } @Override public boolean hasModifyNamespacePermission(String appId, String env, String clusterName, String namespaceName) { return getDelegate().hasModifyNamespacePermission(appId, env, clusterName, namespaceName); } @Override public boolean hasReleaseNamespacePermission(String appId, String env, String clusterName, String namespaceName) { return getDelegate().hasReleaseNamespacePermission(appId, env, clusterName, namespaceName); } @Override public boolean hasAssignRolePermission(String appId) { return getDelegate().hasAssignRolePermission(appId); } @Override public boolean hasCreateNamespacePermission(String appId) { return getDelegate().hasCreateNamespacePermission(appId); } @Override public boolean hasCreateAppNamespacePermission(String appId, AppNamespace appNamespace) { return getDelegate().hasCreateAppNamespacePermission(appId, appNamespace); } @Override public boolean hasCreateClusterPermission(String appId) { return getDelegate().hasCreateClusterPermission(appId); } @Override public boolean isSuperAdmin() { return getDelegate().isSuperAdmin(); } @Override public boolean shouldHideConfigToCurrentUser(String appId, String env, String clusterName, String namespaceName) { return getDelegate().shouldHideConfigToCurrentUser(appId, env, clusterName, namespaceName); } @Override public boolean hasCreateApplicationPermission() { return getDelegate().hasCreateApplicationPermission(); } @Override public boolean hasCreateApplicationPermission(String userId) { return getDelegate().hasCreateApplicationPermission(userId); } @Override public boolean hasDeleteNamespacePermission(String appId) { return getDelegate().hasDeleteNamespacePermission(appId); } @Override public boolean hasManageAppMasterPermission(String appId) { return getDelegate().hasManageAppMasterPermission(appId); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/component/UserIdentityContextHolder.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.component; public final class UserIdentityContextHolder { private static final ThreadLocal AUTH_TYPE_HOLDER = new ThreadLocal<>(); private UserIdentityContextHolder() { // Prevent instantiation } /** * Read authentication source identifier for current thread */ public static String getAuthType() { return AUTH_TYPE_HOLDER.get(); } /** * Write authentication source identifier for current thread */ public static void setAuthType(String authType) { AUTH_TYPE_HOLDER.set(authType); } /** * Clean up current thread variable to prevent memory leaks */ public static void clear() { AUTH_TYPE_HOLDER.remove(); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/component/UserPermissionValidator.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.component; import com.ctrip.framework.apollo.common.entity.AppNamespace; import com.ctrip.framework.apollo.portal.component.config.PortalConfig; import com.ctrip.framework.apollo.portal.entity.po.Permission; import com.ctrip.framework.apollo.portal.service.AppNamespaceService; import com.ctrip.framework.apollo.portal.service.RolePermissionService; import com.ctrip.framework.apollo.portal.service.SystemRoleManagerService; import com.ctrip.framework.apollo.portal.spi.UserInfoHolder; import java.util.List; import org.springframework.stereotype.Component; @Component("userPermissionValidator") public class UserPermissionValidator extends AbstractPermissionValidator implements PermissionValidator { private final UserInfoHolder userInfoHolder; private final RolePermissionService rolePermissionService; private final PortalConfig portalConfig; private final AppNamespaceService appNamespaceService; private final SystemRoleManagerService systemRoleManagerService; public UserPermissionValidator(final UserInfoHolder userInfoHolder, final RolePermissionService rolePermissionService, final PortalConfig portalConfig, final AppNamespaceService appNamespaceService, final SystemRoleManagerService systemRoleManagerService) { this.userInfoHolder = userInfoHolder; this.rolePermissionService = rolePermissionService; this.portalConfig = portalConfig; this.appNamespaceService = appNamespaceService; this.systemRoleManagerService = systemRoleManagerService; } @Override public boolean hasCreateAppNamespacePermission(String appId, AppNamespace appNamespace) { boolean isPublicAppNamespace = appNamespace.isPublic(); if (portalConfig.canAppAdminCreatePrivateNamespace() || isPublicAppNamespace) { return hasCreateNamespacePermission(appId); } return isSuperAdmin(); } @Override public boolean isSuperAdmin() { return rolePermissionService.isSuperAdmin(userInfoHolder.getUser().getUserId()); } @Override public boolean shouldHideConfigToCurrentUser(String appId, String env, String clusterName, String namespaceName) { // Normalize env to ensure consistency with permission checks String normalizedEnv = normalizeEnv(env); // 1. check whether the current environment enables member only function if (!portalConfig.isConfigViewMemberOnly(normalizedEnv)) { return false; } // 2. public namespace is open to every one AppNamespace appNamespace = appNamespaceService.findByAppIdAndName(appId, namespaceName); if (appNamespace != null && appNamespace.isPublic()) { return false; } // 3. check app admin and operate permissions return !isAppAdmin(appId) && !hasOperateNamespacePermission(appId, normalizedEnv, clusterName, namespaceName); } @Override public boolean hasCreateApplicationPermission() { return systemRoleManagerService .hasCreateApplicationPermission(userInfoHolder.getUser().getUserId()); } @Override public boolean hasCreateApplicationPermission(String userId) { return systemRoleManagerService.hasCreateApplicationPermission(userId); } @Override public boolean hasManageAppMasterPermission(String appId) { // the manage app master permission might not be initialized, so we need to check isSuperAdmin // first return isSuperAdmin() || (hasAssignRolePermission(appId) && systemRoleManagerService .hasManageAppMasterPermission(userInfoHolder.getUser().getUserId(), appId)); } @Override protected boolean hasPermissions(List requiredPerms) { if (requiredPerms == null || requiredPerms.isEmpty()) { return false; } String userId = userInfoHolder.getUser().getUserId(); return rolePermissionService.hasAnyPermission(userId, requiredPerms); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/component/config/PortalConfig.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.component.config; import com.ctrip.framework.apollo.common.config.RefreshableConfig; import com.ctrip.framework.apollo.common.config.RefreshablePropertySource; import com.ctrip.framework.apollo.portal.entity.vo.Organization; import com.ctrip.framework.apollo.portal.environment.Env; import com.ctrip.framework.apollo.portal.service.PortalDBPropertySource; import com.ctrip.framework.apollo.portal.service.SystemRoleManagerService; import com.google.common.base.Strings; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; import java.lang.reflect.Type; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; @Component public class PortalConfig extends RefreshableConfig { private static final Logger logger = LoggerFactory.getLogger(PortalConfig.class); private static final int DEFAULT_REFRESH_ADMIN_SERVER_ADDRESS_TASK_NORMAL_INTERVAL_IN_SECOND = 5 * 60; // 5min private static final int DEFAULT_REFRESH_ADMIN_SERVER_ADDRESS_TASK_OFFLINE_INTERVAL_IN_SECOND = 10; // 10s private static final Gson GSON = new Gson(); private static final Type ORGANIZATION = new TypeToken>() {}.getType(); private static final List DEFAULT_USER_PASSWORD_NOT_ALLOW_LIST = Arrays.asList("111", "222", "333", "444", "555", "666", "777", "888", "999", "000", "001122", "112233", "223344", "334455", "445566", "556677", "667788", "778899", "889900", "009988", "998877", "887766", "776655", "665544", "554433", "443322", "332211", "221100", "0123", "1234", "2345", "3456", "4567", "5678", "6789", "7890", "0987", "9876", "8765", "7654", "6543", "5432", "4321", "3210", "1q2w", "2w3e", "3e4r", "5t6y", "abcd", "qwer", "asdf", "zxcv"); /** * meta servers config in "PortalDB.ServerConfig" */ private static final Type META_SERVERS = new TypeToken>() {}.getType(); private final PortalDBPropertySource portalDBPropertySource; public PortalConfig(final PortalDBPropertySource portalDBPropertySource) { this.portalDBPropertySource = portalDBPropertySource; } @Override public List getRefreshablePropertySources() { return Collections.singletonList(portalDBPropertySource); } /*** * Level: important **/ public List portalSupportedEnvs() { String[] configurations = getArrayProperty("apollo.portal.envs", new String[] {"FAT", "UAT", "PRO"}); List envs = Lists.newLinkedList(); for (String env : configurations) { envs.add(Env.addEnvironment(env)); } return envs; } public int getPerEnvSearchMaxResults() { return getIntProperty("apollo.portal.search.perEnvMaxResults", 200); } /** * @return the relationship between environment and its meta server. empty if meet exception */ public Map getMetaServers() { final String key = "apollo.portal.meta.servers"; String jsonContent = getValue(key); if (null == jsonContent) { return Collections.emptyMap(); } // watch out that the format of content may be wrong // that will cause exception Map map = Collections.emptyMap(); try { // try to parse map = GSON.fromJson(jsonContent, META_SERVERS); } catch (Exception e) { logger.error("Wrong format for: {}", key, e); } return map; } public List superAdmins() { String superAdminConfig = getValue("superAdmin", ""); if (Strings.isNullOrEmpty(superAdminConfig)) { return Collections.emptyList(); } return splitter.splitToList(superAdminConfig); } public Set emailSupportedEnvs() { String[] configurations = getArrayProperty("email.supported.envs", null); Set result = Sets.newHashSet(); if (configurations == null) { return result; } for (String env : configurations) { result.add(Env.valueOf(env)); } return result; } public Set webHookSupportedEnvs() { String[] configurations = getArrayProperty("webhook.supported.envs", null); Set result = Sets.newHashSet(); if (configurations == null) { return result; } for (String env : configurations) { result.add(Env.valueOf(env)); } return result; } public boolean isConfigViewMemberOnly(String env) { // Normalize env to handle aliases (prod/PROD/PRO) Env transformedEnv = Env.transformEnv(env); if (Env.UNKNOWN == transformedEnv) { // Invalid env, treat as not member-only for safety return false; } String normalizedEnv = transformedEnv.getName(); String[] configViewMemberOnlyEnvs = getArrayProperty("configView.memberOnly.envs", new String[0]); for (String memberOnlyEnv : configViewMemberOnlyEnvs) { // Normalize configured env as well for consistent comparison Env configEnv = Env.transformEnv(memberOnlyEnv); if (configEnv != Env.UNKNOWN && configEnv.getName().equals(normalizedEnv)) { return true; } } return false; } /*** * Level: normal **/ public int connectTimeout() { return getIntProperty("api.connectTimeout", 3000); } public int readTimeout() { return getIntProperty("api.readTimeout", 10000); } public int connectionTimeToLive() { return getIntProperty("api.connectionTimeToLive", -1); } public int connectPoolMaxTotal() { return getIntProperty("api.pool.max.total", 20); } public int connectPoolMaxPerRoute() { return getIntProperty("api.pool.max.per.route", 2); } public List organizations() { String organizations = getValue("organizations"); return organizations == null ? Collections.emptyList() : GSON.fromJson(organizations, ORGANIZATION); } public String portalAddress() { return getValue("apollo.portal.address"); } public int refreshAdminServerAddressTaskNormalIntervalSecond() { int interval = getIntProperty("refresh.admin.server.address.task.normal.interval.second", DEFAULT_REFRESH_ADMIN_SERVER_ADDRESS_TASK_NORMAL_INTERVAL_IN_SECOND); return checkInt(interval, 5, Integer.MAX_VALUE, DEFAULT_REFRESH_ADMIN_SERVER_ADDRESS_TASK_NORMAL_INTERVAL_IN_SECOND); } public int refreshAdminServerAddressTaskOfflineIntervalSecond() { int interval = getIntProperty("refresh.admin.server.address.task.offline.interval.second", DEFAULT_REFRESH_ADMIN_SERVER_ADDRESS_TASK_OFFLINE_INTERVAL_IN_SECOND); return checkInt(interval, 5, Integer.MAX_VALUE, DEFAULT_REFRESH_ADMIN_SERVER_ADDRESS_TASK_OFFLINE_INTERVAL_IN_SECOND); } public boolean isEmergencyPublishAllowed(Env env) { String targetEnv = env.getName(); String[] emergencyPublishSupportedEnvs = getArrayProperty("emergencyPublish.supported.envs", new String[0]); for (String supportedEnv : emergencyPublishSupportedEnvs) { if (Objects.equals(targetEnv, supportedEnv.toUpperCase().trim())) { return true; } } return false; } /*** * Level: low **/ public Set publishTipsSupportedEnvs() { String[] configurations = getArrayProperty("namespace.publish.tips.supported.envs", null); Set result = Sets.newHashSet(); if (configurations == null) { return result; } for (String env : configurations) { result.add(Env.valueOf(env)); } return result; } public String consumerTokenSalt() { return getValue("consumer.token.salt", "apollo-portal"); } public boolean isEmailEnabled() { return getBooleanProperty("email.enabled", false); } public String emailConfigHost() { return getValue("email.config.host", ""); } public String emailConfigUser() { return getValue("email.config.user", ""); } public String emailConfigPassword() { return getValue("email.config.password", ""); } public String emailSender() { String value = getValue("email.sender", ""); if (Strings.isNullOrEmpty(value)) { value = emailConfigUser(); } return value; } public String emailTemplateFramework() { return getValue("email.template.framework", ""); } public String emailReleaseDiffModuleTemplate() { return getValue("email.template.release.module.diff", ""); } public String emailRollbackDiffModuleTemplate() { return getValue("email.template.rollback.module.diff", ""); } public String emailGrayRulesModuleTemplate() { return getValue("email.template.release.module.rules", ""); } public String wikiAddress() { return getValue("wiki.address", "https://www.apolloconfig.com"); } public boolean canAppAdminCreatePrivateNamespace() { return getBooleanProperty("admin.createPrivateNamespace.switch", true); } public boolean isCreateApplicationPermissionEnabled() { return getBooleanProperty(SystemRoleManagerService.CREATE_APPLICATION_LIMIT_SWITCH_KEY, false); } public boolean isManageAppMasterPermissionEnabled() { return getBooleanProperty(SystemRoleManagerService.MANAGE_APP_MASTER_LIMIT_SWITCH_KEY, false); } public String getAdminServiceAccessTokens() { return getValue("admin-service.access.tokens"); } public String[] webHookUrls() { return getArrayProperty("config.release.webhook.service.url", null); } public boolean supportSearchByItem() { return getBooleanProperty("searchByItem.switch", true); } public List getUserPasswordNotAllowList() { String[] value = getArrayProperty("apollo.portal.auth.user-password-not-allow-list", null); if (value == null || value.length == 0) { return DEFAULT_USER_PASSWORD_NOT_ALLOW_LIST; } return Arrays.asList(value); } private int checkInt(int value, int min, int max, int defaultValue) { if (value >= min && value <= max) { return value; } logger.warn("Configuration value '{}' is out of bounds [{} - {}]. Using default value '{}'.", value, min, max, defaultValue); return defaultValue; } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/component/config/SpringSessionConfig.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.component.config; import com.fasterxml.jackson.databind.ObjectMapper; import java.io.IOException; import org.springframework.beans.factory.BeanClassLoaderAware; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.support.GenericConversionService; import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.security.jackson2.SecurityJackson2Modules; /** * Spring-session JSON serialization mode configuration * * @author kl (http://kailing.pub) * @since 2022/7/26 */ @Configuration public class SpringSessionConfig implements BeanClassLoaderAware { private ClassLoader loader; @Bean("springSessionConversionService") @ConditionalOnProperty(prefix = "spring.session", name = "store-type", havingValue = "jdbc") public ConversionService springSessionConversionService() { GenericConversionService conversionService = new GenericConversionService(); ObjectMapper objectMapper = this.objectMapper(); conversionService.addConverter(Object.class, byte[].class, source -> { try { return objectMapper.writeValueAsBytes(source); } catch (IOException e) { throw new RuntimeException( "Spring-session JSON serializing error, This is usually caused by the system upgrade, please clear the browser cookies and try again.", e); } }); conversionService.addConverter(byte[].class, Object.class, source -> { try { return objectMapper.readValue(source, Object.class); } catch (IOException e) { throw new RuntimeException( "Spring-session JSON deserializing error, This is usually caused by the system upgrade, please clear the browser cookies and try again.", e); } }); return conversionService; } @Bean("springSessionDefaultRedisSerializer") @ConditionalOnProperty(prefix = "spring.session", name = "store-type", havingValue = "redis") public RedisSerializer springSessionDefaultRedisSerializer() { return new GenericJackson2JsonRedisSerializer(objectMapper()); } /** * Customized {@link ObjectMapper} to add mix-in for class that doesn't have default constructors * * @return the {@link ObjectMapper} to use */ private ObjectMapper objectMapper() { ObjectMapper mapper = new ObjectMapper(); mapper.registerModules(SecurityJackson2Modules.getModules(this.loader)); return mapper; } /* * @see * org.springframework.beans.factory.BeanClassLoaderAware#setBeanClassLoader(java.lang * .ClassLoader) */ @Override public void setBeanClassLoader(ClassLoader classLoader) { this.loader = classLoader; } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/component/emailbuilder/ConfigPublishEmailBuilder.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.component.emailbuilder; import com.google.common.collect.Lists; import com.ctrip.framework.apollo.common.constants.ReleaseOperation; import com.ctrip.framework.apollo.common.constants.ReleaseOperationContext; import com.ctrip.framework.apollo.common.dto.ReleaseDTO; import com.ctrip.framework.apollo.common.entity.AppNamespace; import com.ctrip.framework.apollo.core.enums.ConfigFileFormat; import com.ctrip.framework.apollo.portal.environment.Env; import com.ctrip.framework.apollo.portal.component.config.PortalConfig; import com.ctrip.framework.apollo.portal.constant.RoleType; import com.ctrip.framework.apollo.portal.entity.bo.Email; import com.ctrip.framework.apollo.portal.entity.bo.ReleaseHistoryBO; import com.ctrip.framework.apollo.portal.entity.bo.UserInfo; import com.ctrip.framework.apollo.portal.entity.vo.Change; import com.ctrip.framework.apollo.portal.entity.vo.ReleaseCompareResult; import com.ctrip.framework.apollo.portal.service.AppNamespaceService; import com.ctrip.framework.apollo.portal.service.ReleaseService; import com.ctrip.framework.apollo.portal.service.RolePermissionService; import com.ctrip.framework.apollo.portal.spi.UserService; import com.ctrip.framework.apollo.portal.util.RoleUtils; import org.apache.commons.lang3.time.FastDateFormat; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.util.CollectionUtils; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.regex.Matcher; public abstract class ConfigPublishEmailBuilder { private static final String EMERGENCY_PUBLISH_TAG = "(紧急发布)"; // email content common field placeholder private static final String EMAIL_CONTENT_FIELD_APPID = "#\\{appId\\}"; private static final String EMAIL_CONTENT_FIELD_ENV = "#\\{env}"; private static final String EMAIL_CONTENT_FIELD_CLUSTER = "#\\{clusterName}"; private static final String EMAIL_CONTENT_FIELD_NAMESPACE = "#\\{namespaceName}"; private static final String EMAIL_CONTENT_FIELD_OPERATOR = "#\\{operator}"; private static final String EMAIL_CONTENT_FIELD_RELEASE_TIME = "#\\{releaseTime}"; private static final String EMAIL_CONTENT_FIELD_RELEASE_ID = "#\\{releaseId}"; private static final String EMAIL_CONTENT_FIELD_RELEASE_HISTORY_ID = "#\\{releaseHistoryId}"; private static final String EMAIL_CONTENT_FIELD_RELEASE_TITLE = "#\\{releaseTitle}"; private static final String EMAIL_CONTENT_FIELD_RELEASE_COMMENT = "#\\{releaseComment}"; private static final String EMAIL_CONTENT_FIELD_APOLLO_SERVER_ADDRESS = "#\\{apollo.portal.address}"; private static final String EMAIL_CONTENT_FIELD_DIFF_CONTENT = "#\\{diffContent}"; private static final String EMAIL_CONTENT_FIELD_EMERGENCY_PUBLISH = "#\\{emergencyPublish}"; private static final String EMAIL_CONTENT_DIFF_MODULE = "#\\{diffModule}"; protected static final String EMAIL_CONTENT_GRAY_RULES_MODULE = "#\\{rulesModule}"; // email content special field placeholder protected static final String EMAIL_CONTENT_GRAY_RULES_CONTENT = "#\\{rulesContent}"; // set config's value max length to protect email. protected static final int VALUE_MAX_LENGTH = 100; protected FastDateFormat dateFormat = FastDateFormat.getInstance("yyyy-MM-dd HH:mm:ss"); @Autowired private RolePermissionService rolePermissionService; @Autowired private ReleaseService releaseService; @Autowired private AppNamespaceService appNamespaceService; @Autowired private UserService userService; @Autowired protected PortalConfig portalConfig; /** * email subject */ protected abstract String subject(); /** * email body content */ protected abstract String emailContent(Env env, ReleaseHistoryBO releaseHistory); /** * email body template framework */ protected abstract String getTemplateFramework(); /** * email body diff module template */ protected abstract String getDiffModuleTemplate(); public Email build(Env env, ReleaseHistoryBO releaseHistory) { Email email = new Email(); email.setSubject(subject()); email.setSenderEmailAddress(portalConfig.emailSender()); email.setRecipients( recipients(releaseHistory.getAppId(), releaseHistory.getNamespaceName(), env.toString())); String emailBody = emailContent(env, releaseHistory); // clear not used module emailBody = emailBody.replaceAll(EMAIL_CONTENT_DIFF_MODULE, ""); emailBody = emailBody.replaceAll(EMAIL_CONTENT_GRAY_RULES_MODULE, ""); email.setBody(emailBody); return email; } protected String renderEmailCommonContent(Env env, ReleaseHistoryBO releaseHistory) { String template = getTemplateFramework(); String renderResult = renderReleaseBasicInfo(template, env, releaseHistory); renderResult = renderDiffModule(renderResult, env, releaseHistory); return renderResult; } private String renderReleaseBasicInfo(String template, Env env, ReleaseHistoryBO releaseHistory) { String renderResult = template; Map operationContext = releaseHistory.getOperationContext(); boolean isEmergencyPublish = operationContext.containsKey(ReleaseOperationContext.IS_EMERGENCY_PUBLISH) && (boolean) operationContext.get(ReleaseOperationContext.IS_EMERGENCY_PUBLISH); if (isEmergencyPublish) { renderResult = renderResult.replaceAll(EMAIL_CONTENT_FIELD_EMERGENCY_PUBLISH, Matcher.quoteReplacement(EMERGENCY_PUBLISH_TAG)); } else { renderResult = renderResult.replaceAll(EMAIL_CONTENT_FIELD_EMERGENCY_PUBLISH, ""); } renderResult = renderResult.replaceAll(EMAIL_CONTENT_FIELD_APPID, Matcher.quoteReplacement(releaseHistory.getAppId())); renderResult = renderResult.replaceAll(EMAIL_CONTENT_FIELD_ENV, Matcher.quoteReplacement(env.toString())); renderResult = renderResult.replaceAll(EMAIL_CONTENT_FIELD_CLUSTER, Matcher.quoteReplacement(releaseHistory.getClusterName())); renderResult = renderResult.replaceAll(EMAIL_CONTENT_FIELD_NAMESPACE, Matcher.quoteReplacement(releaseHistory.getNamespaceName())); renderResult = renderResult.replaceAll(EMAIL_CONTENT_FIELD_OPERATOR, Matcher.quoteReplacement(releaseHistory.getOperator())); renderResult = renderResult.replaceAll(EMAIL_CONTENT_FIELD_RELEASE_TITLE, Matcher.quoteReplacement(releaseHistory.getReleaseTitle())); renderResult = renderResult.replaceAll(EMAIL_CONTENT_FIELD_RELEASE_ID, String.valueOf(releaseHistory.getReleaseId())); renderResult = renderResult.replaceAll(EMAIL_CONTENT_FIELD_RELEASE_HISTORY_ID, String.valueOf(releaseHistory.getId())); renderResult = renderResult.replaceAll(EMAIL_CONTENT_FIELD_RELEASE_COMMENT, Matcher.quoteReplacement( releaseHistory.getReleaseComment() == null ? "" : releaseHistory.getReleaseComment())); renderResult = renderResult.replaceAll(EMAIL_CONTENT_FIELD_APOLLO_SERVER_ADDRESS, getApolloPortalAddress()); return renderResult.replaceAll(EMAIL_CONTENT_FIELD_RELEASE_TIME, dateFormat.format(releaseHistory.getReleaseTime())); } private String renderDiffModule(String bodyTemplate, Env env, ReleaseHistoryBO releaseHistory) { String appId = releaseHistory.getAppId(); String namespaceName = releaseHistory.getNamespaceName(); AppNamespace appNamespace = appNamespaceService.findByAppIdAndName(appId, namespaceName); if (appNamespace == null) { appNamespace = appNamespaceService.findPublicAppNamespace(namespaceName); } // don't show diff content if namespace's format is file if (appNamespace == null || !appNamespace.getFormat().equals(ConfigFileFormat.Properties.getValue())) { return bodyTemplate.replaceAll(EMAIL_CONTENT_DIFF_MODULE, "

变更内容请点击链接到Apollo上查看

"); } ReleaseCompareResult result = getReleaseCompareResult(env, releaseHistory); if (!result.hasContent()) { return bodyTemplate.replaceAll(EMAIL_CONTENT_DIFF_MODULE, "

无配置变更

"); } List changes = result.getChanges(); StringBuilder changesHtmlBuilder = new StringBuilder(); for (Change change : changes) { String key = change.getEntity().getFirstEntity().getKey(); String oldValue = change.getEntity().getFirstEntity().getValue(); String newValue = change.getEntity().getSecondEntity().getValue(); newValue = newValue == null ? "" : newValue; changesHtmlBuilder.append(""); changesHtmlBuilder.append("").append(change.getType().toString()) .append(""); changesHtmlBuilder.append("").append(cutOffString(key)).append(""); changesHtmlBuilder.append("").append(cutOffString(oldValue)) .append(""); changesHtmlBuilder.append("").append(cutOffString(newValue)) .append(""); changesHtmlBuilder.append(""); } String diffContent = Matcher.quoteReplacement(changesHtmlBuilder.toString()); String diffModuleTemplate = getDiffModuleTemplate(); String diffModuleRenderResult = diffModuleTemplate.replaceAll(EMAIL_CONTENT_FIELD_DIFF_CONTENT, diffContent); return bodyTemplate.replaceAll(EMAIL_CONTENT_DIFF_MODULE, diffModuleRenderResult); } private ReleaseCompareResult getReleaseCompareResult(Env env, ReleaseHistoryBO releaseHistory) { if (releaseHistory.getOperation() == ReleaseOperation.GRAY_RELEASE && releaseHistory.getPreviousReleaseId() == 0) { ReleaseDTO masterLatestActiveRelease = releaseService.loadLatestRelease(releaseHistory.getAppId(), env, releaseHistory.getClusterName(), releaseHistory.getNamespaceName()); ReleaseDTO branchLatestActiveRelease = releaseService.findReleaseById(env, releaseHistory.getReleaseId()); return releaseService.compare(masterLatestActiveRelease, branchLatestActiveRelease); } return releaseService.compare(env, releaseHistory.getPreviousReleaseId(), releaseHistory.getReleaseId()); } private List recipients(String appId, String namespaceName, String env) { Set modifyRoleUsers = rolePermissionService.queryUsersWithRole( RoleUtils.buildNamespaceRoleName(appId, namespaceName, RoleType.MODIFY_NAMESPACE)); Set envModifyRoleUsers = rolePermissionService.queryUsersWithRole( RoleUtils.buildNamespaceRoleName(appId, namespaceName, RoleType.MODIFY_NAMESPACE, env)); Set releaseRoleUsers = rolePermissionService.queryUsersWithRole( RoleUtils.buildNamespaceRoleName(appId, namespaceName, RoleType.RELEASE_NAMESPACE)); Set envReleaseRoleUsers = rolePermissionService.queryUsersWithRole( RoleUtils.buildNamespaceRoleName(appId, namespaceName, RoleType.RELEASE_NAMESPACE, env)); Set owners = rolePermissionService.queryUsersWithRole(RoleUtils.buildAppMasterRoleName(appId)); Set userIds = new HashSet<>(modifyRoleUsers.size() + releaseRoleUsers.size() + owners.size()); for (UserInfo userInfo : modifyRoleUsers) { userIds.add(userInfo.getUserId()); } for (UserInfo userInfo : envModifyRoleUsers) { userIds.add(userInfo.getUserId()); } for (UserInfo userInfo : releaseRoleUsers) { userIds.add(userInfo.getUserId()); } for (UserInfo userInfo : envReleaseRoleUsers) { userIds.add(userInfo.getUserId()); } for (UserInfo userInfo : owners) { userIds.add(userInfo.getUserId()); } List userInfos = userService.findByUserIds(Lists.newArrayList(userIds)); if (CollectionUtils.isEmpty(userInfos)) { return Collections.emptyList(); } List recipients = new ArrayList<>(userInfos.size()); for (UserInfo userInfo : userInfos) { recipients.add(userInfo.getEmail()); } return recipients; } protected String getApolloPortalAddress() { return portalConfig.portalAddress(); } private String cutOffString(String source) { if (source.length() > VALUE_MAX_LENGTH) { return source.substring(0, VALUE_MAX_LENGTH) + "..."; } return source; } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/component/emailbuilder/GrayPublishEmailBuilder.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.component.emailbuilder; import com.google.common.base.Joiner; import com.google.gson.Gson; import com.ctrip.framework.apollo.common.constants.GsonType; import com.ctrip.framework.apollo.common.dto.GrayReleaseRuleItemDTO; import com.ctrip.framework.apollo.portal.environment.Env; import com.ctrip.framework.apollo.portal.entity.bo.ReleaseHistoryBO; import org.springframework.stereotype.Component; import org.springframework.util.CollectionUtils; import java.util.List; import java.util.Map; import java.util.Set; import java.util.regex.Matcher; @Component public class GrayPublishEmailBuilder extends ConfigPublishEmailBuilder { private static final String EMAIL_SUBJECT = "[Apollo] 灰度发布"; private Gson gson = new Gson(); private Joiner IP_JOINER = Joiner.on(", "); @Override protected String subject() { return EMAIL_SUBJECT; } @Override public String emailContent(Env env, ReleaseHistoryBO releaseHistory) { String result = renderEmailCommonContent(env, releaseHistory); return renderGrayReleaseRuleContent(result, releaseHistory); } @Override protected String getTemplateFramework() { return portalConfig.emailTemplateFramework(); } @Override protected String getDiffModuleTemplate() { return portalConfig.emailReleaseDiffModuleTemplate(); } private String renderGrayReleaseRuleContent(String bodyTemplate, ReleaseHistoryBO releaseHistory) { Map context = releaseHistory.getOperationContext(); Object rules = context.get("rules"); List ruleItems = rules == null ? null : gson.fromJson(rules.toString(), GsonType.RULE_ITEMS); if (CollectionUtils.isEmpty(ruleItems)) { return bodyTemplate.replaceAll(EMAIL_CONTENT_GRAY_RULES_MODULE, "

无灰度规则

"); } StringBuilder rulesHtmlBuilder = new StringBuilder(); for (GrayReleaseRuleItemDTO ruleItem : ruleItems) { String clientAppId = ruleItem.getClientAppId(); Set ips = ruleItem.getClientIpList(); rulesHtmlBuilder.append("AppId: ").append(clientAppId) .append("   IP: "); IP_JOINER.appendTo(rulesHtmlBuilder, ips); } String grayRulesModuleContent = portalConfig.emailGrayRulesModuleTemplate().replaceAll( EMAIL_CONTENT_GRAY_RULES_CONTENT, Matcher.quoteReplacement(rulesHtmlBuilder.toString())); return bodyTemplate.replaceAll(EMAIL_CONTENT_GRAY_RULES_MODULE, Matcher.quoteReplacement(grayRulesModuleContent)); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/component/emailbuilder/MergeEmailBuilder.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.component.emailbuilder; import com.ctrip.framework.apollo.portal.environment.Env; import com.ctrip.framework.apollo.portal.entity.bo.ReleaseHistoryBO; import org.springframework.stereotype.Component; @Component public class MergeEmailBuilder extends ConfigPublishEmailBuilder { private static final String EMAIL_SUBJECT = "[Apollo] 全量发布"; @Override protected String subject() { return EMAIL_SUBJECT; } @Override protected String emailContent(Env env, ReleaseHistoryBO releaseHistory) { return renderEmailCommonContent(env, releaseHistory); } @Override protected String getTemplateFramework() { return portalConfig.emailTemplateFramework(); } @Override protected String getDiffModuleTemplate() { return portalConfig.emailReleaseDiffModuleTemplate(); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/component/emailbuilder/NormalPublishEmailBuilder.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.component.emailbuilder; import com.ctrip.framework.apollo.portal.environment.Env; import com.ctrip.framework.apollo.portal.entity.bo.ReleaseHistoryBO; import org.springframework.stereotype.Component; @Component public class NormalPublishEmailBuilder extends ConfigPublishEmailBuilder { private static final String EMAIL_SUBJECT = "[Apollo] 配置发布"; @Override protected String subject() { return EMAIL_SUBJECT; } @Override protected String emailContent(Env env, ReleaseHistoryBO releaseHistory) { return renderEmailCommonContent(env, releaseHistory); } @Override protected String getTemplateFramework() { return portalConfig.emailTemplateFramework(); } @Override protected String getDiffModuleTemplate() { return portalConfig.emailReleaseDiffModuleTemplate(); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/component/emailbuilder/RollbackEmailBuilder.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.component.emailbuilder; import com.ctrip.framework.apollo.portal.environment.Env; import com.ctrip.framework.apollo.portal.entity.bo.ReleaseHistoryBO; import org.springframework.stereotype.Component; @Component public class RollbackEmailBuilder extends ConfigPublishEmailBuilder { private static final String EMAIL_SUBJECT = "[Apollo] 配置回滚"; @Override protected String subject() { return EMAIL_SUBJECT; } @Override protected String emailContent(Env env, ReleaseHistoryBO releaseHistory) { return renderEmailCommonContent(env, releaseHistory); } @Override protected String getTemplateFramework() { return portalConfig.emailTemplateFramework(); } @Override protected String getDiffModuleTemplate() { return portalConfig.emailRollbackDiffModuleTemplate(); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/component/txtresolver/ConfigTextResolver.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.component.txtresolver; import com.ctrip.framework.apollo.common.dto.ItemChangeSets; import com.ctrip.framework.apollo.common.dto.ItemDTO; import java.util.List; /** * users can modify config in text mode.so need resolve text. */ public interface ConfigTextResolver { ItemChangeSets resolve(long namespaceId, String configText, List baseItems); } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/component/txtresolver/FileTextResolver.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.component.txtresolver; import com.ctrip.framework.apollo.common.dto.ItemChangeSets; import com.ctrip.framework.apollo.common.dto.ItemDTO; import com.ctrip.framework.apollo.core.ConfigConsts; import com.ctrip.framework.apollo.core.utils.StringUtils; import org.springframework.stereotype.Component; import org.springframework.util.CollectionUtils; import java.util.List; @Component("fileTextResolver") public class FileTextResolver implements ConfigTextResolver { @Override public ItemChangeSets resolve(long namespaceId, String configText, List baseItems) { ItemChangeSets changeSets = new ItemChangeSets(); if (CollectionUtils.isEmpty(baseItems) && StringUtils.isEmpty(configText)) { return changeSets; } if (CollectionUtils.isEmpty(baseItems)) { changeSets.addCreateItem(createItem(namespaceId, 0, configText)); } else { ItemDTO beforeItem = baseItems.get(0); if (!configText.equals(beforeItem.getValue())) {// update changeSets.addUpdateItem(createItem(namespaceId, beforeItem.getId(), configText)); } } return changeSets; } private ItemDTO createItem(long namespaceId, long itemId, String value) { ItemDTO item = new ItemDTO(); item.setId(itemId); item.setNamespaceId(namespaceId); item.setValue(value); item.setLineNum(1); item.setKey(ConfigConsts.CONFIG_FILE_CONTENT_KEY); return item; } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/component/txtresolver/PropertyResolver.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.component.txtresolver; import com.ctrip.framework.apollo.common.dto.ItemChangeSets; import com.ctrip.framework.apollo.common.dto.ItemDTO; import com.ctrip.framework.apollo.common.exception.BadRequestException; import com.ctrip.framework.apollo.common.utils.BeanUtils; import com.ctrip.framework.apollo.core.utils.StringUtils; import com.google.common.base.Strings; import org.springframework.stereotype.Component; import org.springframework.util.CollectionUtils; import jakarta.validation.constraints.NotNull; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; /** * normal property file resolver. * update comment and blank item implement by create new item and delete old item. * update normal key/value item implement by update. */ @Component("propertyResolver") public class PropertyResolver implements ConfigTextResolver { private static final String KV_SEPARATOR = "="; private static final String ITEM_SEPARATOR = "\n"; @Override public ItemChangeSets resolve(long namespaceId, String configText, List baseItems) { Map oldKeyMapItem = BeanUtils.mapByKey("key", baseItems); // remove comment and blank item map. oldKeyMapItem.remove(""); // comment items List baseCommentItems = new LinkedList<>(); // blank items List baseBlankItems = new LinkedList<>(); if (!CollectionUtils.isEmpty(baseItems)) { baseCommentItems = baseItems.stream().filter(this::isCommentItem) .sorted(Comparator.comparing(ItemDTO::getLineNum)) .collect(Collectors.toCollection(LinkedList::new)); baseBlankItems = baseItems.stream().filter(this::isBlankItem) .sorted(Comparator.comparing(ItemDTO::getLineNum)) .collect(Collectors.toCollection(LinkedList::new)); } String[] newItems = configText.split(ITEM_SEPARATOR); Set repeatKeys = new HashSet<>(); if (isHasRepeatKey(newItems, repeatKeys)) { throw new BadRequestException("Config text has repeated keys: %s, please check your input.", repeatKeys); } ItemChangeSets changeSets = new ItemChangeSets(); Map newLineNumMapItem = new HashMap<>();// use for delete blank and comment // item int lineCounter = 1; for (String newItem : newItems) { newItem = newItem.trim(); newLineNumMapItem.put(lineCounter, newItem); // comment item if (isCommentItem(newItem)) { ItemDTO oldItemDTO = null; if (!CollectionUtils.isEmpty(baseCommentItems)) { oldItemDTO = baseCommentItems.remove(0); } handleCommentLine(namespaceId, oldItemDTO, newItem, lineCounter, changeSets); // blank item } else if (isBlankItem(newItem)) { ItemDTO oldItemDTO = null; if (!CollectionUtils.isEmpty(baseBlankItems)) { oldItemDTO = baseBlankItems.remove(0); } handleBlankLine(namespaceId, oldItemDTO, lineCounter, changeSets); // normal item } else { handleNormalLine(namespaceId, oldKeyMapItem, newItem, lineCounter, changeSets); } lineCounter++; } deleteCommentAndBlankItem(baseCommentItems, baseBlankItems, changeSets); deleteNormalKVItem(oldKeyMapItem, changeSets); return changeSets; } private boolean isHasRepeatKey(String[] newItems, @NotNull Set repeatKeys) { Set keys = new HashSet<>(); int lineCounter = 1; for (String item : newItems) { if (!isCommentItem(item) && !isBlankItem(item)) { String[] kv = parseKeyValueFromItem(item); if (kv != null) { String key = kv[0].toLowerCase(); if (!keys.add(key)) { repeatKeys.add(key); } } else { throw new BadRequestException("line:" + lineCounter + " key value must separate by '='"); } } lineCounter++; } return !repeatKeys.isEmpty(); } private String[] parseKeyValueFromItem(String item) { int kvSeparator = item.indexOf(KV_SEPARATOR); if (kvSeparator == -1) { return null; } String[] kv = new String[2]; kv[0] = item.substring(0, kvSeparator).trim(); kv[1] = item.substring(kvSeparator + 1).trim(); return kv; } private void handleCommentLine(Long namespaceId, ItemDTO oldItemByLine, String newItem, int lineCounter, ItemChangeSets changeSets) { if (null == oldItemByLine) { changeSets.addCreateItem(buildCommentItem(0L, namespaceId, newItem, lineCounter)); } else if (!StringUtils.equals(oldItemByLine.getComment(), newItem) || lineCounter != oldItemByLine.getLineNum()) { changeSets.addUpdateItem( buildCommentItem(oldItemByLine.getId(), namespaceId, newItem, lineCounter)); } } private void handleBlankLine(Long namespaceId, ItemDTO oldItem, int lineCounter, ItemChangeSets changeSets) { if (null == oldItem) { changeSets.addCreateItem(buildBlankItem(0L, namespaceId, lineCounter)); } else if (lineCounter != oldItem.getLineNum()) { changeSets.addUpdateItem(buildBlankItem(oldItem.getId(), namespaceId, lineCounter)); } } private void handleNormalLine(Long namespaceId, Map keyMapOldItem, String newItem, int lineCounter, ItemChangeSets changeSets) { String[] kv = parseKeyValueFromItem(newItem); if (kv == null) { throw new BadRequestException("line:" + lineCounter + " key value must separate by '='"); } String newKey = kv[0]; String newValue = kv[1].replace("\\n", "\n"); // handle user input \n ItemDTO oldItem = keyMapOldItem.get(newKey); // new item if (oldItem == null) { changeSets.addCreateItem(buildNormalItem(0L, namespaceId, newKey, newValue, "", lineCounter)); // update item } else if (!StringUtils.equals(newValue, oldItem.getValue()) || lineCounter != oldItem.getLineNum()) { changeSets.addUpdateItem(buildNormalItem(oldItem.getId(), namespaceId, newKey, newValue, oldItem.getComment(), lineCounter)); } keyMapOldItem.remove(newKey); } private boolean isCommentItem(ItemDTO item) { return item != null && "".equals(item.getKey()) && (item.getComment().startsWith("#") || item.getComment().startsWith("!")); } private boolean isCommentItem(String line) { return line != null && (line.startsWith("#") || line.startsWith("!")); } private boolean isBlankItem(ItemDTO item) { return item != null && "".equals(item.getKey()) && "".equals(item.getComment()); } private boolean isBlankItem(String line) { return Strings.nullToEmpty(line).trim().isEmpty(); } private void deleteNormalKVItem(Map baseKeyMapItem, ItemChangeSets changeSets) { // surplus item is to be deleted for (Map.Entry entry : baseKeyMapItem.entrySet()) { changeSets.addDeleteItem(entry.getValue()); } } private void deleteCommentAndBlankItem(List baseCommentItems, List baseBlankItems, ItemChangeSets changeSets) { baseCommentItems.forEach(changeSets::addDeleteItem); baseBlankItems.forEach(changeSets::addDeleteItem); } private ItemDTO buildCommentItem(Long id, Long namespaceId, String comment, int lineNum) { return buildNormalItem(id, namespaceId, "", "", comment, lineNum); } private ItemDTO buildBlankItem(Long id, Long namespaceId, int lineNum) { return buildNormalItem(id, namespaceId, "", "", "", lineNum); } private ItemDTO buildNormalItem(Long id, Long namespaceId, String key, String value, String comment, int lineNum) { ItemDTO item = new ItemDTO(key, value, comment, lineNum); item.setId(id); item.setNamespaceId(namespaceId); return item; } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/constant/PermissionType.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.constant; public interface PermissionType { /** * system level permission */ String CREATE_APPLICATION = "CreateApplication"; String MANAGE_APP_MASTER = "ManageAppMaster"; /** * APP level permission */ String CREATE_NAMESPACE = "CreateNamespace"; String CREATE_CLUSTER = "CreateCluster"; /** * 分配用户权限的权限 */ String ASSIGN_ROLE = "AssignRole"; /** * namespace level permission */ String MODIFY_NAMESPACE = "ModifyNamespace"; String RELEASE_NAMESPACE = "ReleaseNamespace"; String MODIFY_NAMESPACES_IN_CLUSTER = "ModifyNamespacesInCluster"; String RELEASE_NAMESPACES_IN_CLUSTER = "ReleaseNamespacesInCluster"; } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/constant/RoleType.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.constant; public class RoleType { public static final String MASTER = "Master"; public static final String MODIFY_NAMESPACE = "ModifyNamespace"; public static final String RELEASE_NAMESPACE = "ReleaseNamespace"; public static final String MODIFY_NAMESPACES_IN_CLUSTER = "ModifyNamespacesInCluster"; public static final String RELEASE_NAMESPACES_IN_CLUSTER = "ReleaseNamespacesInCluster"; public static boolean isValidRoleType(String roleType) { return MASTER.equals(roleType) || MODIFY_NAMESPACE.equals(roleType) || RELEASE_NAMESPACE.equals(roleType) || MODIFY_NAMESPACES_IN_CLUSTER.equals(roleType) || RELEASE_NAMESPACES_IN_CLUSTER.equals(roleType); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/constant/TracerEventType.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.constant; public interface TracerEventType { String RELEASE_NAMESPACE = "Namespace.Release"; String MODIFY_NAMESPACE_BY_TEXT = "Namespace.Modify.Text"; String MODIFY_NAMESPACE = "Namespace.Modify"; String SYNC_NAMESPACE = "Namespace.Sync"; String CREATE_APP = "App.Create"; String CREATE_CLUSTER = "Cluster.Create"; String CREATE_ACCESS_KEY = "AccessKey.Create"; String CREATE_NAMESPACE = "Namespace.Create"; String API_RETRY = "API.Retry"; String USER_ACCESS = "User.Access"; String CREATE_GRAY_RELEASE = "GrayRelease.Create"; String DELETE_GRAY_RELEASE = "GrayRelease.Delete"; String MERGE_GRAY_RELEASE = "GrayRelease.Merge"; String UPDATE_GRAY_RELEASE_RULE = "GrayReleaseRule.Update"; } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/constant/UserIdentityConstants.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.constant; public final class UserIdentityConstants { public static final String USER = "USER"; public static final String CONSUMER = "CONSUMER"; public static final String ANONYMOUS = "ANONYMOUS"; private UserIdentityConstants() {} } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/AccessKeyController.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.controller; import static com.ctrip.framework.apollo.common.constants.AccessKeyMode.FILTER; import com.ctrip.framework.apollo.audit.annotation.ApolloAuditLog; import com.ctrip.framework.apollo.audit.annotation.OpType; import com.ctrip.framework.apollo.common.dto.AccessKeyDTO; import com.ctrip.framework.apollo.portal.environment.Env; import com.ctrip.framework.apollo.portal.service.AccessKeyService; import com.ctrip.framework.apollo.portal.spi.UserInfoHolder; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; import java.util.List; import java.util.UUID; /** * @author nisiyong */ @RestController public class AccessKeyController { private final UserInfoHolder userInfoHolder; private final AccessKeyService accessKeyService; public AccessKeyController(UserInfoHolder userInfoHolder, AccessKeyService accessKeyService) { this.userInfoHolder = userInfoHolder; this.accessKeyService = accessKeyService; } @PreAuthorize(value = "@unifiedPermissionValidator.isAppAdmin(#appId)") @PostMapping(value = "/apps/{appId}/envs/{env}/accesskeys") @ApolloAuditLog(type = OpType.CREATE, name = "AccessKey.create") public AccessKeyDTO save(@PathVariable String appId, @PathVariable String env, @RequestBody AccessKeyDTO accessKeyDTO) { String secret = UUID.randomUUID().toString().replaceAll("-", ""); accessKeyDTO.setAppId(appId); accessKeyDTO.setSecret(secret); return accessKeyService.createAccessKey(Env.valueOf(env), accessKeyDTO); } @PreAuthorize(value = "@unifiedPermissionValidator.isAppAdmin(#appId)") @GetMapping(value = "/apps/{appId}/envs/{env}/accesskeys") public List findByAppId(@PathVariable String appId, @PathVariable String env) { return accessKeyService.findByAppId(Env.valueOf(env), appId); } @PreAuthorize(value = "@unifiedPermissionValidator.isAppAdmin(#appId)") @DeleteMapping(value = "/apps/{appId}/envs/{env}/accesskeys/{id}") @ApolloAuditLog(type = OpType.DELETE, name = "AccessKey.delete") public void delete(@PathVariable String appId, @PathVariable String env, @PathVariable long id) { String operator = userInfoHolder.getUser().getUserId(); accessKeyService.deleteAccessKey(Env.valueOf(env), appId, id, operator); } @PreAuthorize(value = "@unifiedPermissionValidator.isAppAdmin(#appId)") @PutMapping(value = "/apps/{appId}/envs/{env}/accesskeys/{id}/enable") @ApolloAuditLog(type = OpType.UPDATE, name = "AccessKey.enable") public void enable(@PathVariable String appId, @PathVariable String env, @PathVariable long id, @RequestParam(required = false, defaultValue = "" + FILTER) int mode) { String operator = userInfoHolder.getUser().getUserId(); accessKeyService.enable(Env.valueOf(env), appId, id, mode, operator); } @PreAuthorize(value = "@unifiedPermissionValidator.isAppAdmin(#appId)") @PutMapping(value = "/apps/{appId}/envs/{env}/accesskeys/{id}/disable") @ApolloAuditLog(type = OpType.UPDATE, name = "AccessKey.disable") public void disable(@PathVariable String appId, @PathVariable String env, @PathVariable long id) { String operator = userInfoHolder.getUser().getUserId(); accessKeyService.disable(Env.valueOf(env), appId, id, operator); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/AppController.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.controller; import com.ctrip.framework.apollo.audit.annotation.ApolloAuditLog; import com.ctrip.framework.apollo.audit.annotation.OpType; import com.ctrip.framework.apollo.common.dto.AppDTO; import com.ctrip.framework.apollo.common.entity.App; import com.ctrip.framework.apollo.common.exception.BadRequestException; import com.ctrip.framework.apollo.common.http.MultiResponseEntity; import com.ctrip.framework.apollo.common.http.RichResponseEntity; import com.ctrip.framework.apollo.common.utils.BeanUtils; import com.ctrip.framework.apollo.core.ConfigConsts; import com.ctrip.framework.apollo.portal.component.PortalSettings; import com.ctrip.framework.apollo.portal.enricher.adapter.AppDtoUserInfoEnrichedAdapter; import com.ctrip.framework.apollo.portal.entity.bo.UserInfo; import com.ctrip.framework.apollo.portal.entity.model.AppModel; import com.ctrip.framework.apollo.portal.entity.po.Role; import com.ctrip.framework.apollo.portal.entity.vo.EnvClusterInfo; import com.ctrip.framework.apollo.portal.environment.Env; import com.ctrip.framework.apollo.portal.listener.AppDeletionEvent; import com.ctrip.framework.apollo.portal.listener.AppInfoChangedEvent; import com.ctrip.framework.apollo.portal.service.AdditionalUserInfoEnrichService; import com.ctrip.framework.apollo.portal.service.AppService; import com.ctrip.framework.apollo.portal.service.RoleInitializationService; import com.ctrip.framework.apollo.portal.service.RolePermissionService; import com.ctrip.framework.apollo.portal.spi.UserInfoHolder; import com.ctrip.framework.apollo.portal.util.RoleUtils; import com.google.common.base.Strings; import com.google.common.collect.Sets; import org.springframework.context.ApplicationEventPublisher; import org.springframework.data.domain.Pageable; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.HttpClientErrorException; import jakarta.validation.Valid; import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.Set; @RestController @RequestMapping("/apps") public class AppController { private final UserInfoHolder userInfoHolder; private final AppService appService; private final PortalSettings portalSettings; private final ApplicationEventPublisher publisher; private final RolePermissionService rolePermissionService; private final RoleInitializationService roleInitializationService; private final AdditionalUserInfoEnrichService additionalUserInfoEnrichService; public AppController(final UserInfoHolder userInfoHolder, final AppService appService, final PortalSettings portalSettings, final ApplicationEventPublisher publisher, final RolePermissionService rolePermissionService, final RoleInitializationService roleInitializationService, final AdditionalUserInfoEnrichService additionalUserInfoEnrichService) { this.userInfoHolder = userInfoHolder; this.appService = appService; this.portalSettings = portalSettings; this.publisher = publisher; this.rolePermissionService = rolePermissionService; this.roleInitializationService = roleInitializationService; this.additionalUserInfoEnrichService = additionalUserInfoEnrichService; } @GetMapping public List findApps(@RequestParam(value = "appIds", required = false) String appIds) { if (Strings.isNullOrEmpty(appIds)) { return appService.findAll(); } return appService.findByAppIds(Sets.newHashSet(appIds.split(","))); } @GetMapping("/by-self") public List findAppsBySelf(Pageable page) { UserInfo loginUser = userInfoHolder.getUser(); String userId = loginUser.getUserId(); Set appIds = Sets.newHashSet(); List userRoles = rolePermissionService.findUserRoles(userId); for (Role role : userRoles) { String appId = RoleUtils.extractAppIdFromRoleName(role.getRoleName()); if (appId != null) { appIds.add(appId); } } return appService.findByAppIds(appIds, page); } @PreAuthorize(value = "@unifiedPermissionValidator.hasCreateApplicationPermission()") @PostMapping @ApolloAuditLog(type = OpType.CREATE, name = "App.create") public App create(@Valid @RequestBody AppModel appModel) { App app = transformToApp(appModel); return appService.createAppAndAddRolePermission(app, appModel.getAdmins()); } @PreAuthorize(value = "@unifiedPermissionValidator.isAppAdmin(#appId)") @PutMapping("/{appId:.+}") @ApolloAuditLog(type = OpType.UPDATE, name = "App.update") public void update(@PathVariable String appId, @Valid @RequestBody AppModel appModel) { if (!Objects.equals(appId, appModel.getAppId())) { throw new BadRequestException("The App Id of path variable and request body is different"); } App app = transformToApp(appModel); App updatedApp = appService.updateAppInLocal(app); publisher.publishEvent(new AppInfoChangedEvent(updatedApp)); } @GetMapping("/{appId}/navtree") public MultiResponseEntity nav(@PathVariable String appId) { MultiResponseEntity response = MultiResponseEntity.ok(); List envs = portalSettings.getActiveEnvs(); for (Env env : envs) { try { response.addResponseEntity(RichResponseEntity.ok(appService.createEnvNavNode(env, appId))); } catch (Exception e) { response.addResponseEntity(RichResponseEntity.error(HttpStatus.INTERNAL_SERVER_ERROR, "load env:" + env.getName() + " cluster error." + e.getMessage())); } } return response; } @PostMapping(value = "/envs/{env}", consumes = {"application/json"}) @ApolloAuditLog(type = OpType.CREATE, name = "App.create.forEnv") public ResponseEntity create(@PathVariable String env, @Valid @RequestBody App app) { appService.createAppInRemote(Env.valueOf(env), app); roleInitializationService.initNamespaceSpecificEnvRoles(app.getAppId(), ConfigConsts.NAMESPACE_APPLICATION, env, userInfoHolder.getUser().getUserId()); return ResponseEntity.ok().build(); } @GetMapping("/{appId:.+}") public AppDTO load(@PathVariable String appId) { App app = appService.load(appId); AppDTO appDto = BeanUtils.transform(AppDTO.class, app); additionalUserInfoEnrichService.enrichAdditionalUserInfo(Collections.singletonList(appDto), AppDtoUserInfoEnrichedAdapter::new); return appDto; } @PreAuthorize(value = "@unifiedPermissionValidator.isSuperAdmin()") @DeleteMapping("/{appId:.+}") @ApolloAuditLog(type = OpType.RPC, name = "App.delete") public void deleteApp(@PathVariable String appId) { App app = appService.deleteAppInLocal(appId); publisher.publishEvent(new AppDeletionEvent(app)); } @GetMapping("/{appId}/miss_envs") public MultiResponseEntity findMissEnvs(@PathVariable String appId) { MultiResponseEntity response = MultiResponseEntity.ok(); for (Env env : portalSettings.getActiveEnvs()) { try { appService.load(env, appId); } catch (Exception e) { if (e instanceof HttpClientErrorException && ((HttpClientErrorException) e).getStatusCode() == HttpStatus.NOT_FOUND) { response.addResponseEntity(RichResponseEntity.ok(env.toString())); } else { response.addResponseEntity(RichResponseEntity.error(HttpStatus.INTERNAL_SERVER_ERROR, String.format("load appId:%s from env %s error.", appId, env) + e.getMessage())); } } } return response; } private App transformToApp(AppModel appModel) { String appId = appModel.getAppId(); String appName = appModel.getName(); String ownerName = appModel.getOwnerName(); String orgId = appModel.getOrgId(); String orgName = appModel.getOrgName(); return App.builder().appId(appId).name(appName).ownerName(ownerName).orgId(orgId) .orgName(orgName).build(); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/ClusterController.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.controller; import com.ctrip.framework.apollo.audit.annotation.ApolloAuditLog; import com.ctrip.framework.apollo.audit.annotation.OpType; import com.ctrip.framework.apollo.common.dto.ClusterDTO; import com.ctrip.framework.apollo.portal.environment.Env; import com.ctrip.framework.apollo.portal.service.ClusterService; import com.ctrip.framework.apollo.portal.spi.UserInfoHolder; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; import jakarta.validation.Valid; @RestController public class ClusterController { private final ClusterService clusterService; private final UserInfoHolder userInfoHolder; public ClusterController(final ClusterService clusterService, final UserInfoHolder userInfoHolder) { this.clusterService = clusterService; this.userInfoHolder = userInfoHolder; } @PreAuthorize(value = "@unifiedPermissionValidator.hasCreateClusterPermission(#appId)") @PostMapping(value = "apps/{appId}/envs/{env}/clusters") @ApolloAuditLog(type = OpType.CREATE, name = "Cluster.create") public ClusterDTO createCluster(@PathVariable String appId, @PathVariable String env, @Valid @RequestBody ClusterDTO cluster) { String operator = userInfoHolder.getUser().getUserId(); cluster.setDataChangeLastModifiedBy(operator); cluster.setDataChangeCreatedBy(operator); return clusterService.createCluster(Env.valueOf(env), cluster); } @PreAuthorize(value = "@unifiedPermissionValidator.isSuperAdmin()") @DeleteMapping(value = "apps/{appId}/envs/{env}/clusters/{clusterName:.+}") @ApolloAuditLog(type = OpType.DELETE, name = "Cluster.delete") public ResponseEntity deleteCluster(@PathVariable String appId, @PathVariable String env, @PathVariable String clusterName) { clusterService.deleteCluster(Env.valueOf(env), appId, clusterName); return ResponseEntity.ok().build(); } @GetMapping(value = "apps/{appId}/envs/{env}/clusters/{clusterName:.+}") public ClusterDTO loadCluster(@PathVariable("appId") String appId, @PathVariable String env, @PathVariable("clusterName") String clusterName) { return clusterService.loadCluster(appId, Env.valueOf(env), clusterName); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/CommitController.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.controller; import com.ctrip.framework.apollo.common.dto.CommitDTO; import com.ctrip.framework.apollo.core.utils.StringUtils; import com.ctrip.framework.apollo.portal.component.UnifiedPermissionValidator; import com.ctrip.framework.apollo.portal.environment.Env; import com.ctrip.framework.apollo.portal.service.CommitService; import jakarta.validation.Valid; import jakarta.validation.constraints.Positive; import jakarta.validation.constraints.PositiveOrZero; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.util.Collections; import java.util.List; @Validated @RestController public class CommitController { private final CommitService commitService; private final UnifiedPermissionValidator unifiedPermissionValidator; public CommitController(final CommitService commitService, final UnifiedPermissionValidator unifiedPermissionValidator) { this.commitService = commitService; this.unifiedPermissionValidator = unifiedPermissionValidator; } @GetMapping("/apps/{appId}/envs/{env}/clusters/{clusterName}/namespaces/{namespaceName}/commits") public List find(@PathVariable String appId, @PathVariable String env, @PathVariable String clusterName, @PathVariable String namespaceName, @RequestParam(required = false) String key, @Valid @PositiveOrZero(message = "page should be positive or 0") @RequestParam(defaultValue = "0") int page, @Valid @Positive(message = "size should be positive number") @RequestParam(defaultValue = "10") int size) { if (unifiedPermissionValidator.shouldHideConfigToCurrentUser(appId, env, clusterName, namespaceName)) { return Collections.emptyList(); } if (StringUtils.isEmpty(key)) { return commitService.find(appId, Env.valueOf(env), clusterName, namespaceName, page, size); } else { return commitService.findByKey(appId, Env.valueOf(env), clusterName, namespaceName, key, page, size); } } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/ConfigsExportController.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.controller; import com.google.common.base.Joiner; import com.google.common.base.Splitter; import com.ctrip.framework.apollo.common.exception.ServiceException; import com.ctrip.framework.apollo.core.enums.ConfigFileFormat; import com.ctrip.framework.apollo.portal.entity.bo.NamespaceBO; import com.ctrip.framework.apollo.portal.environment.Env; import com.ctrip.framework.apollo.portal.service.ConfigsExportService; import com.ctrip.framework.apollo.portal.service.NamespaceService; import com.ctrip.framework.apollo.portal.util.NamespaceBOUtils; import org.apache.commons.lang3.time.DateFormatUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Lazy; import org.springframework.http.HttpHeaders; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.io.IOException; import java.io.OutputStream; import java.util.Date; import java.util.List; import java.util.stream.Collectors; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; /** * jian.tan */ @RestController public class ConfigsExportController { private static final Logger logger = LoggerFactory.getLogger(ConfigsExportController.class); private static final String ENV_SEPARATOR = ","; private final ConfigsExportService configsExportService; private final NamespaceService namespaceService; public ConfigsExportController(final ConfigsExportService configsExportService, final @Lazy NamespaceService namespaceService) { this.configsExportService = configsExportService; this.namespaceService = namespaceService; } /** * export one config as file. * keep compatibility. * file name examples: *
   *   application.properties
   *   application.yml
   *   application.json
   * 
*/ @PreAuthorize( value = "!@unifiedPermissionValidator.shouldHideConfigToCurrentUser(#appId, #env, #clusterName, #namespaceName)") @GetMapping("/apps/{appId}/envs/{env}/clusters/{clusterName}/namespaces/{namespaceName}/items/export") public void exportItems(@PathVariable String appId, @PathVariable String env, @PathVariable String clusterName, @PathVariable String namespaceName, HttpServletResponse res) { List fileNameSplit = Splitter.on(".").splitToList(namespaceName); String fileName = namespaceName; // properties file or public namespace has not suffix (.properties) if (fileNameSplit.size() <= 1 || !ConfigFileFormat.isValidFormat(fileNameSplit.get(fileNameSplit.size() - 1))) { fileName = Joiner.on(".").join(namespaceName, ConfigFileFormat.Properties.getValue()); } NamespaceBO namespaceBO = namespaceService.loadNamespaceBO(appId, Env.valueOf(env), clusterName, namespaceName, true, false); // generate a file. res.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + fileName); // file content final String configFileContent = NamespaceBOUtils.convert2configFileContent(namespaceBO); try { // write content to net res.getOutputStream().write(configFileContent.getBytes()); } catch (Exception e) { throw new ServiceException("export items failed:{}", e); } } /** * Export all configs in a compressed file. Just export namespace which current exists read permission. The permission * check in service. */ @PreAuthorize(value = "@unifiedPermissionValidator.isSuperAdmin()") @GetMapping("/configs/export") public void exportAll(@RequestParam(value = "envs") String envs, HttpServletRequest request, HttpServletResponse response) throws IOException { // filename must contain the information of time final String filename = "apollo_config_export_" + DateFormatUtils.format(new Date(), "yyyy_MMdd_HH_mm_ss") + ".zip"; // log who download the configs logger.info("Download configs, remote addr [{}], remote host [{}]. Filename is [{}]", request.getRemoteAddr(), request.getRemoteHost(), filename); // set downloaded filename response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + filename); List exportEnvs = Splitter.on(ENV_SEPARATOR).splitToList(envs).stream().map(Env::valueOf) .collect(Collectors.toList()); try (OutputStream outputStream = response.getOutputStream()) { configsExportService.exportData(outputStream, exportEnvs); } } @PreAuthorize(value = "@unifiedPermissionValidator.isAppAdmin(#appId)") @GetMapping("/apps/{appId}/envs/{env}/clusters/{clusterName}/export") public void exportAppConfig(@PathVariable String appId, @PathVariable String env, @PathVariable String clusterName, HttpServletRequest request, HttpServletResponse response) throws IOException { // filename must contain the information of time final String filename = String.format("%s+%s+%s+%s.zip", appId, env, clusterName, DateFormatUtils.format(new Date(), "yyyy_MMdd_HH_mm_ss")); // log who download the configs logger.info( "Download configs, remote addr [{}], remote host [{}]. Filename is [{}]. AppId is [{}], env is [{}], clusterName is [{}]", request.getRemoteAddr(), request.getRemoteHost(), filename, appId, env, clusterName); // set downloaded filename response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + filename); try (OutputStream outputStream = response.getOutputStream()) { configsExportService.exportAppConfigByEnvAndCluster(appId, Env.valueOf(env), clusterName, outputStream); } } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/ConfigsImportController.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.controller; import com.ctrip.framework.apollo.common.exception.BadRequestException; import com.google.common.base.Splitter; import com.ctrip.framework.apollo.core.enums.ConfigFileFormat; import com.ctrip.framework.apollo.portal.environment.Env; import com.ctrip.framework.apollo.portal.service.ConfigsImportService; import com.ctrip.framework.apollo.portal.util.ConfigFileUtils; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; import java.io.ByteArrayInputStream; import java.io.IOException; import java.util.List; import java.util.stream.Collectors; import java.util.zip.ZipInputStream; /** * Import the configs from file. * First version: move code from {@link ConfigsExportController} * @author wxq */ @RestController public class ConfigsImportController { private static final String ENV_SEPARATOR = ","; private static final String CONFLICT_ACTION_IGNORE = "ignore"; private static final String CONFLICT_ACTION_COVER = "cover"; private final ConfigsImportService configsImportService; public ConfigsImportController(final ConfigsImportService configsImportService) { this.configsImportService = configsImportService; } /** * copy from old {@link ConfigsExportController}. * @param file Yml file's name must ends with {@code .yml}. * Properties file's name must ends with {@code .properties}. * etc. * @throws IOException */ @PreAuthorize( value = "@unifiedPermissionValidator.hasModifyNamespacePermission(#appId, #env, #clusterName, #namespaceName)") @PostMapping("/apps/{appId}/envs/{env}/clusters/{clusterName}/namespaces/{namespaceName}/items/import") public void importConfigFile(@PathVariable String appId, @PathVariable String env, @PathVariable String clusterName, @PathVariable String namespaceName, @RequestParam("file") MultipartFile file) throws IOException { // check file ConfigFileUtils.check(file); final String format = ConfigFileUtils.getFormat(file.getOriginalFilename()); final String standardFilename = ConfigFileUtils.toFilename(appId, clusterName, namespaceName, ConfigFileFormat.fromString(format)); configsImportService.forceImportNamespaceFromFile(Env.valueOf(env), standardFilename, file.getInputStream()); } @PreAuthorize(value = "@unifiedPermissionValidator.isSuperAdmin()") @PostMapping(value = "/configs/import") public void importConfigByZip(@RequestParam(value = "envs") String envs, @RequestParam(defaultValue = CONFLICT_ACTION_IGNORE) String conflictAction, @RequestParam("file") MultipartFile file) throws IOException { validateConflictAction(conflictAction); boolean ignoreConflictNamespace = conflictAction.equals(CONFLICT_ACTION_IGNORE); List importEnvs = Splitter.on(ENV_SEPARATOR).splitToList(envs).stream().map(Env::valueOf) .collect(Collectors.toList()); byte[] bytes = file.getBytes(); try (ZipInputStream zipInputStream = new ZipInputStream(new ByteArrayInputStream(bytes))) { configsImportService.importDataFromZipFile(importEnvs, zipInputStream, ignoreConflictNamespace); } } @PreAuthorize(value = "@unifiedPermissionValidator.isAppAdmin(#appId)") @PostMapping(value = "/apps/{appId}/envs/{env}/clusters/{clusterName}/import") public void importAppConfigByZip(@PathVariable String appId, @PathVariable String env, @PathVariable String clusterName, @RequestParam(defaultValue = CONFLICT_ACTION_IGNORE) String conflictAction, @RequestParam("file") MultipartFile file) throws IOException { validateConflictAction(conflictAction); boolean ignoreConflictNamespace = conflictAction.equals(CONFLICT_ACTION_IGNORE); byte[] bytes = file.getBytes(); try (ZipInputStream zipInputStream = new ZipInputStream(new ByteArrayInputStream(bytes))) { configsImportService.importAppConfigFromZipFile(appId, Env.valueOf(env), clusterName, zipInputStream, ignoreConflictNamespace); } } private void validateConflictAction(String conflictAction) { if (!conflictAction.equals(CONFLICT_ACTION_COVER) && !conflictAction.equals(CONFLICT_ACTION_IGNORE)) { throw new BadRequestException("ConflictAction is incorrect."); } } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/ConsumerController.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.controller; import com.ctrip.framework.apollo.common.dto.NamespaceDTO; import com.ctrip.framework.apollo.common.exception.BadRequestException; import com.ctrip.framework.apollo.core.utils.StringUtils; import com.ctrip.framework.apollo.openapi.entity.Consumer; import com.ctrip.framework.apollo.openapi.entity.ConsumerRole; import com.ctrip.framework.apollo.openapi.entity.ConsumerToken; import com.ctrip.framework.apollo.openapi.service.ConsumerService; import com.ctrip.framework.apollo.portal.entity.vo.consumer.ConsumerCreateRequestVO; import com.ctrip.framework.apollo.portal.entity.vo.consumer.ConsumerInfo; import com.ctrip.framework.apollo.portal.environment.Env; import com.google.common.base.Strings; import com.google.common.collect.Lists; import org.springframework.data.domain.Pageable; import org.springframework.format.annotation.DateTimeFormat; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.*; import java.util.*; /** * @author Jason Song(song_s@ctrip.com) */ @RestController public class ConsumerController { private static final Date DEFAULT_EXPIRES = new GregorianCalendar(2099, Calendar.JANUARY, 1).getTime(); private final ConsumerService consumerService; public ConsumerController(final ConsumerService consumerService) { this.consumerService = consumerService; } private Consumer convertToConsumer(ConsumerCreateRequestVO requestVO) { Consumer consumer = new Consumer(); consumer.setAppId(requestVO.getAppId()); consumer.setName(requestVO.getName()); consumer.setOwnerName(requestVO.getOwnerName()); consumer.setOrgId(requestVO.getOrgId()); consumer.setOrgName(requestVO.getOrgName()); return consumer; } @Transactional @PreAuthorize(value = "@unifiedPermissionValidator.isSuperAdmin()") @PostMapping(value = "/consumers") public ConsumerInfo create(@RequestBody ConsumerCreateRequestVO requestVO, @RequestParam(value = "expires", required = false) @DateTimeFormat(pattern = "yyyyMMddHHmmss") Date expires) { if (StringUtils.isBlank(requestVO.getAppId())) { throw BadRequestException.appIdIsBlank(); } if (StringUtils.isBlank(requestVO.getName())) { throw BadRequestException.appNameIsBlank(); } if (StringUtils.isBlank(requestVO.getOwnerName())) { throw BadRequestException.ownerNameIsBlank(); } if (StringUtils.isBlank(requestVO.getOrgId())) { throw BadRequestException.orgIdIsBlank(); } if (requestVO.isRateLimitEnabled()) { if (requestVO.getRateLimit() <= 0) { throw BadRequestException.rateLimitIsInvalid(); } } else { requestVO.setRateLimit(0); } Consumer createdConsumer = consumerService.createConsumer(convertToConsumer(requestVO)); if (Objects.isNull(expires)) { expires = DEFAULT_EXPIRES; } ConsumerToken consumerToken = consumerService.generateAndSaveConsumerToken(createdConsumer, requestVO.getRateLimit(), expires); if (requestVO.isAllowCreateApplication()) { consumerService.assignCreateApplicationRoleToConsumer(consumerToken.getToken()); } return consumerService.getConsumerInfoByAppId(requestVO.getAppId()); } @PreAuthorize(value = "@unifiedPermissionValidator.isSuperAdmin()") @GetMapping(value = "/consumer-tokens/by-appId") public ConsumerToken getConsumerTokenByAppId(@RequestParam String appId) { return consumerService.getConsumerTokenByAppId(appId); } @PreAuthorize(value = "@unifiedPermissionValidator.isSuperAdmin()") @GetMapping(value = "/consumer/info/by-appId") public ConsumerInfo getConsumerInfoByAppId(@RequestParam String appId) { return consumerService.getConsumerInfoByAppId(appId); } @PreAuthorize(value = "@unifiedPermissionValidator.isSuperAdmin()") @PostMapping(value = "/consumers/{token}/assign-role") public List assignNamespaceRoleToConsumer(@PathVariable String token, @RequestParam String type, @RequestParam(required = false) String envs, @RequestBody NamespaceDTO namespace) { List consumerRoleList = new ArrayList<>(8); String appId = namespace.getAppId(); String namespaceName = namespace.getNamespaceName(); if (StringUtils.isEmpty(appId)) { throw new BadRequestException("Params(AppId) can not be empty."); } if (Objects.equals("AppRole", type)) { return Collections.singletonList(consumerService.assignAppRoleToConsumer(token, appId)); } if (StringUtils.isEmpty(namespaceName)) { throw new BadRequestException("Params(NamespaceName) can not be empty."); } if (null != envs) { String[] envArray = envs.split(","); List envList = Lists.newArrayList(); // validate env parameter for (String env : envArray) { if (Strings.isNullOrEmpty(env)) { continue; } if (Env.UNKNOWN.equals(Env.transformEnv(env))) { throw BadRequestException.invalidEnvFormat(env); } envList.add(env); } List consumeRoles = new ArrayList<>(); for (String env : envList) { consumeRoles.addAll( consumerService.assignNamespaceRoleToConsumer(token, appId, namespaceName, env)); } return consumeRoles; } consumerRoleList .addAll(consumerService.assignNamespaceRoleToConsumer(token, appId, namespaceName)); return consumerRoleList; } @GetMapping("/consumers") @PreAuthorize(value = "@unifiedPermissionValidator.isSuperAdmin()") public List getConsumerList(Pageable page) { return consumerService.findConsumerInfoList(page); } @DeleteMapping(value = "/consumers/by-appId") @PreAuthorize(value = "@unifiedPermissionValidator.isSuperAdmin()") public void deleteConsumers(@RequestParam String appId) { consumerService.deleteConsumer(appId); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/EnvController.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.controller; import com.ctrip.framework.apollo.portal.component.PortalSettings; import com.ctrip.framework.apollo.portal.environment.Env; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.ArrayList; import java.util.List; @RestController @RequestMapping("/envs") public class EnvController { private final PortalSettings portalSettings; public EnvController(final PortalSettings portalSettings) { this.portalSettings = portalSettings; } @GetMapping public List envs() { List environments = new ArrayList<>(); for (Env env : portalSettings.getActiveEnvs()) { environments.add(env.toString()); } return environments; } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/FavoriteController.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.controller; import com.ctrip.framework.apollo.portal.entity.po.Favorite; import com.ctrip.framework.apollo.portal.service.FavoriteService; import org.springframework.data.domain.Pageable; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.util.List; @RestController public class FavoriteController { private final FavoriteService favoriteService; public FavoriteController(final FavoriteService favoriteService) { this.favoriteService = favoriteService; } @PostMapping("/favorites") public Favorite addFavorite(@RequestBody Favorite favorite) { return favoriteService.addFavorite(favorite); } @GetMapping("/favorites") public List findFavorites( @RequestParam(value = "userId", required = false) String userId, @RequestParam(value = "appId", required = false) String appId, Pageable page) { return favoriteService.search(userId, appId, page); } @DeleteMapping("/favorites/{favoriteId}") public void deleteFavorite(@PathVariable long favoriteId) { favoriteService.deleteFavorite(favoriteId); } @PutMapping("/favorites/{favoriteId}") public void toTop(@PathVariable long favoriteId) { favoriteService.adjustFavoriteToFirst(favoriteId); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/GlobalSearchController.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.controller; import com.ctrip.framework.apollo.common.exception.BadRequestException; import com.ctrip.framework.apollo.common.http.SearchResponseEntity; import com.ctrip.framework.apollo.portal.component.config.PortalConfig; import com.ctrip.framework.apollo.portal.entity.vo.ItemInfo; import com.ctrip.framework.apollo.portal.service.GlobalSearchService; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.util.List; @RestController public class GlobalSearchController { private final GlobalSearchService globalSearchService; private final PortalConfig portalConfig; public GlobalSearchController(final GlobalSearchService globalSearchService, final PortalConfig portalConfig) { this.globalSearchService = globalSearchService; this.portalConfig = portalConfig; } @PreAuthorize(value = "@unifiedPermissionValidator.isSuperAdmin()") @GetMapping("/global-search/item-info/by-key-or-value") public SearchResponseEntity> getItemInfoBySearch( @RequestParam(value = "key", required = false, defaultValue = "") String key, @RequestParam(value = "value", required = false, defaultValue = "") String value) { if (key.isEmpty() && value.isEmpty()) { throw new BadRequestException( "Please enter at least one search criterion in either key or value."); } return globalSearchService.getAllEnvItemInfoBySearch(key, value, 0, portalConfig.getPerEnvSearchMaxResults()); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/InstanceController.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.controller; import com.ctrip.framework.apollo.common.dto.InstanceDTO; import com.ctrip.framework.apollo.common.dto.PageDTO; import com.ctrip.framework.apollo.common.exception.BadRequestException; import com.ctrip.framework.apollo.portal.environment.Env; import com.ctrip.framework.apollo.portal.entity.vo.Number; import com.ctrip.framework.apollo.portal.service.InstanceService; import com.google.common.base.Splitter; import org.springframework.http.ResponseEntity; import org.springframework.util.CollectionUtils; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.util.List; import java.util.Set; import java.util.stream.Collectors; @RestController public class InstanceController { private static final Splitter RELEASES_SPLITTER = Splitter.on(",").omitEmptyStrings().trimResults(); private final InstanceService instanceService; public InstanceController(final InstanceService instanceService) { this.instanceService = instanceService; } @GetMapping("/envs/{env}/instances/by-release") public PageDTO getByRelease(@PathVariable String env, @RequestParam long releaseId, @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "20") int size) { return instanceService.getByRelease(Env.valueOf(env), releaseId, page, size); } @GetMapping("/envs/{env}/instances/by-namespace") public PageDTO getByNamespace(@PathVariable String env, @RequestParam String appId, @RequestParam String clusterName, @RequestParam String namespaceName, @RequestParam(required = false) String instanceAppId, @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "20") int size) { return instanceService.getByNamespace(Env.valueOf(env), appId, clusterName, namespaceName, instanceAppId, page, size); } @GetMapping("/envs/{env}/instances/by-namespace/count") public ResponseEntity getInstanceCountByNamespace(@PathVariable String env, @RequestParam String appId, @RequestParam String clusterName, @RequestParam String namespaceName) { int count = instanceService.getInstanceCountByNamespace(appId, Env.valueOf(env), clusterName, namespaceName); return ResponseEntity.ok(new Number(count)); } @GetMapping("/envs/{env}/instances/by-namespace-and-releases-not-in") public List getByReleasesNotIn(@PathVariable String env, @RequestParam String appId, @RequestParam String clusterName, @RequestParam String namespaceName, @RequestParam String releaseIds) { Set releaseIdSet = RELEASES_SPLITTER.splitToList(releaseIds).stream().map(Long::parseLong) .collect(Collectors.toSet()); if (CollectionUtils.isEmpty(releaseIdSet)) { throw new BadRequestException("release ids can not be empty"); } return instanceService.getByReleasesNotIn(Env.valueOf(env), appId, clusterName, namespaceName, releaseIdSet); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/ItemController.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.controller; import com.ctrip.framework.apollo.common.dto.ItemChangeSets; import com.ctrip.framework.apollo.common.dto.ItemDTO; import com.ctrip.framework.apollo.common.dto.NamespaceDTO; import com.ctrip.framework.apollo.common.exception.BadRequestException; import com.ctrip.framework.apollo.core.enums.ConfigFileFormat; import com.ctrip.framework.apollo.portal.component.UnifiedPermissionValidator; import com.ctrip.framework.apollo.portal.environment.Env; import com.ctrip.framework.apollo.core.utils.StringUtils; import com.ctrip.framework.apollo.portal.entity.model.NamespaceSyncModel; import com.ctrip.framework.apollo.portal.entity.model.NamespaceTextModel; import com.ctrip.framework.apollo.portal.entity.vo.ItemDiffs; import com.ctrip.framework.apollo.portal.entity.vo.NamespaceIdentifier; import com.ctrip.framework.apollo.portal.service.ItemService; import com.ctrip.framework.apollo.portal.service.NamespaceService; import com.ctrip.framework.apollo.portal.spi.UserInfoHolder; import org.springframework.beans.factory.config.YamlPropertiesFactoryBean; import org.springframework.core.io.ByteArrayResource; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.util.Collections; import java.util.List; import java.util.Objects; import org.yaml.snakeyaml.DumperOptions; import org.yaml.snakeyaml.LoaderOptions; import org.yaml.snakeyaml.Yaml; import org.yaml.snakeyaml.constructor.SafeConstructor; import org.yaml.snakeyaml.representer.Representer; import static com.ctrip.framework.apollo.common.utils.RequestPrecondition.checkModel; @RestController public class ItemController { private final ItemService configService; private final NamespaceService namespaceService; private final UserInfoHolder userInfoHolder; private final UnifiedPermissionValidator unifiedPermissionValidator; public ItemController(final ItemService configService, final UserInfoHolder userInfoHolder, final NamespaceService namespaceService, final UnifiedPermissionValidator unifiedPermissionValidator) { this.configService = configService; this.userInfoHolder = userInfoHolder; this.unifiedPermissionValidator = unifiedPermissionValidator; this.namespaceService = namespaceService; } @PreAuthorize( value = "@unifiedPermissionValidator.hasModifyNamespacePermission(#appId, #env, #clusterName, #namespaceName)") @PutMapping( value = "/apps/{appId}/envs/{env}/clusters/{clusterName}/namespaces/{namespaceName}/items", consumes = {"application/json"}) public void modifyItemsByText(@PathVariable String appId, @PathVariable String env, @PathVariable String clusterName, @PathVariable String namespaceName, @RequestBody NamespaceTextModel model) { model.setAppId(appId); model.setClusterName(clusterName); model.setEnv(env); model.setNamespaceName(namespaceName); configService.updateConfigItemByText(model); } @PreAuthorize( value = "@unifiedPermissionValidator.hasModifyNamespacePermission(#appId, #env, #clusterName, #namespaceName)") @PostMapping("/apps/{appId}/envs/{env}/clusters/{clusterName}/namespaces/{namespaceName}/item") public ItemDTO createItem(@PathVariable String appId, @PathVariable String env, @PathVariable String clusterName, @PathVariable String namespaceName, @RequestBody ItemDTO item) { checkModel(isValidItem(item)); // protect item.setLineNum(0); item.setId(0); String userId = userInfoHolder.getUser().getUserId(); item.setDataChangeCreatedBy(userId); item.setDataChangeLastModifiedBy(userId); item.setDataChangeCreatedTime(null); item.setDataChangeLastModifiedTime(null); return configService.createItem(appId, Env.valueOf(env), clusterName, namespaceName, item); } @PreAuthorize( value = "@unifiedPermissionValidator.hasModifyNamespacePermission(#appId, #env, #clusterName, #namespaceName)") @PutMapping("/apps/{appId}/envs/{env}/clusters/{clusterName}/namespaces/{namespaceName}/item") public void updateItem(@PathVariable String appId, @PathVariable String env, @PathVariable String clusterName, @PathVariable String namespaceName, @RequestBody ItemDTO item) { checkModel(isValidItem(item)); String username = userInfoHolder.getUser().getUserId(); item.setDataChangeLastModifiedBy(username); configService.updateItem(appId, Env.valueOf(env), clusterName, namespaceName, item); } @PreAuthorize( value = "@unifiedPermissionValidator.hasModifyNamespacePermission(#appId, #env, #clusterName, #namespaceName)") @DeleteMapping("/apps/{appId}/envs/{env}/clusters/{clusterName}/namespaces/{namespaceName}/items/{itemId}") public void deleteItem(@PathVariable String appId, @PathVariable String env, @PathVariable String clusterName, @PathVariable String namespaceName, @PathVariable long itemId) { ItemDTO item = configService.loadItemById(Env.valueOf(env), itemId); NamespaceDTO namespace = namespaceService.loadNamespaceBaseInfo(appId, Env.valueOf(env), clusterName, namespaceName); // In case someone constructs an attack scenario if (namespace == null || item.getNamespaceId() != namespace.getId()) { throw BadRequestException.namespaceNotMatch(); } configService.deleteItem(Env.valueOf(env), itemId, userInfoHolder.getUser().getUserId()); } @GetMapping("/apps/{appId}/envs/{env}/clusters/{clusterName}/namespaces/{namespaceName}/items") public List findItems(@PathVariable String appId, @PathVariable String env, @PathVariable String clusterName, @PathVariable String namespaceName, @RequestParam(defaultValue = "lineNum") String orderBy) { if (unifiedPermissionValidator.shouldHideConfigToCurrentUser(appId, env, clusterName, namespaceName)) { return Collections.emptyList(); } List items = configService.findItems(appId, Env.valueOf(env), clusterName, namespaceName); if ("lastModifiedTime".equals(orderBy)) { items.sort((o1, o2) -> { if (o1.getDataChangeLastModifiedTime().after(o2.getDataChangeLastModifiedTime())) { return -1; } if (o1.getDataChangeLastModifiedTime().before(o2.getDataChangeLastModifiedTime())) { return 1; } return 0; }); } return items; } @GetMapping("/apps/{appId}/envs/{env}/clusters/{clusterName}/namespaces/{namespaceName}/branches/{branchName}/items") public List findBranchItems(@PathVariable("appId") String appId, @PathVariable String env, @PathVariable("clusterName") String clusterName, @PathVariable("namespaceName") String namespaceName, @PathVariable("branchName") String branchName) { return findItems(appId, env, branchName, namespaceName, "lastModifiedTime"); } @PostMapping(value = "/namespaces/{namespaceName}/diff", consumes = {"application/json"}) public List diff(@RequestBody NamespaceSyncModel model) { checkModel(!model.isInvalid()); List itemDiffs = configService.compare(model.getSyncToNamespaces(), model.getSyncItems()); for (ItemDiffs diff : itemDiffs) { NamespaceIdentifier namespace = diff.getNamespace(); if (namespace == null) { continue; } if (unifiedPermissionValidator.shouldHideConfigToCurrentUser(namespace.getAppId(), namespace.getEnv().getName(), namespace.getClusterName(), namespace.getNamespaceName())) { diff.setDiffs(new ItemChangeSets()); diff.setExtInfo( "You are not this project's administrator, nor you have edit or release permission for the namespace: " + namespace); } } return itemDiffs; } @PutMapping(value = "/apps/{appId}/namespaces/{namespaceName}/items", consumes = {"application/json"}) public ResponseEntity update(@PathVariable String appId, @PathVariable String namespaceName, @RequestBody NamespaceSyncModel model) { checkModel(!model.isInvalid() && model.syncToNamespacesValid(appId, namespaceName)); NamespaceIdentifier noPermissionNamespace = null; // check if user has every namespace's ModifyNamespace permission boolean hasPermission = true; for (NamespaceIdentifier namespaceIdentifier : model.getSyncToNamespaces()) { // once user has not one of the namespace's ModifyNamespace permission, then break the loop hasPermission = unifiedPermissionValidator.hasModifyNamespacePermission( namespaceIdentifier.getAppId(), namespaceIdentifier.getEnv().getName(), namespaceIdentifier.getClusterName(), namespaceIdentifier.getNamespaceName()); if (!hasPermission) { noPermissionNamespace = namespaceIdentifier; break; } } if (hasPermission) { configService.syncItems(model.getSyncToNamespaces(), model.getSyncItems()); return ResponseEntity.status(HttpStatus.OK).build(); } throw new AccessDeniedException(String .format("You don't have the permission to modify namespace: %s", noPermissionNamespace)); } @PreAuthorize( value = "@unifiedPermissionValidator.hasModifyNamespacePermission(#appId, #env, #clusterName, #namespaceName)") @PostMapping( value = "/apps/{appId}/envs/{env}/clusters/{clusterName}/namespaces/{namespaceName}/syntax-check", consumes = {"application/json"}) public ResponseEntity syntaxCheckText(@PathVariable String appId, @PathVariable String env, @PathVariable String clusterName, @PathVariable String namespaceName, @RequestBody NamespaceTextModel model) { doSyntaxCheck(model); return ResponseEntity.ok().build(); } @PreAuthorize( value = "@unifiedPermissionValidator.hasModifyNamespacePermission(#appId, #env, #clusterName, #namespaceName)") @PutMapping("/apps/{appId}/envs/{env}/clusters/{clusterName}/namespaces/{namespaceName}/revoke-items") public void revokeItems(@PathVariable String appId, @PathVariable String env, @PathVariable String clusterName, @PathVariable String namespaceName) { configService.revokeItem(appId, Env.valueOf(env), clusterName, namespaceName); } void doSyntaxCheck(NamespaceTextModel model) { if (StringUtils.isBlank(model.getConfigText())) { return; } // only support yaml syntax check if (model.getFormat() != ConfigFileFormat.YAML && model.getFormat() != ConfigFileFormat.YML) { return; } // use YamlPropertiesFactoryBean to check the yaml syntax TypeLimitedYamlPropertiesFactoryBean yamlPropertiesFactoryBean = new TypeLimitedYamlPropertiesFactoryBean(); yamlPropertiesFactoryBean.setResources(new ByteArrayResource(model.getConfigText().getBytes())); try { // this call converts yaml to properties and will throw exception if the conversion fails yamlPropertiesFactoryBean.getObject(); } catch (Exception ex) { throw new BadRequestException(ex.getMessage()); } } private boolean isValidItem(ItemDTO item) { return Objects.nonNull(item) && !StringUtils.isContainEmpty(item.getKey()); } private static class TypeLimitedYamlPropertiesFactoryBean extends YamlPropertiesFactoryBean { @Override protected Yaml createYaml() { LoaderOptions loaderOptions = new LoaderOptions(); loaderOptions.setAllowDuplicateKeys(false); DumperOptions dumperOptions = new DumperOptions(); return new Yaml(new SafeConstructor(loaderOptions), new Representer(dumperOptions), dumperOptions, loaderOptions); } } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/NamespaceBranchController.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.controller; import com.ctrip.framework.apollo.audit.annotation.ApolloAuditLog; import com.ctrip.framework.apollo.audit.annotation.OpType; import com.ctrip.framework.apollo.common.dto.GrayReleaseRuleDTO; import com.ctrip.framework.apollo.common.dto.NamespaceDTO; import com.ctrip.framework.apollo.common.dto.ReleaseDTO; import com.ctrip.framework.apollo.common.exception.BadRequestException; import com.ctrip.framework.apollo.portal.component.UnifiedPermissionValidator; import com.ctrip.framework.apollo.portal.environment.Env; import com.ctrip.framework.apollo.portal.component.config.PortalConfig; import com.ctrip.framework.apollo.portal.entity.bo.NamespaceBO; import com.ctrip.framework.apollo.portal.entity.model.NamespaceReleaseModel; import com.ctrip.framework.apollo.portal.listener.ConfigPublishEvent; import com.ctrip.framework.apollo.portal.service.NamespaceBranchService; import com.ctrip.framework.apollo.portal.service.ReleaseService; import org.springframework.context.ApplicationEventPublisher; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController public class NamespaceBranchController { private final ReleaseService releaseService; private final NamespaceBranchService namespaceBranchService; private final ApplicationEventPublisher publisher; private final PortalConfig portalConfig; private final UnifiedPermissionValidator unifiedPermissionValidator; public NamespaceBranchController(final ReleaseService releaseService, final NamespaceBranchService namespaceBranchService, final ApplicationEventPublisher publisher, final PortalConfig portalConfig, UnifiedPermissionValidator unifiedPermissionValidator) { this.releaseService = releaseService; this.namespaceBranchService = namespaceBranchService; this.publisher = publisher; this.portalConfig = portalConfig; this.unifiedPermissionValidator = unifiedPermissionValidator; } @GetMapping("/apps/{appId}/envs/{env}/clusters/{clusterName}/namespaces/{namespaceName}/branches") public NamespaceBO findBranch(@PathVariable String appId, @PathVariable String env, @PathVariable String clusterName, @PathVariable String namespaceName) { NamespaceBO namespaceBO = namespaceBranchService.findBranch(appId, Env.valueOf(env), clusterName, namespaceName); if (namespaceBO != null && unifiedPermissionValidator.shouldHideConfigToCurrentUser(appId, env, clusterName, namespaceName)) { namespaceBO.hideItems(); } return namespaceBO; } @PreAuthorize( value = "@unifiedPermissionValidator.hasModifyNamespacePermission(#appId, #env, #clusterName, #namespaceName)") @PostMapping( value = "/apps/{appId}/envs/{env}/clusters/{clusterName}/namespaces/{namespaceName}/branches") @ApolloAuditLog(type = OpType.CREATE, name = "NamespaceBranch.create") public NamespaceDTO createBranch(@PathVariable String appId, @PathVariable String env, @PathVariable String clusterName, @PathVariable String namespaceName) { return namespaceBranchService.createBranch(appId, Env.valueOf(env), clusterName, namespaceName); } @DeleteMapping( value = "/apps/{appId}/envs/{env}/clusters/{clusterName}/namespaces/{namespaceName}/branches/{branchName}") @ApolloAuditLog(type = OpType.DELETE, name = "NamespaceBranch.delete") public void deleteBranch(@PathVariable String appId, @PathVariable String env, @PathVariable String clusterName, @PathVariable String namespaceName, @PathVariable String branchName) { boolean hasModifyPermission = unifiedPermissionValidator.hasModifyNamespacePermission(appId, env, clusterName, namespaceName); boolean hasReleasePermission = unifiedPermissionValidator.hasReleaseNamespacePermission(appId, env, clusterName, namespaceName); boolean canDelete = hasReleasePermission || (hasModifyPermission && releaseService .loadLatestRelease(appId, Env.valueOf(env), branchName, namespaceName) == null); if (!canDelete) { throw new AccessDeniedException( "Forbidden operation. " + "Caused by: 1.you don't have release permission " + "or 2. you don't have modification permission " + "or 3. you have modification permission but branch has been released"); } namespaceBranchService.deleteBranch(appId, Env.valueOf(env), clusterName, namespaceName, branchName); } @PreAuthorize( value = "@unifiedPermissionValidator.hasModifyNamespacePermission(#appId, #env, #clusterName, #namespaceName)") @PostMapping( value = "/apps/{appId}/envs/{env}/clusters/{clusterName}/namespaces/{namespaceName}/branches/{branchName}/merge") @ApolloAuditLog(type = OpType.UPDATE, name = "NamespaceBranch.merge") public ReleaseDTO merge(@PathVariable String appId, @PathVariable String env, @PathVariable String clusterName, @PathVariable String namespaceName, @PathVariable String branchName, @RequestParam(value = "deleteBranch", defaultValue = "true") boolean deleteBranch, @RequestBody NamespaceReleaseModel model) { if (model.isEmergencyPublish() && !portalConfig.isEmergencyPublishAllowed(Env.valueOf(env))) { throw new BadRequestException("Env: %s is not supported emergency publish now", env); } ReleaseDTO createdRelease = namespaceBranchService.merge(appId, Env.valueOf(env), clusterName, namespaceName, branchName, model.getReleaseTitle(), model.getReleaseComment(), model.isEmergencyPublish(), deleteBranch); ConfigPublishEvent event = ConfigPublishEvent.instance(); event.withAppId(appId).withCluster(clusterName).withNamespace(namespaceName) .withReleaseId(createdRelease.getId()).setMergeEvent(true).setEnv(Env.valueOf(env)); publisher.publishEvent(event); return createdRelease; } @GetMapping( value = "/apps/{appId}/envs/{env}/clusters/{clusterName}/namespaces/{namespaceName}/branches/{branchName}/rules") public GrayReleaseRuleDTO getBranchGrayRules(@PathVariable String appId, @PathVariable String env, @PathVariable String clusterName, @PathVariable String namespaceName, @PathVariable String branchName) { return namespaceBranchService.findBranchGrayRules(appId, Env.valueOf(env), clusterName, namespaceName, branchName); } @PreAuthorize( value = "@unifiedPermissionValidator.hasOperateNamespacePermission(#appId, #env, #clusterName, #namespaceName)") @PutMapping( value = "/apps/{appId}/envs/{env}/clusters/{clusterName}/namespaces/{namespaceName}/branches/{branchName}/rules") @ApolloAuditLog(type = OpType.UPDATE, name = "NamespaceBranch.updateBranchRules") public void updateBranchRules(@PathVariable String appId, @PathVariable String env, @PathVariable String clusterName, @PathVariable String namespaceName, @PathVariable String branchName, @RequestBody GrayReleaseRuleDTO rules) { namespaceBranchService.updateBranchGrayRules(appId, Env.valueOf(env), clusterName, namespaceName, branchName, rules); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/NamespaceController.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.controller; import com.ctrip.framework.apollo.audit.annotation.ApolloAuditLog; import com.ctrip.framework.apollo.audit.annotation.OpType; import com.ctrip.framework.apollo.common.dto.AppNamespaceDTO; import com.ctrip.framework.apollo.common.dto.NamespaceDTO; import com.ctrip.framework.apollo.common.entity.AppNamespace; import com.ctrip.framework.apollo.common.exception.BadRequestException; import com.ctrip.framework.apollo.common.http.MultiResponseEntity; import com.ctrip.framework.apollo.common.http.RichResponseEntity; import com.ctrip.framework.apollo.common.utils.BeanUtils; import com.ctrip.framework.apollo.common.utils.InputValidator; import com.ctrip.framework.apollo.common.utils.RequestPrecondition; import com.ctrip.framework.apollo.portal.component.UnifiedPermissionValidator; import com.ctrip.framework.apollo.portal.entity.vo.NamespaceUsage; import com.ctrip.framework.apollo.portal.environment.Env; import com.ctrip.framework.apollo.portal.api.AdminServiceAPI; import com.ctrip.framework.apollo.portal.component.config.PortalConfig; import com.ctrip.framework.apollo.portal.entity.bo.NamespaceBO; import com.ctrip.framework.apollo.portal.entity.model.NamespaceCreationModel; import com.ctrip.framework.apollo.portal.listener.AppNamespaceCreationEvent; import com.ctrip.framework.apollo.portal.listener.AppNamespaceDeletionEvent; import com.ctrip.framework.apollo.portal.service.AppNamespaceService; import com.ctrip.framework.apollo.portal.service.NamespaceService; import com.ctrip.framework.apollo.portal.service.RoleInitializationService; import com.ctrip.framework.apollo.portal.spi.UserInfoHolder; import com.ctrip.framework.apollo.tracer.Tracer; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.ApplicationEventPublisher; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.util.CollectionUtils; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import jakarta.validation.Valid; import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; import static com.ctrip.framework.apollo.common.utils.RequestPrecondition.checkModel; @RestController public class NamespaceController { private static final Logger logger = LoggerFactory.getLogger(NamespaceController.class); private final ApplicationEventPublisher publisher; private final UserInfoHolder userInfoHolder; private final NamespaceService namespaceService; private final AppNamespaceService appNamespaceService; private final RoleInitializationService roleInitializationService; private final PortalConfig portalConfig; private final UnifiedPermissionValidator unifiedPermissionValidator; private final AdminServiceAPI.NamespaceAPI namespaceAPI; public NamespaceController(final ApplicationEventPublisher publisher, final UserInfoHolder userInfoHolder, final NamespaceService namespaceService, final AppNamespaceService appNamespaceService, final RoleInitializationService roleInitializationService, final PortalConfig portalConfig, final UnifiedPermissionValidator unifiedPermissionValidator, final AdminServiceAPI.NamespaceAPI namespaceAPI) { this.publisher = publisher; this.userInfoHolder = userInfoHolder; this.namespaceService = namespaceService; this.appNamespaceService = appNamespaceService; this.roleInitializationService = roleInitializationService; this.portalConfig = portalConfig; this.unifiedPermissionValidator = unifiedPermissionValidator; this.namespaceAPI = namespaceAPI; } @GetMapping("/appnamespaces/public") public List findPublicAppNamespaces() { return appNamespaceService.findPublicAppNamespaces(); } @GetMapping("/appnamespaces/public/names") public List findPublicAppNamespaceNames() { return appNamespaceService.findPublicAppNamespaceNames(); } @GetMapping("/apps/{appId}/envs/{env}/clusters/{clusterName}/namespaces") public List findNamespaces(@PathVariable String appId, @PathVariable String env, @PathVariable String clusterName) { List namespaceBOs = namespaceService.findNamespaceBOs(appId, Env.valueOf(env), clusterName); for (NamespaceBO namespaceBO : namespaceBOs) { if (unifiedPermissionValidator.shouldHideConfigToCurrentUser(appId, env, clusterName, namespaceBO.getBaseInfo().getNamespaceName())) { namespaceBO.hideItems(); } } return namespaceBOs; } @GetMapping("/apps/{appId}/envs/{env}/clusters/{clusterName}/namespaces/{namespaceName:.+}") public NamespaceBO findNamespace(@PathVariable String appId, @PathVariable String env, @PathVariable String clusterName, @PathVariable String namespaceName) { NamespaceBO namespaceBO = namespaceService.loadNamespaceBO(appId, Env.valueOf(env), clusterName, namespaceName); if (namespaceBO != null && unifiedPermissionValidator.shouldHideConfigToCurrentUser(appId, env, clusterName, namespaceName)) { namespaceBO.hideItems(); } return namespaceBO; } @GetMapping("/envs/{env}/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/associated-public-namespace") public NamespaceBO findPublicNamespaceForAssociatedNamespace(@PathVariable String env, @PathVariable String appId, @PathVariable String namespaceName, @PathVariable String clusterName) { return namespaceService.findPublicNamespaceForAssociatedNamespace(Env.valueOf(env), appId, clusterName, namespaceName); } @PreAuthorize(value = "@unifiedPermissionValidator.hasCreateNamespacePermission(#appId)") @PostMapping("/apps/{appId}/namespaces") @ApolloAuditLog(type = OpType.CREATE, name = "Namespace.create") public ResponseEntity createNamespace(@PathVariable String appId, @RequestBody List models) { checkModel(!CollectionUtils.isEmpty(models)); String operator = userInfoHolder.getUser().getUserId(); for (NamespaceCreationModel model : models) { String namespaceName = model.getNamespace().getNamespaceName(); roleInitializationService.initNamespaceRoles(appId, namespaceName, operator); roleInitializationService.initNamespaceEnvRoles(appId, namespaceName, operator); NamespaceDTO namespace = model.getNamespace(); RequestPrecondition.checkArgumentsNotEmpty(model.getEnv(), namespace.getAppId(), namespace.getClusterName(), namespace.getNamespaceName()); try { namespaceService.createNamespace(Env.valueOf(model.getEnv()), namespace); } catch (Exception e) { logger.error("create namespace fail.", e); Tracer.logError(String.format("create namespace fail. (env=%s namespace=%s)", model.getEnv(), namespace.getNamespaceName()), e); } namespaceService.assignNamespaceRoleToOperator(appId, namespaceName, userInfoHolder.getUser().getUserId()); } return ResponseEntity.ok().build(); } @PreAuthorize(value = "@unifiedPermissionValidator.hasDeleteNamespacePermission(#appId)") @DeleteMapping("/apps/{appId}/envs/{env}/clusters/{clusterName}/linked-namespaces/{namespaceName:.+}") @ApolloAuditLog(type = OpType.DELETE, name = "Namespace.deleteLinkedNamespace") public ResponseEntity deleteLinkedNamespace(@PathVariable String appId, @PathVariable String env, @PathVariable String clusterName, @PathVariable String namespaceName) { namespaceService.deleteNamespace(appId, Env.valueOf(env), clusterName, namespaceName); return ResponseEntity.ok().build(); } @GetMapping("/apps/{appId}/envs/{env}/clusters/{clusterName}/linked-namespaces/{namespaceName}/usage") public List findLinkedNamespaceUsage(@PathVariable String appId, @PathVariable String env, @PathVariable String clusterName, @PathVariable String namespaceName) { NamespaceUsage usage = namespaceService.getNamespaceUsageByEnv(appId, namespaceName, Env.valueOf(env), clusterName); return Lists.newArrayList(usage); } @GetMapping("/apps/{appId}/namespaces/{namespaceName}/usage") public List findNamespaceUsage(@PathVariable String appId, @PathVariable String namespaceName) { return namespaceService.getNamespaceUsageByAppId(appId, namespaceName); } @PreAuthorize(value = "@unifiedPermissionValidator.hasDeleteNamespacePermission(#appId)") @DeleteMapping("/apps/{appId}/appnamespaces/{namespaceName:.+}") @ApolloAuditLog(type = OpType.DELETE, name = "AppNamespace.delete") public ResponseEntity deleteAppNamespace(@PathVariable String appId, @PathVariable String namespaceName) { AppNamespace appNamespace = appNamespaceService.deleteAppNamespace(appId, namespaceName); publisher.publishEvent(new AppNamespaceDeletionEvent(appNamespace)); return ResponseEntity.ok().build(); } @GetMapping("/apps/{appId}/appnamespaces/{namespaceName:.+}") public AppNamespaceDTO findAppNamespace(@PathVariable String appId, @PathVariable String namespaceName) { AppNamespace appNamespace = appNamespaceService.findByAppIdAndName(appId, namespaceName); if (appNamespace == null) { throw BadRequestException.appNamespaceNotExists(appId, namespaceName); } return BeanUtils.transform(AppNamespaceDTO.class, appNamespace); } @PreAuthorize( value = "@unifiedPermissionValidator.hasCreateAppNamespacePermission(#appId, #appNamespace)") @PostMapping("/apps/{appId}/appnamespaces") @ApolloAuditLog(type = OpType.CREATE, name = "AppNamespace.create") public AppNamespace createAppNamespace(@PathVariable String appId, @RequestParam(defaultValue = "true") boolean appendNamespacePrefix, @Valid @RequestBody AppNamespace appNamespace) { if (!InputValidator.isValidAppNamespace(appNamespace.getName())) { throw BadRequestException .invalidNamespaceFormat(InputValidator.INVALID_CLUSTER_NAMESPACE_MESSAGE + " & " + InputValidator.INVALID_NAMESPACE_NAMESPACE_MESSAGE); } AppNamespace createdAppNamespace = appNamespaceService.createAppNamespaceInLocal(appNamespace, appendNamespacePrefix); if (portalConfig.canAppAdminCreatePrivateNamespace() || createdAppNamespace.isPublic()) { namespaceService.assignNamespaceRoleToOperator(appId, appNamespace.getName(), userInfoHolder.getUser().getUserId()); } publisher.publishEvent(new AppNamespaceCreationEvent(createdAppNamespace)); return createdAppNamespace; } /** * env -> cluster -> cluster has not published namespace? * Example: * dev -> * default -> true (default cluster has not published namespace) * customCluster -> false (customCluster cluster's all namespaces had published) */ @GetMapping("/apps/{appId}/namespaces/publish_info") public Map> getNamespacesPublishInfo(@PathVariable String appId) { return namespaceService.getNamespacesPublishInfo(appId); } @GetMapping("/envs/{env}/appnamespaces/{publicNamespaceName}/namespaces") public List getPublicAppNamespaceAllNamespaces(@PathVariable String env, @PathVariable String publicNamespaceName, @RequestParam(name = "page", defaultValue = "0") int page, @RequestParam(name = "size", defaultValue = "10") int size) { return namespaceService.getPublicAppNamespaceAllNamespaces(Env.valueOf(env), publicNamespaceName, page, size); } @GetMapping("/apps/{appId}/envs/{env}/clusters/{clusterName}/missing-namespaces") public MultiResponseEntity findMissingNamespaces(@PathVariable String appId, @PathVariable String env, @PathVariable String clusterName) { MultiResponseEntity response = MultiResponseEntity.ok(); Set missingNamespaces = findMissingNamespaceNames(appId, env, clusterName); for (String missingNamespace : missingNamespaces) { response.addResponseEntity(RichResponseEntity.ok(missingNamespace)); } return response; } @PostMapping("/apps/{appId}/envs/{env}/clusters/{clusterName}/missing-namespaces") @ApolloAuditLog(type = OpType.CREATE, name = "Namespace.createMissingNamespaces") public ResponseEntity createMissingNamespaces(@PathVariable String appId, @PathVariable String env, @PathVariable String clusterName) { Set missingNamespaces = findMissingNamespaceNames(appId, env, clusterName); for (String missingNamespace : missingNamespaces) { namespaceAPI.createMissingAppNamespace(Env.valueOf(env), findAppNamespace(appId, missingNamespace)); } return ResponseEntity.ok().build(); } private Set findMissingNamespaceNames(String appId, String env, String clusterName) { List configDbAppNamespaces = namespaceAPI.getAppNamespaces(appId, Env.valueOf(env)); List configDbNamespaces = namespaceService.findNamespaces(appId, Env.valueOf(env), clusterName); List portalDbAppNamespaces = appNamespaceService.findByAppId(appId); Set configDbAppNamespaceNames = configDbAppNamespaces.stream().map(AppNamespaceDTO::getName).collect(Collectors.toSet()); Set configDbNamespaceNames = configDbNamespaces.stream().map(NamespaceDTO::getNamespaceName).collect(Collectors.toSet()); Set portalDbAllAppNamespaceNames = Sets.newHashSet(); Set portalDbPrivateAppNamespaceNames = Sets.newHashSet(); for (AppNamespace appNamespace : portalDbAppNamespaces) { portalDbAllAppNamespaceNames.add(appNamespace.getName()); if (!appNamespace.isPublic()) { portalDbPrivateAppNamespaceNames.add(appNamespace.getName()); } } // AppNamespaces should be the same Set missingAppNamespaceNames = Sets.difference(portalDbAllAppNamespaceNames, configDbAppNamespaceNames); // Private namespaces should all exist Set missingNamespaceNames = Sets.difference(portalDbPrivateAppNamespaceNames, configDbNamespaceNames); return Sets.union(missingAppNamespaceNames, missingNamespaceNames); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/NamespaceLockController.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.controller; import com.ctrip.framework.apollo.common.dto.NamespaceLockDTO; import com.ctrip.framework.apollo.portal.environment.Env; import com.ctrip.framework.apollo.portal.entity.vo.LockInfo; import com.ctrip.framework.apollo.portal.service.NamespaceLockService; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; @RestController public class NamespaceLockController { private final NamespaceLockService namespaceLockService; public NamespaceLockController(final NamespaceLockService namespaceLockService) { this.namespaceLockService = namespaceLockService; } @Deprecated @GetMapping( value = "/apps/{appId}/envs/{env}/clusters/{clusterName}/namespaces/{namespaceName}/lock") public NamespaceLockDTO getNamespaceLock(@PathVariable String appId, @PathVariable String env, @PathVariable String clusterName, @PathVariable String namespaceName) { return namespaceLockService.getNamespaceLock(appId, Env.valueOf(env), clusterName, namespaceName); } @GetMapping( value = "/apps/{appId}/envs/{env}/clusters/{clusterName}/namespaces/{namespaceName}/lock-info") public LockInfo getNamespaceLockInfo(@PathVariable String appId, @PathVariable String env, @PathVariable String clusterName, @PathVariable String namespaceName) { return namespaceLockService.getNamespaceLockInfo(appId, Env.valueOf(env), clusterName, namespaceName); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/OrganizationController.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.controller; import com.ctrip.framework.apollo.portal.component.config.PortalConfig; import com.ctrip.framework.apollo.portal.entity.vo.Organization; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.List; /** * @author Jason Song(song_s@ctrip.com) */ @RestController @RequestMapping("/organizations") public class OrganizationController { private final PortalConfig portalConfig; public OrganizationController(final PortalConfig portalConfig) { this.portalConfig = portalConfig; } @RequestMapping public List loadOrganization() { return portalConfig.organizations(); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/PageSettingController.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.controller; import com.ctrip.framework.apollo.portal.component.config.PortalConfig; import com.ctrip.framework.apollo.portal.entity.vo.PageSetting; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class PageSettingController { private final PortalConfig portalConfig; public PageSettingController(final PortalConfig portalConfig) { this.portalConfig = portalConfig; } @GetMapping("/page-settings") public PageSetting getPageSetting() { PageSetting setting = new PageSetting(); setting.setWikiAddress(portalConfig.wikiAddress()); setting.setCanAppAdminCreatePrivateNamespace(portalConfig.canAppAdminCreatePrivateNamespace()); return setting; } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/PermissionController.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.controller; import com.ctrip.framework.apollo.audit.annotation.ApolloAuditLog; import com.ctrip.framework.apollo.audit.annotation.OpType; import com.ctrip.framework.apollo.common.exception.BadRequestException; import com.ctrip.framework.apollo.common.utils.RequestPrecondition; import com.ctrip.framework.apollo.portal.component.UnifiedPermissionValidator; import com.ctrip.framework.apollo.portal.constant.PermissionType; import com.ctrip.framework.apollo.portal.constant.RoleType; import com.ctrip.framework.apollo.portal.entity.bo.UserInfo; import com.ctrip.framework.apollo.portal.entity.vo.AppRolesAssignedUsers; import com.ctrip.framework.apollo.portal.entity.vo.ClusterNamespaceRolesAssignedUsers; import com.ctrip.framework.apollo.portal.entity.vo.NamespaceEnvRolesAssignedUsers; import com.ctrip.framework.apollo.portal.entity.vo.NamespaceRolesAssignedUsers; import com.ctrip.framework.apollo.portal.entity.vo.PermissionCondition; import com.ctrip.framework.apollo.portal.environment.Env; import com.ctrip.framework.apollo.portal.service.RoleInitializationService; import com.ctrip.framework.apollo.portal.service.RolePermissionService; import com.ctrip.framework.apollo.portal.service.SystemRoleManagerService; import com.ctrip.framework.apollo.portal.spi.UserInfoHolder; import com.ctrip.framework.apollo.portal.spi.UserService; import com.ctrip.framework.apollo.portal.util.RoleUtils; import com.google.common.collect.Sets; import com.google.gson.JsonObject; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.util.CollectionUtils; import org.springframework.web.bind.annotation.*; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.stream.Collectors; @RestController public class PermissionController { private final UserInfoHolder userInfoHolder; private final RolePermissionService rolePermissionService; private final UserService userService; private final RoleInitializationService roleInitializationService; private final SystemRoleManagerService systemRoleManagerService; private final UnifiedPermissionValidator unifiedPermissionValidator; public PermissionController(final UserInfoHolder userInfoHolder, final RolePermissionService rolePermissionService, final UserService userService, final RoleInitializationService roleInitializationService, final SystemRoleManagerService systemRoleManagerService, final UnifiedPermissionValidator unifiedPermissionValidator) { this.userInfoHolder = userInfoHolder; this.rolePermissionService = rolePermissionService; this.userService = userService; this.roleInitializationService = roleInitializationService; this.systemRoleManagerService = systemRoleManagerService; this.unifiedPermissionValidator = unifiedPermissionValidator; } @PostMapping("/apps/{appId}/initPermission") public ResponseEntity initAppPermission(@PathVariable String appId, @RequestBody String namespaceName) { roleInitializationService.initNamespaceEnvRoles(appId, namespaceName, userInfoHolder.getUser().getUserId()); return ResponseEntity.ok().build(); } @PostMapping("/apps/{appId}/envs/{env}/clusters/{clusterName}/initNsPermission") public ResponseEntity initClusterNamespacePermission(@PathVariable String appId, @PathVariable String env, @PathVariable String clusterName) { String normalizedEnv = normalizeEnv(env); roleInitializationService.initClusterNamespaceRoles(appId, normalizedEnv, clusterName, userInfoHolder.getUser().getUserId()); return ResponseEntity.ok().build(); } @GetMapping("/apps/{appId}/permissions/{permissionType}") public ResponseEntity hasPermission(@PathVariable String appId, @PathVariable String permissionType) { PermissionCondition permissionCondition = new PermissionCondition(); permissionCondition.setHasPermission(rolePermissionService .userHasPermission(userInfoHolder.getUser().getUserId(), permissionType, appId)); return ResponseEntity.ok().body(permissionCondition); } @GetMapping("/apps/{appId}/namespaces/{namespaceName}/permissions/{permissionType}") public ResponseEntity hasPermission(@PathVariable String appId, @PathVariable String namespaceName, @PathVariable String permissionType) { PermissionCondition permissionCondition = new PermissionCondition(); permissionCondition.setHasPermission( rolePermissionService.userHasPermission(userInfoHolder.getUser().getUserId(), permissionType, RoleUtils.buildNamespaceTargetId(appId, namespaceName))); return ResponseEntity.ok().body(permissionCondition); } @GetMapping("/apps/{appId}/envs/{env}/namespaces/{namespaceName}/permissions/{permissionType}") public ResponseEntity hasPermission(@PathVariable String appId, @PathVariable String env, @PathVariable String namespaceName, @PathVariable String permissionType) { String normalizedEnv = normalizeEnv(env); PermissionCondition permissionCondition = new PermissionCondition(); permissionCondition.setHasPermission( rolePermissionService.userHasPermission(userInfoHolder.getUser().getUserId(), permissionType, RoleUtils.buildNamespaceTargetId(appId, namespaceName, normalizedEnv))); return ResponseEntity.ok().body(permissionCondition); } @GetMapping("/apps/{appId}/envs/{env}/clusters/{clusterName}/ns_permissions/{permissionType}") public ResponseEntity hasClusterNamespacePermission( @PathVariable String appId, @PathVariable String env, @PathVariable String clusterName, @PathVariable String permissionType) { String normalizedEnv = normalizeEnv(env); PermissionCondition permissionCondition = new PermissionCondition(); permissionCondition.setHasPermission( rolePermissionService.userHasPermission(userInfoHolder.getUser().getUserId(), permissionType, RoleUtils.buildClusterTargetId(appId, normalizedEnv, clusterName))); return ResponseEntity.ok().body(permissionCondition); } @GetMapping("/permissions/root") public ResponseEntity hasRootPermission() { PermissionCondition permissionCondition = new PermissionCondition(); permissionCondition .setHasPermission(rolePermissionService.isSuperAdmin(userInfoHolder.getUser().getUserId())); return ResponseEntity.ok().body(permissionCondition); } @GetMapping("/apps/{appId}/envs/{env}/namespaces/{namespaceName}/role_users") public NamespaceEnvRolesAssignedUsers getNamespaceEnvRoles(@PathVariable String appId, @PathVariable String env, @PathVariable String namespaceName) { String normalizedEnv = normalizeEnv(env); NamespaceEnvRolesAssignedUsers assignedUsers = new NamespaceEnvRolesAssignedUsers(); assignedUsers.setNamespaceName(namespaceName); assignedUsers.setAppId(appId); assignedUsers.setEnv(Env.valueOf(normalizedEnv)); Set releaseNamespaceUsers = rolePermissionService.queryUsersWithRole( RoleUtils.buildReleaseNamespaceRoleName(appId, namespaceName, normalizedEnv)); assignedUsers.setReleaseRoleUsers(releaseNamespaceUsers); Set modifyNamespaceUsers = rolePermissionService.queryUsersWithRole( RoleUtils.buildModifyNamespaceRoleName(appId, namespaceName, normalizedEnv)); assignedUsers.setModifyRoleUsers(modifyNamespaceUsers); return assignedUsers; } @PreAuthorize(value = "@unifiedPermissionValidator.hasAssignRolePermission(#appId)") @PostMapping("/apps/{appId}/envs/{env}/namespaces/{namespaceName}/roles/{roleType}") @ApolloAuditLog(type = OpType.CREATE, name = "Auth.assignNamespaceEnvRoleToUser") public ResponseEntity assignNamespaceEnvRoleToUser(@PathVariable String appId, @PathVariable String env, @PathVariable String namespaceName, @PathVariable String roleType, @RequestBody String user) { checkUserExists(user); RequestPrecondition.checkArgumentsNotEmpty(user); if (!RoleType.isValidRoleType(roleType)) { throw BadRequestException.invalidRoleTypeFormat(roleType); } String normalizedEnv = normalizeEnv(env); Set assignedUser = rolePermissionService.assignRoleToUsers( RoleUtils.buildNamespaceRoleName(appId, namespaceName, roleType, normalizedEnv), Sets.newHashSet(user), userInfoHolder.getUser().getUserId()); if (CollectionUtils.isEmpty(assignedUser)) { throw BadRequestException.userAlreadyAuthorized(user); } return ResponseEntity.ok().build(); } @PreAuthorize(value = "@unifiedPermissionValidator.hasAssignRolePermission(#appId)") @DeleteMapping("/apps/{appId}/envs/{env}/namespaces/{namespaceName}/roles/{roleType}") @ApolloAuditLog(type = OpType.DELETE, name = "Auth.removeNamespaceEnvRoleFromUser") public ResponseEntity removeNamespaceEnvRoleFromUser(@PathVariable String appId, @PathVariable String env, @PathVariable String namespaceName, @PathVariable String roleType, @RequestParam String user) { RequestPrecondition.checkArgumentsNotEmpty(user); if (!RoleType.isValidRoleType(roleType)) { throw BadRequestException.invalidRoleTypeFormat(roleType); } String normalizedEnv = normalizeEnv(env); rolePermissionService.removeRoleFromUsers( RoleUtils.buildNamespaceRoleName(appId, namespaceName, roleType, normalizedEnv), Sets.newHashSet(user), userInfoHolder.getUser().getUserId()); return ResponseEntity.ok().build(); } @GetMapping("/apps/{appId}/envs/{env}/clusters/{clusterName}/ns_role_users") public ClusterNamespaceRolesAssignedUsers getClusterNamespaceRoles(@PathVariable String appId, @PathVariable String env, @PathVariable String clusterName) { String normalizedEnv = normalizeEnv(env); ClusterNamespaceRolesAssignedUsers assignedUsers = new ClusterNamespaceRolesAssignedUsers(); assignedUsers.setAppId(appId); assignedUsers.setEnv(normalizedEnv); assignedUsers.setCluster(clusterName); Set releaseNamespacesInClusterUsers = rolePermissionService.queryUsersWithRole( RoleUtils.buildReleaseNamespacesInClusterRoleName(appId, normalizedEnv, clusterName)); assignedUsers.setReleaseRoleUsers(releaseNamespacesInClusterUsers); Set modifyNamespacesInClusterUsers = rolePermissionService.queryUsersWithRole( RoleUtils.buildModifyNamespacesInClusterRoleName(appId, normalizedEnv, clusterName)); assignedUsers.setModifyRoleUsers(modifyNamespacesInClusterUsers); return assignedUsers; } @PreAuthorize(value = "@unifiedPermissionValidator.hasAssignRolePermission(#appId)") @PostMapping("/apps/{appId}/envs/{env}/clusters/{clusterName}/ns_roles/{roleType}") public ResponseEntity assignClusterNamespaceRoleToUser(@PathVariable String appId, @PathVariable String env, @PathVariable String clusterName, @PathVariable String roleType, @RequestBody String user) { checkUserExists(user); RequestPrecondition.checkArgumentsNotEmpty(user); if (!RoleType.isValidRoleType(roleType)) { throw BadRequestException.invalidRoleTypeFormat(roleType); } String normalizedEnv = normalizeEnv(env); Set assignedUser = rolePermissionService.assignRoleToUsers( RoleUtils.buildClusterRoleName(appId, normalizedEnv, clusterName, roleType), Sets.newHashSet(user), userInfoHolder.getUser().getUserId()); if (CollectionUtils.isEmpty(assignedUser)) { throw BadRequestException.userAlreadyAuthorized(user); } return ResponseEntity.ok().build(); } @PreAuthorize(value = "@unifiedPermissionValidator.hasAssignRolePermission(#appId)") @DeleteMapping("/apps/{appId}/envs/{env}/clusters/{clusterName}/ns_roles/{roleType}") public ResponseEntity removeClusterNamespaceRoleFromUser(@PathVariable String appId, @PathVariable String env, @PathVariable String clusterName, @PathVariable String roleType, @RequestParam String user) { RequestPrecondition.checkArgumentsNotEmpty(user); if (!RoleType.isValidRoleType(roleType)) { throw BadRequestException.invalidRoleTypeFormat(roleType); } String normalizedEnv = normalizeEnv(env); rolePermissionService.removeRoleFromUsers( RoleUtils.buildClusterRoleName(appId, normalizedEnv, clusterName, roleType), Sets.newHashSet(user), userInfoHolder.getUser().getUserId()); return ResponseEntity.ok().build(); } @GetMapping("/apps/{appId}/namespaces/{namespaceName}/role_users") public NamespaceRolesAssignedUsers getNamespaceRoles(@PathVariable String appId, @PathVariable String namespaceName) { NamespaceRolesAssignedUsers assignedUsers = new NamespaceRolesAssignedUsers(); assignedUsers.setNamespaceName(namespaceName); assignedUsers.setAppId(appId); Set releaseNamespaceUsers = rolePermissionService .queryUsersWithRole(RoleUtils.buildReleaseNamespaceRoleName(appId, namespaceName)); assignedUsers.setReleaseRoleUsers(releaseNamespaceUsers); Set modifyNamespaceUsers = rolePermissionService .queryUsersWithRole(RoleUtils.buildModifyNamespaceRoleName(appId, namespaceName)); assignedUsers.setModifyRoleUsers(modifyNamespaceUsers); return assignedUsers; } @PreAuthorize(value = "@unifiedPermissionValidator.hasAssignRolePermission(#appId)") @PostMapping("/apps/{appId}/namespaces/{namespaceName}/roles/{roleType}") @ApolloAuditLog(type = OpType.CREATE, name = "Auth.assignNamespaceRoleToUser") public ResponseEntity assignNamespaceRoleToUser(@PathVariable String appId, @PathVariable String namespaceName, @PathVariable String roleType, @RequestBody String user) { checkUserExists(user); RequestPrecondition.checkArgumentsNotEmpty(user); if (!RoleType.isValidRoleType(roleType)) { throw BadRequestException.invalidRoleTypeFormat(roleType); } Set assignedUser = rolePermissionService.assignRoleToUsers( RoleUtils.buildNamespaceRoleName(appId, namespaceName, roleType), Sets.newHashSet(user), userInfoHolder.getUser().getUserId()); if (CollectionUtils.isEmpty(assignedUser)) { throw BadRequestException.userAlreadyAuthorized(user); } return ResponseEntity.ok().build(); } @PreAuthorize(value = "@unifiedPermissionValidator.hasAssignRolePermission(#appId)") @DeleteMapping("/apps/{appId}/namespaces/{namespaceName}/roles/{roleType}") @ApolloAuditLog(type = OpType.DELETE, name = "Auth.removeNamespaceRoleFromUser") public ResponseEntity removeNamespaceRoleFromUser(@PathVariable String appId, @PathVariable String namespaceName, @PathVariable String roleType, @RequestParam String user) { RequestPrecondition.checkArgumentsNotEmpty(user); if (!RoleType.isValidRoleType(roleType)) { throw BadRequestException.invalidRoleTypeFormat(roleType); } rolePermissionService.removeRoleFromUsers( RoleUtils.buildNamespaceRoleName(appId, namespaceName, roleType), Sets.newHashSet(user), userInfoHolder.getUser().getUserId()); return ResponseEntity.ok().build(); } @GetMapping("/apps/{appId}/role_users") public AppRolesAssignedUsers getAppRoles(@PathVariable String appId) { AppRolesAssignedUsers users = new AppRolesAssignedUsers(); users.setAppId(appId); Set masterUsers = rolePermissionService.queryUsersWithRole(RoleUtils.buildAppMasterRoleName(appId)); users.setMasterUsers(masterUsers); return users; } @PreAuthorize(value = "@unifiedPermissionValidator.hasManageAppMasterPermission(#appId)") @PostMapping("/apps/{appId}/roles/{roleType}") @ApolloAuditLog(type = OpType.CREATE, name = "Auth.assignAppRoleToUser") public ResponseEntity assignAppRoleToUser(@PathVariable String appId, @PathVariable String roleType, @RequestBody String user) { checkUserExists(user); RequestPrecondition.checkArgumentsNotEmpty(user); if (!RoleType.isValidRoleType(roleType)) { throw BadRequestException.invalidRoleTypeFormat(roleType); } Set assignedUsers = rolePermissionService.assignRoleToUsers(RoleUtils.buildAppRoleName(appId, roleType), Sets.newHashSet(user), userInfoHolder.getUser().getUserId()); if (CollectionUtils.isEmpty(assignedUsers)) { throw BadRequestException.userAlreadyAuthorized(user); } return ResponseEntity.ok().build(); } @PreAuthorize(value = "@unifiedPermissionValidator.hasManageAppMasterPermission(#appId)") @DeleteMapping("/apps/{appId}/roles/{roleType}") @ApolloAuditLog(type = OpType.DELETE, name = "Auth.removeAppRoleFromUser") public ResponseEntity removeAppRoleFromUser(@PathVariable String appId, @PathVariable String roleType, @RequestParam String user) { RequestPrecondition.checkArgumentsNotEmpty(user); if (!RoleType.isValidRoleType(roleType)) { throw BadRequestException.invalidRoleTypeFormat(roleType); } rolePermissionService.removeRoleFromUsers(RoleUtils.buildAppRoleName(appId, roleType), Sets.newHashSet(user), userInfoHolder.getUser().getUserId()); return ResponseEntity.ok().build(); } /** * Normalize the env name to ensure consistency between UI display and permission control. * For example, "prod" -> "PROD" -> "PRO" via {@link Env#transformEnv(String)}. * * @see #5442 */ private String normalizeEnv(String env) { Env transformedEnv = Env.transformEnv(env); if (Env.UNKNOWN == transformedEnv) { throw BadRequestException.invalidEnvFormat(env); } return transformedEnv.getName(); } private void checkUserExists(String userId) { if (userService.findByUserId(userId) == null) { throw BadRequestException.userNotExists(userId); } } @PreAuthorize(value = "@unifiedPermissionValidator.isSuperAdmin()") @PostMapping("/system/role/createApplication") @ApolloAuditLog(type = OpType.CREATE, name = "Auth.addCreateApplicationRoleToUser") public ResponseEntity addCreateApplicationRoleToUser(@RequestBody List userIds) { userIds.forEach(this::checkUserExists); rolePermissionService.assignRoleToUsers(SystemRoleManagerService.CREATE_APPLICATION_ROLE_NAME, new HashSet<>(userIds), userInfoHolder.getUser().getUserId()); return ResponseEntity.ok().build(); } @PreAuthorize(value = "@unifiedPermissionValidator.isSuperAdmin()") @DeleteMapping("/system/role/createApplication/{userId}") @ApolloAuditLog(type = OpType.DELETE, name = "Auth.deleteCreateApplicationRoleFromUser") public ResponseEntity deleteCreateApplicationRoleFromUser( @PathVariable("userId") String userId) { checkUserExists(userId); Set userIds = new HashSet<>(); userIds.add(userId); rolePermissionService.removeRoleFromUsers(SystemRoleManagerService.CREATE_APPLICATION_ROLE_NAME, userIds, userInfoHolder.getUser().getUserId()); return ResponseEntity.ok().build(); } @PreAuthorize(value = "@unifiedPermissionValidator.isSuperAdmin()") @GetMapping("/system/role/createApplication") public List getCreateApplicationRoleUsers() { return rolePermissionService .queryUsersWithRole(SystemRoleManagerService.CREATE_APPLICATION_ROLE_NAME).stream() .map(UserInfo::getUserId).collect(Collectors.toList()); } @GetMapping("/system/role/createApplication/{userId}") public JsonObject hasCreateApplicationPermission(@PathVariable String userId) { JsonObject rs = new JsonObject(); rs.addProperty("hasCreateApplicationPermission", unifiedPermissionValidator.hasCreateApplicationPermission(userId)); return rs; } @PreAuthorize(value = "@unifiedPermissionValidator.isSuperAdmin()") @PostMapping("/apps/{appId}/system/master/{userId}") @ApolloAuditLog(type = OpType.CREATE, name = "Auth.addManageAppMasterRoleToUser") public ResponseEntity addManageAppMasterRoleToUser(@PathVariable String appId, @PathVariable String userId) { checkUserExists(userId); roleInitializationService.initManageAppMasterRole(appId, userInfoHolder.getUser().getUserId()); Set userIds = new HashSet<>(); userIds.add(userId); rolePermissionService.assignRoleToUsers( RoleUtils.buildAppRoleName(appId, PermissionType.MANAGE_APP_MASTER), userIds, userInfoHolder.getUser().getUserId()); return ResponseEntity.ok().build(); } @PreAuthorize(value = "@unifiedPermissionValidator.isSuperAdmin()") @DeleteMapping("/apps/{appId}/system/master/{userId}") @ApolloAuditLog(type = OpType.DELETE, name = "Auth.forbidManageAppMaster") public ResponseEntity forbidManageAppMaster(@PathVariable String appId, @PathVariable String userId) { checkUserExists(userId); roleInitializationService.initManageAppMasterRole(appId, userInfoHolder.getUser().getUserId()); Set userIds = new HashSet<>(); userIds.add(userId); rolePermissionService.removeRoleFromUsers( RoleUtils.buildAppRoleName(appId, PermissionType.MANAGE_APP_MASTER), userIds, userInfoHolder.getUser().getUserId()); return ResponseEntity.ok().build(); } @GetMapping("/system/role/manageAppMaster") public JsonObject isManageAppMasterPermissionEnabled() { JsonObject rs = new JsonObject(); rs.addProperty("isManageAppMasterPermissionEnabled", systemRoleManagerService.isManageAppMasterPermissionEnabled()); return rs; } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/PrefixPathController.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.controller; import com.google.common.base.Strings; import jakarta.servlet.ServletContext; import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class PrefixPathController { private final ServletContext servletContext; // We suggest users use server.servlet.context-path to configure the prefix path instead @Deprecated @Value("${prefix.path:}") private String prefixPath; public PrefixPathController(ServletContext servletContext) { this.servletContext = servletContext; } @GetMapping("/prefix-path") public String getPrefixPath() { if (Strings.isNullOrEmpty(prefixPath)) { return servletContext.getContextPath(); } return prefixPath; } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/ReleaseController.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.controller; import com.ctrip.framework.apollo.common.dto.ReleaseDTO; import com.ctrip.framework.apollo.common.exception.BadRequestException; import com.ctrip.framework.apollo.common.exception.NotFoundException; import com.ctrip.framework.apollo.portal.component.UnifiedPermissionValidator; import com.ctrip.framework.apollo.portal.environment.Env; import com.ctrip.framework.apollo.portal.component.config.PortalConfig; import com.ctrip.framework.apollo.portal.entity.bo.ReleaseBO; import com.ctrip.framework.apollo.portal.entity.model.NamespaceReleaseModel; import com.ctrip.framework.apollo.portal.entity.vo.ReleaseCompareResult; import com.ctrip.framework.apollo.portal.listener.ConfigPublishEvent; import com.ctrip.framework.apollo.portal.service.ReleaseService; import com.ctrip.framework.apollo.portal.spi.UserInfoHolder; import jakarta.validation.Valid; import jakarta.validation.constraints.Positive; import jakarta.validation.constraints.PositiveOrZero; import org.springframework.context.ApplicationEventPublisher; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.util.Collections; import java.util.List; @Validated @RestController public class ReleaseController { private final ReleaseService releaseService; private final ApplicationEventPublisher publisher; private final PortalConfig portalConfig; private final UnifiedPermissionValidator unifiedPermissionValidator; private final UserInfoHolder userInfoHolder; public ReleaseController(final ReleaseService releaseService, final ApplicationEventPublisher publisher, final PortalConfig portalConfig, final UnifiedPermissionValidator unifiedPermissionValidator, final UserInfoHolder userInfoHolder) { this.releaseService = releaseService; this.publisher = publisher; this.portalConfig = portalConfig; this.unifiedPermissionValidator = unifiedPermissionValidator; this.userInfoHolder = userInfoHolder; } @PreAuthorize( value = "@unifiedPermissionValidator.hasReleaseNamespacePermission(#appId, #env, #clusterName, #namespaceName)") @PostMapping( value = "/apps/{appId}/envs/{env}/clusters/{clusterName}/namespaces/{namespaceName}/releases") public ReleaseDTO createRelease(@PathVariable String appId, @PathVariable String env, @PathVariable String clusterName, @PathVariable String namespaceName, @RequestBody NamespaceReleaseModel model) { model.setAppId(appId); model.setEnv(env); model.setClusterName(clusterName); model.setNamespaceName(namespaceName); if (model.isEmergencyPublish() && !portalConfig.isEmergencyPublishAllowed(Env.valueOf(env))) { throw new BadRequestException("Env: %s is not supported emergency publish now", env); } ReleaseDTO createdRelease = releaseService.publish(model); ConfigPublishEvent event = ConfigPublishEvent.instance(); event.withAppId(appId).withCluster(clusterName).withNamespace(namespaceName) .withReleaseId(createdRelease.getId()).setNormalPublishEvent(true).setEnv(Env.valueOf(env)); publisher.publishEvent(event); return createdRelease; } @PreAuthorize( value = "@unifiedPermissionValidator.hasReleaseNamespacePermission(#appId, #env, #clusterName, #namespaceName)") @PostMapping( value = "/apps/{appId}/envs/{env}/clusters/{clusterName}/namespaces/{namespaceName}/branches/{branchName}/releases") public ReleaseDTO createGrayRelease(@PathVariable String appId, @PathVariable String env, @PathVariable String clusterName, @PathVariable String namespaceName, @PathVariable String branchName, @RequestBody NamespaceReleaseModel model) { model.setAppId(appId); model.setEnv(env); model.setClusterName(branchName); model.setNamespaceName(namespaceName); if (model.isEmergencyPublish() && !portalConfig.isEmergencyPublishAllowed(Env.valueOf(env))) { throw new BadRequestException("Env: %s is not supported emergency publish now", env); } ReleaseDTO createdRelease = releaseService.publish(model); ConfigPublishEvent event = ConfigPublishEvent.instance(); event.withAppId(appId).withCluster(clusterName).withNamespace(namespaceName) .withReleaseId(createdRelease.getId()).setGrayPublishEvent(true).setEnv(Env.valueOf(env)); publisher.publishEvent(event); return createdRelease; } @GetMapping("/envs/{env}/releases/{releaseId}") public ReleaseDTO get(@PathVariable String env, @PathVariable long releaseId) { ReleaseDTO release = releaseService.findReleaseById(Env.valueOf(env), releaseId); if (release == null) { throw NotFoundException.releaseNotFound(releaseId); } if (unifiedPermissionValidator.shouldHideConfigToCurrentUser(release.getAppId(), env, release.getClusterName(), release.getNamespaceName())) { throw new AccessDeniedException("Access is denied"); } return release; } @GetMapping( value = "/apps/{appId}/envs/{env}/clusters/{clusterName}/namespaces/{namespaceName}/releases/all") public List findAllReleases(@PathVariable String appId, @PathVariable String env, @PathVariable String clusterName, @PathVariable String namespaceName, @Valid @PositiveOrZero(message = "page should be positive or 0") @RequestParam(defaultValue = "0") int page, @Valid @Positive(message = "size should be positive number") @RequestParam(defaultValue = "5") int size) { if (unifiedPermissionValidator.shouldHideConfigToCurrentUser(appId, env, clusterName, namespaceName)) { return Collections.emptyList(); } return releaseService.findAllReleases(appId, Env.valueOf(env), clusterName, namespaceName, page, size); } @GetMapping( value = "/apps/{appId}/envs/{env}/clusters/{clusterName}/namespaces/{namespaceName}/releases/active") public List findActiveReleases(@PathVariable String appId, @PathVariable String env, @PathVariable String clusterName, @PathVariable String namespaceName, @Valid @PositiveOrZero(message = "page should be positive or 0") @RequestParam(defaultValue = "0") int page, @Valid @Positive(message = "size should be positive number") @RequestParam(defaultValue = "5") int size) { if (unifiedPermissionValidator.shouldHideConfigToCurrentUser(appId, env, clusterName, namespaceName)) { return Collections.emptyList(); } return releaseService.findActiveReleases(appId, Env.valueOf(env), clusterName, namespaceName, page, size); } @GetMapping(value = "/envs/{env}/releases/compare") public ReleaseCompareResult compareRelease(@PathVariable String env, @RequestParam long baseReleaseId, @RequestParam long toCompareReleaseId) { return releaseService.compare(Env.valueOf(env), baseReleaseId, toCompareReleaseId); } @PutMapping(path = "/envs/{env}/releases/{releaseId}/rollback") public void rollback(@PathVariable String env, @PathVariable long releaseId, @RequestParam(defaultValue = "-1") long toReleaseId) { ReleaseDTO release = releaseService.findReleaseById(Env.valueOf(env), releaseId); if (release == null) { throw NotFoundException.releaseNotFound(releaseId); } if (!unifiedPermissionValidator.hasReleaseNamespacePermission(release.getAppId(), env, release.getClusterName(), release.getNamespaceName())) { throw new AccessDeniedException("Access is denied"); } if (toReleaseId > -1) { releaseService.rollbackTo(Env.valueOf(env), releaseId, toReleaseId, userInfoHolder.getUser().getUserId()); } else { releaseService.rollback(Env.valueOf(env), releaseId, userInfoHolder.getUser().getUserId()); } ConfigPublishEvent event = ConfigPublishEvent.instance(); event.withAppId(release.getAppId()).withCluster(release.getClusterName()) .withNamespace(release.getNamespaceName()).withPreviousReleaseId(releaseId) .setRollbackEvent(true).setEnv(Env.valueOf(env)); publisher.publishEvent(event); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/ReleaseHistoryController.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.controller; import com.ctrip.framework.apollo.portal.component.UnifiedPermissionValidator; import com.ctrip.framework.apollo.portal.environment.Env; import com.ctrip.framework.apollo.portal.entity.bo.ReleaseHistoryBO; import com.ctrip.framework.apollo.portal.service.ReleaseHistoryService; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.util.Collections; import java.util.List; @RestController public class ReleaseHistoryController { private final ReleaseHistoryService releaseHistoryService; private final UnifiedPermissionValidator unifiedPermissionValidator; public ReleaseHistoryController(final ReleaseHistoryService releaseHistoryService, final UnifiedPermissionValidator unifiedPermissionValidator) { this.releaseHistoryService = releaseHistoryService; this.unifiedPermissionValidator = unifiedPermissionValidator; } @GetMapping("/apps/{appId}/envs/{env}/clusters/{clusterName}/namespaces/{namespaceName}/releases/histories") public List findReleaseHistoriesByNamespace(@PathVariable String appId, @PathVariable String env, @PathVariable String clusterName, @PathVariable String namespaceName, @RequestParam(value = "page", defaultValue = "0") int page, @RequestParam(value = "size", defaultValue = "10") int size) { if (unifiedPermissionValidator.shouldHideConfigToCurrentUser(appId, env, clusterName, namespaceName)) { return Collections.emptyList(); } return releaseHistoryService.findNamespaceReleaseHistory(appId, Env.valueOf(env), clusterName, namespaceName, page, size); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/SearchController.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.controller; import com.google.common.collect.Lists; import com.ctrip.framework.apollo.common.dto.NamespaceDTO; import com.ctrip.framework.apollo.common.dto.PageDTO; import com.ctrip.framework.apollo.common.entity.App; import com.ctrip.framework.apollo.portal.component.PortalSettings; import com.ctrip.framework.apollo.portal.component.config.PortalConfig; import com.ctrip.framework.apollo.portal.environment.Env; import com.ctrip.framework.apollo.portal.service.AppService; import com.ctrip.framework.apollo.portal.service.NamespaceService; import org.springframework.data.domain.Pageable; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.util.List; import java.util.concurrent.atomic.AtomicLong; /** * @author lepdou 2021-09-13 */ @RestController("/app") public class SearchController { private AppService appService; private PortalSettings portalSettings; private NamespaceService namespaceService; private PortalConfig portalConfig; public SearchController(final AppService appService, final PortalSettings portalSettings, final PortalConfig portalConfig, final NamespaceService namespaceService) { this.appService = appService; this.portalConfig = portalConfig; this.portalSettings = portalSettings; this.namespaceService = namespaceService; } @GetMapping("/apps/search/by-appid-or-name") public PageDTO search(@RequestParam(value = "query", required = false) String query, Pageable pageable) { if (StringUtils.isEmpty(query)) { return appService.findAll(pageable); } // search app PageDTO appPageDTO = appService.searchByAppIdOrAppName(query, pageable); if (appPageDTO.hasContent()) { return appPageDTO; } if (!portalConfig.supportSearchByItem()) { return new PageDTO<>(Lists.newLinkedList(), pageable, 0); } // search item return searchByItem(query, pageable); } private PageDTO searchByItem(String itemKey, Pageable pageable) { List result = Lists.newLinkedList(); if (StringUtils.isEmpty(itemKey)) { return new PageDTO<>(result, pageable, 0); } // use the env witch has the most namespace as page index. final AtomicLong maxTotal = new AtomicLong(0); List activeEnvs = portalSettings.getActiveEnvs(); activeEnvs.forEach(env -> { PageDTO namespacePage = namespaceService.findNamespacesByItem(env, itemKey, pageable); if (!namespacePage.hasContent()) { return; } long currentEnvNSTotal = namespacePage.getTotal(); if (currentEnvNSTotal > maxTotal.get()) { maxTotal.set(namespacePage.getTotal()); } List namespaceDTOS = namespacePage.getContent(); namespaceDTOS.forEach(namespaceDTO -> { String cluster = namespaceDTO.getClusterName(); String namespaceName = namespaceDTO.getNamespaceName(); App app = new App(); app.setAppId(namespaceDTO.getAppId()); app.setName(env.getName() + " / " + cluster + " / " + namespaceName); app.setOrgId(env.getName() + "+" + cluster + "+" + namespaceName); app.setOrgName("SearchByItem" + "+" + itemKey); result.add(app); }); }); return new PageDTO<>(result, pageable, maxTotal.get()); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/ServerConfigController.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.controller; import com.ctrip.framework.apollo.audit.annotation.ApolloAuditLog; import com.ctrip.framework.apollo.audit.annotation.OpType; import com.ctrip.framework.apollo.portal.entity.po.ServerConfig; import com.ctrip.framework.apollo.portal.environment.Env; import com.ctrip.framework.apollo.portal.service.ServerConfigService; import java.util.List; import jakarta.validation.Valid; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; /** * 配置中心本身需要一些配置,这些配置放在数据库里面 */ @RestController public class ServerConfigController { private final ServerConfigService serverConfigService; public ServerConfigController(final ServerConfigService serverConfigService) { this.serverConfigService = serverConfigService; } @PreAuthorize(value = "@unifiedPermissionValidator.isSuperAdmin()") @PostMapping("/server/portal-db/config") @ApolloAuditLog(type = OpType.CREATE, name = "ServerConfig.createOrUpdatePortalDBConfig") public ServerConfig createOrUpdatePortalDBConfig(@Valid @RequestBody ServerConfig serverConfig) { return serverConfigService.createOrUpdatePortalDBConfig(serverConfig); } @PreAuthorize(value = "@unifiedPermissionValidator.isSuperAdmin()") @PostMapping("/server/envs/{env}/config-db/config") @ApolloAuditLog(type = OpType.CREATE, name = "ServerConfig.createOrUpdateConfigDBConfig") public ServerConfig createOrUpdateConfigDBConfig(@Valid @RequestBody ServerConfig serverConfig, @PathVariable String env) { return serverConfigService.createOrUpdateConfigDBConfig(Env.transformEnv(env), serverConfig); } @PreAuthorize(value = "@unifiedPermissionValidator.isSuperAdmin()") @GetMapping("/server/portal-db/config/find-all-config") public List findAllPortalDBServerConfig() { return serverConfigService.findAllPortalDBConfig(); } @PreAuthorize(value = "@unifiedPermissionValidator.isSuperAdmin()") @GetMapping("/server/envs/{env}/config-db/config/find-all-config") public List findAllConfigDBServerConfig(@PathVariable String env) { return serverConfigService.findAllConfigDBConfig(Env.transformEnv(env)); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/SignInController.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.controller; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; /** * @author lepdou 2017-08-30 */ @Controller public class SignInController { @GetMapping("/signin") public String login(@RequestParam(value = "error", required = false) String error, @RequestParam(value = "logout", required = false) String logout) { return "login.html"; } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/SsoHeartbeatController.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.controller; import com.ctrip.framework.apollo.portal.spi.SsoHeartbeatHandler; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; /** * Since sso auth information has a limited expiry time, so we need to do sso heartbeat to keep the * information refreshed when unavailable * * @author Jason Song(song_s@ctrip.com) */ @Controller @RequestMapping("/sso_heartbeat") public class SsoHeartbeatController { private final SsoHeartbeatHandler handler; public SsoHeartbeatController(final SsoHeartbeatHandler handler) { this.handler = handler; } @GetMapping public void heartbeat(HttpServletRequest request, HttpServletResponse response) { handler.doHeartbeat(request, response); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/SystemInfoController.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.controller; import com.ctrip.framework.apollo.common.constants.ApolloServer; import com.ctrip.framework.apollo.core.dto.ServiceDTO; import com.ctrip.framework.apollo.portal.component.PortalSettings; import com.ctrip.framework.apollo.portal.component.RestTemplateFactory; import com.ctrip.framework.apollo.portal.entity.vo.EnvironmentInfo; import com.ctrip.framework.apollo.portal.entity.vo.SystemInfo; import com.ctrip.framework.apollo.portal.environment.Env; import com.ctrip.framework.apollo.portal.environment.PortalMetaDomainService; import java.util.List; import java.util.Objects; import jakarta.annotation.PostConstruct; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.actuate.health.Health; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestTemplate; @RestController @RequestMapping("/system-info") public class SystemInfoController { private static final Logger logger = LoggerFactory.getLogger(SystemInfoController.class); private static final String CONFIG_SERVICE_URL_PATH = "/services/config"; private static final String ADMIN_SERVICE_URL_PATH = "/services/admin"; private RestTemplate restTemplate; private final PortalSettings portalSettings; private final RestTemplateFactory restTemplateFactory; private final PortalMetaDomainService portalMetaDomainService; public SystemInfoController(final PortalSettings portalSettings, final RestTemplateFactory restTemplateFactory, final PortalMetaDomainService portalMetaDomainService) { this.portalSettings = portalSettings; this.restTemplateFactory = restTemplateFactory; this.portalMetaDomainService = portalMetaDomainService; } @PostConstruct private void init() { restTemplate = restTemplateFactory.getObject(); } @PreAuthorize(value = "@unifiedPermissionValidator.isSuperAdmin()") @GetMapping public SystemInfo getSystemInfo() { SystemInfo systemInfo = new SystemInfo(); String version = ApolloServer.VERSION; if (isValidVersion(version)) { systemInfo.setVersion(version); } List allEnvList = portalSettings.getAllEnvs(); for (Env env : allEnvList) { EnvironmentInfo environmentInfo = adaptEnv2EnvironmentInfo(env); systemInfo.addEnvironment(environmentInfo); } return systemInfo; } @PreAuthorize(value = "@unifiedPermissionValidator.isSuperAdmin()") @GetMapping(value = "/health") public Health checkHealth(@RequestParam String instanceId) { List allEnvs = portalSettings.getAllEnvs(); ServiceDTO service = null; for (final Env env : allEnvs) { EnvironmentInfo envInfo = adaptEnv2EnvironmentInfo(env); if (envInfo.getAdminServices() != null) { for (final ServiceDTO s : envInfo.getAdminServices()) { if (instanceId.equals(s.getInstanceId())) { service = s; break; } } } if (envInfo.getConfigServices() != null) { for (final ServiceDTO s : envInfo.getConfigServices()) { if (instanceId.equals(s.getInstanceId())) { service = s; break; } } } } if (service == null) { throw new IllegalArgumentException("No such instance of instanceId: " + instanceId); } return restTemplate.getForObject(service.getHomepageUrl() + "/health", Health.class); } private EnvironmentInfo adaptEnv2EnvironmentInfo(final Env env) { EnvironmentInfo environmentInfo = new EnvironmentInfo(); String metaServerAddresses = portalMetaDomainService.getMetaServerAddress(env); environmentInfo.setEnv(env); environmentInfo.setActive(portalSettings.isEnvActive(env)); environmentInfo.setMetaServerAddress(metaServerAddresses); String selectedMetaServerAddress = portalMetaDomainService.getDomain(env); try { environmentInfo .setConfigServices(getServerAddress(selectedMetaServerAddress, CONFIG_SERVICE_URL_PATH)); environmentInfo .setAdminServices(getServerAddress(selectedMetaServerAddress, ADMIN_SERVICE_URL_PATH)); } catch (Throwable ex) { String errorMessage = "Loading config/admin services from meta server: " + selectedMetaServerAddress + " failed!"; logger.error(errorMessage, ex); environmentInfo.setErrorMessage(errorMessage + " Exception: " + ex.getMessage()); } return environmentInfo; } private ServiceDTO[] getServerAddress(String metaServerAddress, String path) { String url = metaServerAddress + path; return restTemplate.getForObject(url, ServiceDTO[].class); } private boolean isValidVersion(String version) { return !Objects.equals(version, "java-null"); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/UserInfoController.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.controller; import com.ctrip.framework.apollo.common.exception.BadRequestException; import com.ctrip.framework.apollo.core.utils.StringUtils; import com.ctrip.framework.apollo.portal.component.UnifiedPermissionValidator; import com.ctrip.framework.apollo.portal.entity.bo.UserInfo; import com.ctrip.framework.apollo.portal.entity.po.UserPO; import com.ctrip.framework.apollo.portal.spi.LogoutHandler; import com.ctrip.framework.apollo.portal.spi.UserInfoHolder; import com.ctrip.framework.apollo.portal.spi.UserService; import com.ctrip.framework.apollo.portal.spi.springsecurity.SpringSecurityUserService; import com.ctrip.framework.apollo.portal.util.checker.AuthUserPasswordChecker; import com.ctrip.framework.apollo.portal.util.checker.CheckResult; import java.util.List; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController public class UserInfoController { private static final int USER_ENABLED = 1; private final UserInfoHolder userInfoHolder; private final LogoutHandler logoutHandler; private final UserService userService; private final AuthUserPasswordChecker passwordChecker; private final UnifiedPermissionValidator unifiedPermissionValidator; public UserInfoController(final UserInfoHolder userInfoHolder, final LogoutHandler logoutHandler, final UserService userService, final AuthUserPasswordChecker passwordChecker, UnifiedPermissionValidator unifiedPermissionValidator) { this.userInfoHolder = userInfoHolder; this.logoutHandler = logoutHandler; this.userService = userService; this.passwordChecker = passwordChecker; this.unifiedPermissionValidator = unifiedPermissionValidator; } @PostMapping("/users") public void createOrUpdateUser( @RequestParam(value = "isCreate", defaultValue = "false") boolean isCreate, @RequestBody UserPO user) { if (StringUtils.isContainEmpty(user.getUsername(), user.getPassword())) { throw new BadRequestException("Username and password can not be empty."); } if (!unifiedPermissionValidator.isSuperAdmin() && (!user.getUsername().equals(userInfoHolder.getUser().getUserId()) || user.getEnabled() != USER_ENABLED)) { throw new UnsupportedOperationException("Create or update user operation is unsupported"); } CheckResult pwdCheckRes = passwordChecker.checkWeakPassword(user.getPassword()); if (!pwdCheckRes.isSuccess()) { throw new BadRequestException(pwdCheckRes.getMessage()); } if (userService instanceof SpringSecurityUserService) { if (isCreate) { ((SpringSecurityUserService) userService).create(user); } else { ((SpringSecurityUserService) userService).update(user); } } else { throw new UnsupportedOperationException("Create or update user operation is unsupported"); } } @PreAuthorize(value = "@unifiedPermissionValidator.isSuperAdmin()") @PutMapping("/users/enabled") public void changeUserEnabled(@RequestBody UserPO user) { if (userService instanceof SpringSecurityUserService) { ((SpringSecurityUserService) userService).changeEnabled(user); } else { throw new UnsupportedOperationException("change user enabled is unsupported"); } } @GetMapping("/user") public UserInfo getCurrentUserName() { return userInfoHolder.getUser(); } @GetMapping("/user/logout") public void logout(HttpServletRequest request, HttpServletResponse response) { logoutHandler.logout(request, response); } @GetMapping("/users") public List searchUsersByKeyword(@RequestParam(value = "keyword") String keyword, @RequestParam(value = "includeInactiveUsers", defaultValue = "false") boolean includeInactiveUsers, @RequestParam(value = "offset", defaultValue = "0") int offset, @RequestParam(value = "limit", defaultValue = "10") int limit) { return userService.searchUsers(keyword, offset, limit, includeInactiveUsers); } @GetMapping("/users/{userId}") public UserInfo getUserByUserId(@PathVariable String userId) { return userService.findByUserId(userId); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/enricher/AdditionalUserInfoEnricher.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.enricher; import com.ctrip.framework.apollo.portal.enricher.adapter.UserInfoEnrichedAdapter; import com.ctrip.framework.apollo.portal.entity.bo.UserInfo; import java.util.Map; /** * @author vdisk */ public interface AdditionalUserInfoEnricher { /** * enrich an additional user info for the dto list * * @param adapter enrich adapter * @param userInfoMap userInfo map */ void enrichAdditionalUserInfo(UserInfoEnrichedAdapter adapter, Map userInfoMap); } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/enricher/adapter/AppDtoUserInfoEnrichedAdapter.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.enricher.adapter; import com.ctrip.framework.apollo.common.dto.AppDTO; /** * @author vdisk */ public class AppDtoUserInfoEnrichedAdapter implements UserInfoEnrichedAdapter { private final AppDTO dto; public AppDtoUserInfoEnrichedAdapter(AppDTO dto) { this.dto = dto; } @Override public final String getFirstUserId() { return this.dto.getDataChangeCreatedBy(); } @Override public final void setFirstUserDisplayName(String userDisplayName) { this.dto.setDataChangeCreatedByDisplayName(userDisplayName); } @Override public final String getSecondUserId() { return this.dto.getDataChangeLastModifiedBy(); } @Override public final void setSecondUserDisplayName(String userDisplayName) { this.dto.setDataChangeLastModifiedByDisplayName(userDisplayName); } @Override public final String getThirdUserId() { return this.dto.getOwnerName(); } @Override public final void setThirdUserDisplayName(String userDisplayName) { this.dto.setOwnerDisplayName(userDisplayName); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/enricher/adapter/BaseDtoUserInfoEnrichedAdapter.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.enricher.adapter; import com.ctrip.framework.apollo.common.dto.BaseDTO; /** * @author vdisk */ public class BaseDtoUserInfoEnrichedAdapter implements UserInfoEnrichedAdapter { private final BaseDTO dto; public BaseDtoUserInfoEnrichedAdapter(BaseDTO dto) { this.dto = dto; } @Override public final String getFirstUserId() { return this.dto.getDataChangeCreatedBy(); } @Override public final void setFirstUserDisplayName(String userDisplayName) { this.dto.setDataChangeCreatedByDisplayName(userDisplayName); } @Override public final String getSecondUserId() { return this.dto.getDataChangeLastModifiedBy(); } @Override public final void setSecondUserDisplayName(String userDisplayName) { this.dto.setDataChangeLastModifiedByDisplayName(userDisplayName); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/enricher/adapter/UserInfoEnrichedAdapter.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.enricher.adapter; /** * @author vdisk */ public interface UserInfoEnrichedAdapter { /** * get user id from the object * * @return user id */ String getFirstUserId(); /** * set the user display name for the object * * @param userDisplayName user display name */ void setFirstUserDisplayName(String userDisplayName); /** * get operator id from the object * * @return operator id */ default String getSecondUserId() { return null; } /** * set the user display name for the object * * @param userDisplayName user display name */ default void setSecondUserDisplayName(String userDisplayName) {} /** * get operator id from the object * * @return operator id */ default String getThirdUserId() { return null; } /** * set the user display name for the object * * @param userDisplayName user display name */ default void setThirdUserDisplayName(String userDisplayName) {} } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/enricher/impl/UserDisplayNameEnricher.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.enricher.impl; import com.ctrip.framework.apollo.portal.enricher.AdditionalUserInfoEnricher; import com.ctrip.framework.apollo.portal.enricher.adapter.UserInfoEnrichedAdapter; import com.ctrip.framework.apollo.portal.entity.bo.UserInfo; import java.util.Map; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; /** * @author vdisk */ @Component public class UserDisplayNameEnricher implements AdditionalUserInfoEnricher { @Override public void enrichAdditionalUserInfo(UserInfoEnrichedAdapter adapter, Map userInfoMap) { if (StringUtils.hasText(adapter.getFirstUserId())) { UserInfo userInfo = userInfoMap.get(adapter.getFirstUserId()); if (userInfo != null && StringUtils.hasText(userInfo.getName())) { adapter.setFirstUserDisplayName(userInfo.getName()); } } if (StringUtils.hasText(adapter.getSecondUserId())) { UserInfo userInfo = userInfoMap.get(adapter.getSecondUserId()); if (userInfo != null && StringUtils.hasText(userInfo.getName())) { adapter.setSecondUserDisplayName(userInfo.getName()); } } if (StringUtils.hasText(adapter.getThirdUserId())) { UserInfo userInfo = userInfoMap.get(adapter.getThirdUserId()); if (userInfo != null && StringUtils.hasText(userInfo.getName())) { adapter.setThirdUserDisplayName(userInfo.getName()); } } } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/bo/ConfigBO.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.entity.bo; import com.ctrip.framework.apollo.core.enums.ConfigFileFormat; import com.ctrip.framework.apollo.portal.environment.Env; import com.ctrip.framework.apollo.portal.util.NamespaceBOUtils; /** * a namespace represent. * @author wxq */ public class ConfigBO { private final Env env; private final String ownerName; private final String appId; private final String clusterName; private final String namespace; private final String configFileContent; private final ConfigFileFormat format; private final boolean isPublic; public ConfigBO(Env env, String ownerName, String appId, String clusterName, String namespace, boolean isPublic, String configFileContent, ConfigFileFormat format) { this.env = env; this.ownerName = ownerName; this.appId = appId; this.clusterName = clusterName; this.namespace = namespace; this.isPublic = isPublic; this.configFileContent = configFileContent; this.format = format; } public ConfigBO(Env env, String ownerName, String appId, String clusterName, NamespaceBO namespaceBO) { this(env, ownerName, appId, clusterName, namespaceBO.getBaseInfo().getNamespaceName(), namespaceBO.isPublic(), NamespaceBOUtils.convert2configFileContent(namespaceBO), ConfigFileFormat.fromString(namespaceBO.getFormat())); } @Override public String toString() { return "ConfigBO{" + "env=" + env + ", ownerName='" + ownerName + '\'' + ", appId='" + appId + '\'' + ", clusterName='" + clusterName + '\'' + ", namespace='" + namespace + '\'' + ", isPublic='" + isPublic + '\'' + ", configFileContent='" + configFileContent + '\'' + ", format=" + format + '}'; } public Env getEnv() { return env; } public String getOwnerName() { return ownerName; } public String getAppId() { return appId; } public String getClusterName() { return clusterName; } public String getNamespace() { return namespace; } public String getConfigFileContent() { return configFileContent; } public ConfigFileFormat getFormat() { return format; } public boolean isPublic() { return isPublic; } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/bo/Email.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.entity.bo; import java.util.List; public class Email { private String senderEmailAddress; private List recipients; private String subject; private String body; public String getSenderEmailAddress() { return senderEmailAddress; } public void setSenderEmailAddress(String senderEmailAddress) { this.senderEmailAddress = senderEmailAddress; } public List getRecipients() { return recipients; } public String getRecipientsString() { return String.join(",", recipients); } public void setRecipients(List recipients) { this.recipients = recipients; } public String getSubject() { return subject; } public void setSubject(String subject) { this.subject = subject; } public String getBody() { return body; } public void setBody(String body) { this.body = body; } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/bo/ItemBO.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.entity.bo; import com.ctrip.framework.apollo.common.dto.ItemDTO; public class ItemBO { private ItemDTO item; private boolean isModified; private boolean isDeleted; private boolean isNewlyAdded; private String oldValue; private String newValue; public ItemDTO getItem() { return item; } public void setItem(ItemDTO item) { this.item = item; } public boolean isDeleted() { return isDeleted; } public void setDeleted(boolean deleted) { isDeleted = deleted; } public boolean isModified() { return isModified; } public void setModified(boolean isModified) { this.isModified = isModified; } public String getOldValue() { return oldValue; } public void setOldValue(String oldValue) { this.oldValue = oldValue; } public String getNewValue() { return newValue; } public void setNewValue(String newValue) { this.newValue = newValue; } public boolean isNewlyAdded() { return isNewlyAdded; } public void setNewlyAdded(boolean newlyAdded) { isNewlyAdded = newlyAdded; } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/bo/KVEntity.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.entity.bo; public class KVEntity { private String key; private String value; public KVEntity(String key, String value) { this.key = key; this.value = value; } public String getKey() { return key; } public void setKey(String key) { this.key = key; } public String getValue() { return value; } public void setValue(String value) { this.value = value; } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/bo/NamespaceBO.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.entity.bo; import com.ctrip.framework.apollo.common.dto.NamespaceDTO; import java.util.List; public class NamespaceBO { private NamespaceDTO baseInfo; private int itemModifiedCnt; private List items; private String format; private boolean isPublic; private String parentAppId; private String comment; // is the configs hidden to current user? private boolean isConfigHidden; public NamespaceDTO getBaseInfo() { return baseInfo; } public void setBaseInfo(NamespaceDTO baseInfo) { this.baseInfo = baseInfo; } public int getItemModifiedCnt() { return itemModifiedCnt; } public void setItemModifiedCnt(int itemModifiedCnt) { this.itemModifiedCnt = itemModifiedCnt; } public List getItems() { return items; } public void setItems(List items) { this.items = items; } public String getFormat() { return format; } public void setFormat(String format) { this.format = format; } public boolean isPublic() { return isPublic; } public void setPublic(boolean aPublic) { isPublic = aPublic; } public String getParentAppId() { return parentAppId; } public void setParentAppId(String parentAppId) { this.parentAppId = parentAppId; } public String getComment() { return comment; } public void setComment(String comment) { this.comment = comment; } public boolean isConfigHidden() { return isConfigHidden; } public void setConfigHidden(boolean hidden) { isConfigHidden = hidden; } public void hideItems() { setConfigHidden(true); items.clear(); setItemModifiedCnt(0); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/bo/ReleaseBO.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.entity.bo; import com.ctrip.framework.apollo.common.dto.ReleaseDTO; import java.util.Set; public class ReleaseBO { private ReleaseDTO baseInfo; private Set items; public ReleaseDTO getBaseInfo() { return baseInfo; } public void setBaseInfo(ReleaseDTO baseInfo) { this.baseInfo = baseInfo; } public Set getItems() { return items; } public void setItems(Set items) { this.items = items; } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/bo/ReleaseHistoryBO.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.entity.bo; import com.ctrip.framework.apollo.common.entity.EntityPair; import java.util.Date; import java.util.List; import java.util.Map; public class ReleaseHistoryBO { private long id; private String appId; private String clusterName; private String namespaceName; private String branchName; private String operator; private String operatorDisplayName; private long releaseId; private String releaseTitle; private String releaseComment; private Date releaseTime; private String releaseTimeFormatted; private List> configuration; private boolean isReleaseAbandoned; private long previousReleaseId; private int operation; private Map operationContext; public long getId() { return id; } public void setId(long id) { this.id = id; } public String getAppId() { return appId; } public void setAppId(String appId) { this.appId = appId; } public String getClusterName() { return clusterName; } public void setClusterName(String clusterName) { this.clusterName = clusterName; } public String getNamespaceName() { return namespaceName; } public void setNamespaceName(String namespaceName) { this.namespaceName = namespaceName; } public String getBranchName() { return branchName; } public void setBranchName(String branchName) { this.branchName = branchName; } public String getOperator() { return operator; } public void setOperator(String operator) { this.operator = operator; } public String getOperatorDisplayName() { return operatorDisplayName; } public void setOperatorDisplayName(String operatorDisplayName) { this.operatorDisplayName = operatorDisplayName; } public long getReleaseId() { return releaseId; } public void setReleaseId(long releaseId) { this.releaseId = releaseId; } public String getReleaseTitle() { return releaseTitle; } public void setReleaseTitle(String releaseTitle) { this.releaseTitle = releaseTitle; } public String getReleaseComment() { return releaseComment; } public void setReleaseComment(String releaseComment) { this.releaseComment = releaseComment; } public Date getReleaseTime() { return releaseTime; } public void setReleaseTime(Date releaseTime) { this.releaseTime = releaseTime; } public String getReleaseTimeFormatted() { return releaseTimeFormatted; } public void setReleaseTimeFormatted(String releaseTimeFormatted) { this.releaseTimeFormatted = releaseTimeFormatted; } public List> getConfiguration() { return configuration; } public void setConfiguration(List> configuration) { this.configuration = configuration; } public boolean isReleaseAbandoned() { return isReleaseAbandoned; } public void setReleaseAbandoned(boolean releaseAbandoned) { isReleaseAbandoned = releaseAbandoned; } public long getPreviousReleaseId() { return previousReleaseId; } public void setPreviousReleaseId(long previousReleaseId) { this.previousReleaseId = previousReleaseId; } public int getOperation() { return operation; } public void setOperation(int operation) { this.operation = operation; } public Map getOperationContext() { return operationContext; } public void setOperationContext(Map operationContext) { this.operationContext = operationContext; } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/bo/UserInfo.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.entity.bo; public class UserInfo { private String userId; private String name; private String email; private int enabled; public UserInfo() { } public UserInfo(String userId) { this.userId = userId; } public String getUserId() { return userId; } public void setUserId(String userId) { this.userId = userId; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public int getEnabled() { return enabled; } public void setEnabled(int enabled) { this.enabled = enabled; } @Override public boolean equals(Object o) { if (o instanceof UserInfo) { if (o == this) { return true; } UserInfo anotherUser = (UserInfo) o; return userId.equals(anotherUser.userId); } return false; } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/model/AppModel.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.entity.model; import com.ctrip.framework.apollo.common.utils.InputValidator; import java.util.Set; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Pattern; public class AppModel { @NotBlank(message = "name cannot be blank") private String name; @NotBlank(message = "appId cannot be blank") @Pattern(regexp = InputValidator.CLUSTER_NAMESPACE_VALIDATOR, message = "Invalid AppId format: " + InputValidator.INVALID_CLUSTER_NAMESPACE_MESSAGE) private String appId; @NotBlank(message = "orgId cannot be blank") private String orgId; @NotBlank(message = "orgName cannot be blank") private String orgName; @NotBlank(message = "ownerName cannot be blank") private String ownerName; private Set admins; public String getName() { return name; } public void setName(String name) { this.name = name; } public String getAppId() { return appId; } public void setAppId(String appId) { this.appId = appId; } public String getOrgId() { return orgId; } public void setOrgId(String orgId) { this.orgId = orgId; } public String getOrgName() { return orgName; } public void setOrgName(String orgName) { this.orgName = orgName; } public String getOwnerName() { return ownerName; } public void setOwnerName(String ownerName) { this.ownerName = ownerName; } public Set getAdmins() { return admins; } public void setAdmins(Set admins) { this.admins = admins; } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/model/NamespaceCreationModel.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.entity.model; import com.ctrip.framework.apollo.common.dto.NamespaceDTO; public class NamespaceCreationModel { private String env; private NamespaceDTO namespace; public String getEnv() { return env; } public void setEnv(String env) { this.env = env; } public NamespaceDTO getNamespace() { return namespace; } public void setNamespace(NamespaceDTO namespace) { this.namespace = namespace; } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/model/NamespaceGrayDelReleaseModel.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.entity.model; import java.util.Set; public class NamespaceGrayDelReleaseModel extends NamespaceReleaseModel implements Verifiable { private Set grayDelKeys; public Set getGrayDelKeys() { return grayDelKeys; } public void setGrayDelKeys(Set grayDelKeys) { this.grayDelKeys = grayDelKeys; } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/model/NamespaceReleaseModel.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.entity.model; import com.ctrip.framework.apollo.portal.environment.Env; import com.ctrip.framework.apollo.core.utils.StringUtils; public class NamespaceReleaseModel implements Verifiable { private String appId; private String env; private String clusterName; private String namespaceName; private String releaseTitle; private String releaseComment; private String releasedBy; private boolean isEmergencyPublish; @Override public boolean isInvalid() { return StringUtils.isContainEmpty(appId, env, clusterName, namespaceName, releaseTitle); } public String getAppId() { return appId; } public void setAppId(String appId) { this.appId = appId; } public Env getEnv() { return Env.valueOf(env); } public void setEnv(String env) { this.env = env; } public String getClusterName() { return clusterName; } public void setClusterName(String clusterName) { this.clusterName = clusterName; } public String getNamespaceName() { return namespaceName; } public void setNamespaceName(String namespaceName) { this.namespaceName = namespaceName; } public String getReleaseTitle() { return releaseTitle; } public void setReleaseTitle(String releaseTitle) { this.releaseTitle = releaseTitle; } public String getReleaseComment() { return releaseComment; } public void setReleaseComment(String releaseComment) { this.releaseComment = releaseComment; } public String getReleasedBy() { return releasedBy; } public void setReleasedBy(String releasedBy) { this.releasedBy = releasedBy; } public boolean isEmergencyPublish() { return isEmergencyPublish; } public void setEmergencyPublish(boolean emergencyPublish) { isEmergencyPublish = emergencyPublish; } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/model/NamespaceSyncModel.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.entity.model; import com.ctrip.framework.apollo.common.dto.ItemDTO; import com.ctrip.framework.apollo.portal.entity.vo.NamespaceIdentifier; import org.springframework.util.CollectionUtils; import java.util.List; public class NamespaceSyncModel implements Verifiable { private List syncToNamespaces; private List syncItems; @Override public boolean isInvalid() { if (CollectionUtils.isEmpty(syncToNamespaces) || CollectionUtils.isEmpty(syncItems)) { return true; } for (NamespaceIdentifier namespaceIdentifier : syncToNamespaces) { if (namespaceIdentifier.isInvalid()) { return true; } } return false; } public boolean syncToNamespacesValid(String appId, String namespaceName) { for (NamespaceIdentifier namespaceIdentifier : syncToNamespaces) { if (appId.equals(namespaceIdentifier.getAppId()) && namespaceName.equals(namespaceIdentifier.getNamespaceName())) { continue; } return false; } return true; } public List getSyncToNamespaces() { return syncToNamespaces; } public void setSyncToNamespaces(List syncToNamespaces) { this.syncToNamespaces = syncToNamespaces; } public List getSyncItems() { return syncItems; } public void setSyncItems(List syncItems) { this.syncItems = syncItems; } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/model/NamespaceTextModel.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.entity.model; import com.ctrip.framework.apollo.core.enums.ConfigFileFormat; import com.ctrip.framework.apollo.portal.environment.Env; import com.ctrip.framework.apollo.core.utils.StringUtils; public class NamespaceTextModel implements Verifiable { private String appId; private String env; private String clusterName; private String namespaceName; private long namespaceId; private String format; private String configText; private String operator; @Override public boolean isInvalid() { return StringUtils.isContainEmpty(appId, env, clusterName, namespaceName) || namespaceId <= 0; } public String getAppId() { return appId; } public void setAppId(String appId) { this.appId = appId; } public Env getEnv() { return Env.valueOf(env); } public void setEnv(String env) { this.env = env; } public String getClusterName() { return clusterName; } public void setClusterName(String clusterName) { this.clusterName = clusterName; } public String getNamespaceName() { return namespaceName; } public void setNamespaceName(String namespaceName) { this.namespaceName = namespaceName; } public long getNamespaceId() { return namespaceId; } public void setNamespaceId(long namespaceId) { this.namespaceId = namespaceId; } public String getConfigText() { return configText; } public void setConfigText(String configText) { this.configText = configText; } public ConfigFileFormat getFormat() { return ConfigFileFormat.fromString(this.format); } public void setFormat(String format) { this.format = format; } public String getOperator() { return operator; } public void setOperator(String operator) { this.operator = operator; } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/model/Verifiable.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.entity.model; public interface Verifiable { boolean isInvalid(); } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/po/Authority.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.entity.po; import com.google.common.base.MoreObjects; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.Table; /** * @author lepdou 2022-01-20 */ @Entity @Table(name = "`Authorities`") public class Authority { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "`Id`") private long id; @Column(name = "`Username`", nullable = false) private String username; @Column(name = "`Authority`", nullable = false) private String authority; public long getId() { return id; } public void setId(long id) { this.id = id; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getAuthority() { return authority; } public void setAuthority(String authority) { this.authority = authority; } @Override public String toString() { return MoreObjects.toStringHelper(this).omitNullValues().add("id", id).add("username", username) .add("authority", authority).toString(); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/po/Favorite.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.entity.po; import com.ctrip.framework.apollo.common.entity.BaseEntity; import org.hibernate.annotations.SQLDelete; import org.hibernate.annotations.Where; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.Table; @Entity @Table(name = "`Favorite`") @SQLDelete( sql = "Update `Favorite` set IsDeleted = true, DeletedAt = ROUND(UNIX_TIMESTAMP(NOW(4))*1000) where Id = ?") @Where(clause = "`IsDeleted` = false") public class Favorite extends BaseEntity { @Column(name = "`AppId`", nullable = false) private String appId; @Column(name = "`UserId`", nullable = false) private String userId; @Column(name = "`Position`") private long position; public String getAppId() { return appId; } public void setAppId(String appId) { this.appId = appId; } public String getUserId() { return userId; } public void setUserId(String userId) { this.userId = userId; } public long getPosition() { return position; } public void setPosition(long position) { this.position = position; } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/po/Permission.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.entity.po; import com.ctrip.framework.apollo.common.entity.BaseEntity; import java.util.Objects; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.Table; import org.hibernate.annotations.SQLDelete; import org.hibernate.annotations.Where; /** * @author Jason Song(song_s@ctrip.com) */ @Entity @Table(name = "`Permission`") @SQLDelete( sql = "Update `Permission` set IsDeleted = true, DeletedAt = ROUND(UNIX_TIMESTAMP(NOW(4))*1000) where Id = ?") @Where(clause = "`IsDeleted` = false") public class Permission extends BaseEntity { @Column(name = "`PermissionType`", nullable = false) private String permissionType; @Column(name = "`TargetId`", nullable = false) private String targetId; public Permission() {} public Permission(String permissionType, String targetId) { this.permissionType = permissionType; this.targetId = targetId; } public String getPermissionType() { return permissionType; } public void setPermissionType(String permissionType) { this.permissionType = permissionType; } public String getTargetId() { return targetId; } public void setTargetId(String targetId) { this.targetId = targetId; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (!(o instanceof Permission)) { return false; } Permission that = (Permission) o; return Objects.equals(permissionType, that.permissionType) && Objects.equals(targetId, that.targetId); } @Override public int hashCode() { return Objects.hash(permissionType, targetId); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/po/Role.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.entity.po; import com.ctrip.framework.apollo.audit.annotation.ApolloAuditLogDataInfluenceTable; import com.ctrip.framework.apollo.audit.annotation.ApolloAuditLogDataInfluenceTableField; import com.ctrip.framework.apollo.common.entity.BaseEntity; import org.hibernate.annotations.SQLDelete; import org.hibernate.annotations.Where; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.Table; /** * @author Jason Song(song_s@ctrip.com) */ @Entity @Table(name = "`Role`") @SQLDelete( sql = "Update `Role` set IsDeleted = true, DeletedAt = ROUND(UNIX_TIMESTAMP(NOW(4))*1000) where Id = ?") @Where(clause = "`IsDeleted` = false") @ApolloAuditLogDataInfluenceTable(tableName = "Role") public class Role extends BaseEntity { @ApolloAuditLogDataInfluenceTableField(fieldName = "RoleName") @Column(name = "`RoleName`", nullable = false) private String roleName; public String getRoleName() { return roleName; } public void setRoleName(String roleName) { this.roleName = roleName; } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/po/RolePermission.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.entity.po; import com.ctrip.framework.apollo.common.entity.BaseEntity; import org.hibernate.annotations.SQLDelete; import org.hibernate.annotations.Where; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.Table; /** * @author Jason Song(song_s@ctrip.com) */ @Entity @Table(name = "`RolePermission`") @SQLDelete( sql = "Update `RolePermission` set IsDeleted = true, DeletedAt = ROUND(UNIX_TIMESTAMP(NOW(4))*1000) where Id = ?") @Where(clause = "`IsDeleted` = false") public class RolePermission extends BaseEntity { @Column(name = "`RoleId`", nullable = false) private long roleId; @Column(name = "`PermissionId`", nullable = false) private long permissionId; public long getRoleId() { return roleId; } public void setRoleId(long roleId) { this.roleId = roleId; } public long getPermissionId() { return permissionId; } public void setPermissionId(long permissionId) { this.permissionId = permissionId; } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/po/ServerConfig.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.entity.po; import com.ctrip.framework.apollo.audit.annotation.ApolloAuditLogDataInfluenceTable; import com.ctrip.framework.apollo.audit.annotation.ApolloAuditLogDataInfluenceTableField; import com.ctrip.framework.apollo.common.entity.BaseEntity; import jakarta.validation.constraints.NotBlank; import org.hibernate.annotations.SQLDelete; import org.hibernate.annotations.Where; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.Table; /** * @author Jason Song(song_s@ctrip.com) */ @Entity @Table(name = "`ServerConfig`") @SQLDelete( sql = "Update `ServerConfig` set IsDeleted = true, DeletedAt = ROUND(UNIX_TIMESTAMP(NOW(4))*1000) where Id = ?") @Where(clause = "`IsDeleted` = false") @ApolloAuditLogDataInfluenceTable(tableName = "ServerConfig") public class ServerConfig extends BaseEntity { @NotBlank(message = "ServerConfig.Key cannot be blank") @Column(name = "`Key`", nullable = false) @ApolloAuditLogDataInfluenceTableField(fieldName = "Key") private String key; @NotBlank(message = "ServerConfig.Value cannot be blank") @Column(name = "`Value`", nullable = false) @ApolloAuditLogDataInfluenceTableField(fieldName = "Value") private String value; @Column(name = "`Comment`", nullable = false) private String comment; public String getKey() { return key; } public void setKey(String key) { this.key = key; } public String getValue() { return value; } public void setValue(String value) { this.value = value; } public String getComment() { return comment; } public void setComment(String comment) { this.comment = comment; } @Override public String toString() { return toStringHelper().add("key", key).add("value", value).add("comment", comment).toString(); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/po/UserPO.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.entity.po; import com.ctrip.framework.apollo.portal.entity.bo.UserInfo; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.Table; /** * @author lepdou 2017-04-08 */ @Entity @Table(name = "`Users`") public class UserPO { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "`Id`") private long id; @Column(name = "`Username`", nullable = false) private String username; @Column(name = "`UserDisplayName`", nullable = false) private String userDisplayName; @Column(name = "`Password`", nullable = false) private String password; @Column(name = "`Email`", nullable = false) private String email; @Column(name = "`Enabled`", nullable = false) private int enabled; public long getId() { return id; } public void setId(long id) { this.id = id; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getUserDisplayName() { return userDisplayName; } public void setUserDisplayName(String userDisplayName) { this.userDisplayName = userDisplayName; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public int getEnabled() { return enabled; } public void setEnabled(int enabled) { this.enabled = enabled; } public UserInfo toUserInfo() { UserInfo userInfo = new UserInfo(); userInfo.setUserId(this.getUsername()); userInfo.setName(this.getUserDisplayName()); userInfo.setEmail(this.getEmail()); userInfo.setEnabled(this.getEnabled()); return userInfo; } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/po/UserRole.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.entity.po; import com.ctrip.framework.apollo.audit.annotation.ApolloAuditLogDataInfluenceTable; import com.ctrip.framework.apollo.audit.annotation.ApolloAuditLogDataInfluenceTableField; import com.ctrip.framework.apollo.common.entity.BaseEntity; import org.hibernate.annotations.SQLDelete; import org.hibernate.annotations.Where; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.Table; /** * @author Jason Song(song_s@ctrip.com) */ @Entity @Table(name = "`UserRole`") @SQLDelete( sql = "Update `UserRole` set IsDeleted = true, DeletedAt = ROUND(UNIX_TIMESTAMP(NOW(4))*1000) where Id = ?") @Where(clause = "`IsDeleted` = false") @ApolloAuditLogDataInfluenceTable(tableName = "UserRole") public class UserRole extends BaseEntity { @ApolloAuditLogDataInfluenceTableField(fieldName = "UserId") @Column(name = "`UserId`", nullable = false) private String userId; @ApolloAuditLogDataInfluenceTableField(fieldName = "RoleId") @Column(name = "`RoleId`", nullable = false) private long roleId; public String getUserId() { return userId; } public void setUserId(String userId) { this.userId = userId; } public long getRoleId() { return roleId; } public void setRoleId(long roleId) { this.roleId = roleId; } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/vo/AppRolesAssignedUsers.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.entity.vo; import com.ctrip.framework.apollo.portal.entity.bo.UserInfo; import java.util.Set; public class AppRolesAssignedUsers { private String appId; private Set masterUsers; public String getAppId() { return appId; } public void setAppId(String appId) { this.appId = appId; } public Set getMasterUsers() { return masterUsers; } public void setMasterUsers(Set masterUsers) { this.masterUsers = masterUsers; } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/vo/Change.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.entity.vo; import com.ctrip.framework.apollo.common.entity.EntityPair; import com.ctrip.framework.apollo.portal.entity.bo.KVEntity; import com.ctrip.framework.apollo.portal.enums.ChangeType; public class Change { private ChangeType type; private EntityPair entity; public Change(ChangeType type, EntityPair entity) { this.type = type; this.entity = entity; } public ChangeType getType() { return type; } public void setType(ChangeType type) { this.type = type; } public EntityPair getEntity() { return entity; } public void setEntity(EntityPair entity) { this.entity = entity; } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/vo/ClusterNamespaceRolesAssignedUsers.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.entity.vo; import com.ctrip.framework.apollo.portal.entity.bo.UserInfo; import java.util.Set; public class ClusterNamespaceRolesAssignedUsers { private String appId; private String env; private String cluster; private Set modifyRoleUsers; private Set releaseRoleUsers; public String getEnv() { return env; } public void setEnv(String env) { this.env = env; } public String getAppId() { return appId; } public void setAppId(String appId) { this.appId = appId; } public String getCluster() { return cluster; } public void setCluster(String cluster) { this.cluster = cluster; } public Set getModifyRoleUsers() { return modifyRoleUsers; } public void setModifyRoleUsers(Set modifyRoleUsers) { this.modifyRoleUsers = modifyRoleUsers; } public Set getReleaseRoleUsers() { return releaseRoleUsers; } public void setReleaseRoleUsers(Set releaseRoleUsers) { this.releaseRoleUsers = releaseRoleUsers; } @Override public String toString() { return "ClusterNamespaceRolesAssignedUsers{" + "appId='" + appId + '\'' + ", env='" + env + '\'' + ", cluster='" + cluster + '\'' + ", modifyRoleUsers=" + modifyRoleUsers + ", releaseRoleUsers=" + releaseRoleUsers + '}'; } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/vo/EnvClusterInfo.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.entity.vo; import com.ctrip.framework.apollo.common.dto.ClusterDTO; import com.ctrip.framework.apollo.portal.environment.Env; import java.util.List; public class EnvClusterInfo { private String env; private List clusters; public EnvClusterInfo(Env env) { this.env = env.toString(); } public Env getEnv() { return Env.valueOf(env); } public void setEnv(Env env) { this.env = env.toString(); } public List getClusters() { return clusters; } public void setClusters(List clusters) { this.clusters = clusters; } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/vo/EnvironmentInfo.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.entity.vo; import com.ctrip.framework.apollo.core.dto.ServiceDTO; import com.ctrip.framework.apollo.portal.environment.Env; public class EnvironmentInfo { private String env; private boolean active; private String metaServerAddress; private ServiceDTO[] configServices; private ServiceDTO[] adminServices; private String errorMessage; public Env getEnv() { return Env.valueOf(env); } public void setEnv(Env env) { this.env = env.toString(); } public boolean isActive() { return active; } public void setActive(boolean active) { this.active = active; } public String getMetaServerAddress() { return metaServerAddress; } public void setMetaServerAddress(String metaServerAddress) { this.metaServerAddress = metaServerAddress; } public ServiceDTO[] getConfigServices() { return configServices; } public void setConfigServices(ServiceDTO[] configServices) { this.configServices = configServices; } public ServiceDTO[] getAdminServices() { return adminServices; } public void setAdminServices(ServiceDTO[] adminServices) { this.adminServices = adminServices; } public String getErrorMessage() { return errorMessage; } public void setErrorMessage(String errorMessage) { this.errorMessage = errorMessage; } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/vo/ItemDiffs.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.entity.vo; import com.ctrip.framework.apollo.common.dto.ItemChangeSets; public class ItemDiffs { private NamespaceIdentifier namespace; private ItemChangeSets diffs; private String extInfo; public ItemDiffs(NamespaceIdentifier namespace) { this.namespace = namespace; } public NamespaceIdentifier getNamespace() { return namespace; } public void setNamespace(NamespaceIdentifier namespace) { this.namespace = namespace; } public ItemChangeSets getDiffs() { return diffs; } public void setDiffs(ItemChangeSets diffs) { this.diffs = diffs; } public String getExtInfo() { return extInfo; } public void setExtInfo(String extInfo) { this.extInfo = extInfo; } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/vo/ItemInfo.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.entity.vo; public class ItemInfo { private String appId; private String envName; private String clusterName; private String namespaceName; private String key; private String value; public ItemInfo() {} public ItemInfo(String appId, String envName, String clusterName, String namespaceName, String key, String value) { this.appId = appId; this.envName = envName; this.clusterName = clusterName; this.namespaceName = namespaceName; this.key = key; this.value = value; } public String getAppId() { return appId; } public void setAppId(String appId) { this.appId = appId; } public String getEnvName() { return envName; } public void setEnvName(String envName) { this.envName = envName; } public String getClusterName() { return clusterName; } public void setClusterName(String clusterName) { this.clusterName = clusterName; } public String getNamespaceName() { return namespaceName; } public void setNamespaceName(String namespaceName) { this.namespaceName = namespaceName; } public String getKey() { return key; } public void setKey(String key) { this.key = key; } public String getValue() { return value; } public void setValue(String value) { this.value = value; } @Override public String toString() { return "ItemInfo{" + "appId='" + appId + '\'' + ", envName='" + envName + '\'' + ", clusterName='" + clusterName + '\'' + ", namespaceName='" + namespaceName + '\'' + ", key='" + key + '\'' + ", value='" + value + '\'' + '}'; } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/vo/LockInfo.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.entity.vo; public class LockInfo { private String lockOwner; private boolean isEmergencyPublishAllowed; public String getLockOwner() { return lockOwner; } public void setLockOwner(String lockOwner) { this.lockOwner = lockOwner; } public boolean isEmergencyPublishAllowed() { return isEmergencyPublishAllowed; } public void setEmergencyPublishAllowed(boolean emergencyPublishAllowed) { isEmergencyPublishAllowed = emergencyPublishAllowed; } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/vo/NamespaceEnvRolesAssignedUsers.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.entity.vo; import com.ctrip.framework.apollo.portal.environment.Env; public class NamespaceEnvRolesAssignedUsers extends NamespaceRolesAssignedUsers { private String env; public Env getEnv() { return Env.valueOf(env); } public void setEnv(Env env) { this.env = env.toString(); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/vo/NamespaceIdentifier.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.entity.vo; import com.ctrip.framework.apollo.portal.environment.Env; import com.ctrip.framework.apollo.core.utils.StringUtils; import com.ctrip.framework.apollo.portal.entity.model.Verifiable; public class NamespaceIdentifier implements Verifiable { private String appId; private String env; private String clusterName; private String namespaceName; public String getAppId() { return appId; } public void setAppId(String appId) { this.appId = appId; } public Env getEnv() { return Env.valueOf(env); } public void setEnv(String env) { this.env = env; } public String getClusterName() { return clusterName; } public void setClusterName(String clusterName) { this.clusterName = clusterName; } public String getNamespaceName() { return namespaceName; } public void setNamespaceName(String namespaceName) { this.namespaceName = namespaceName; } @Override public boolean isInvalid() { return StringUtils.isContainEmpty(env, clusterName, namespaceName); } @Override public String toString() { return "NamespaceIdentifer{" + "appId='" + appId + '\'' + ", env='" + env + '\'' + ", clusterName='" + clusterName + '\'' + ", namespaceName='" + namespaceName + '\'' + '}'; } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/vo/NamespaceRolesAssignedUsers.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.entity.vo; import com.ctrip.framework.apollo.portal.entity.bo.UserInfo; import java.util.Set; public class NamespaceRolesAssignedUsers { private String appId; private String namespaceName; private Set modifyRoleUsers; private Set releaseRoleUsers; public String getAppId() { return appId; } public void setAppId(String appId) { this.appId = appId; } public String getNamespaceName() { return namespaceName; } public void setNamespaceName(String namespaceName) { this.namespaceName = namespaceName; } public Set getModifyRoleUsers() { return modifyRoleUsers; } public void setModifyRoleUsers(Set modifyRoleUsers) { this.modifyRoleUsers = modifyRoleUsers; } public Set getReleaseRoleUsers() { return releaseRoleUsers; } public void setReleaseRoleUsers(Set releaseRoleUsers) { this.releaseRoleUsers = releaseRoleUsers; } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/vo/NamespaceUsage.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.entity.vo; /** * @author kl (http://kailing.pub) * @since 2022/8/9 */ public class NamespaceUsage { private String namespaceName; private String appId; private String clusterName; private String envName; private int instanceCount; private int branchInstanceCount; private int linkedNamespaceCount; public NamespaceUsage() {} public NamespaceUsage(String namespaceName, String appId, String clusterName, String envName) { this.namespaceName = namespaceName; this.appId = appId; this.clusterName = clusterName; this.envName = envName; } public String getNamespaceName() { return namespaceName; } public void setNamespaceName(String namespaceName) { this.namespaceName = namespaceName; } public String getAppId() { return appId; } public void setAppId(String appId) { this.appId = appId; } public String getClusterName() { return clusterName; } public void setClusterName(String clusterName) { this.clusterName = clusterName; } public String getEnvName() { return envName; } public void setEnvName(String envName) { this.envName = envName; } public int getInstanceCount() { return instanceCount; } public void setInstanceCount(int instanceCount) { this.instanceCount = instanceCount; } public int getBranchInstanceCount() { return branchInstanceCount; } public void setBranchInstanceCount(int branchInstanceCount) { this.branchInstanceCount = branchInstanceCount; } public int getLinkedNamespaceCount() { return linkedNamespaceCount; } public void setLinkedNamespaceCount(int linkedNamespaceCount) { this.linkedNamespaceCount = linkedNamespaceCount; } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/vo/Number.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.entity.vo; public class Number { private int num; public Number(int num) { this.num = num; } public int getNum() { return num; } public void setNum(int num) { this.num = num; } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/vo/Organization.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.entity.vo; /** * @author Jason Song(song_s@ctrip.com) */ public class Organization { private String orgId; private String orgName; public String getOrgId() { return orgId; } public void setOrgId(String orgId) { this.orgId = orgId; } public String getOrgName() { return orgName; } public void setOrgName(String orgName) { this.orgName = orgName; } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/vo/PageSetting.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.entity.vo; public class PageSetting { private String wikiAddress; private boolean canAppAdminCreatePrivateNamespace; public String getWikiAddress() { return wikiAddress; } public void setWikiAddress(String wikiAddress) { this.wikiAddress = wikiAddress; } public boolean isCanAppAdminCreatePrivateNamespace() { return canAppAdminCreatePrivateNamespace; } public void setCanAppAdminCreatePrivateNamespace(boolean canAppAdminCreatePrivateNamespace) { this.canAppAdminCreatePrivateNamespace = canAppAdminCreatePrivateNamespace; } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/vo/PermissionCondition.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.entity.vo; public class PermissionCondition { private boolean hasPermission; public boolean hasPermission() { return hasPermission; } public void setHasPermission(boolean hasPermission) { this.hasPermission = hasPermission; } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/vo/ReleaseCompareResult.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.entity.vo; import com.ctrip.framework.apollo.common.entity.EntityPair; import com.ctrip.framework.apollo.portal.entity.bo.KVEntity; import com.ctrip.framework.apollo.portal.enums.ChangeType; import java.util.LinkedList; import java.util.List; public class ReleaseCompareResult { private List changes = new LinkedList<>(); public void addEntityPair(ChangeType type, KVEntity firstEntity, KVEntity secondEntity) { changes.add(new Change(type, new EntityPair<>(firstEntity, secondEntity))); } public boolean hasContent() { return !changes.isEmpty(); } public List getChanges() { return changes; } public void setChanges(List changes) { this.changes = changes; } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/vo/SystemInfo.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.entity.vo; import com.google.common.collect.Lists; import java.util.List; public class SystemInfo { private String version; private List environments = Lists.newLinkedList(); public String getVersion() { return version; } public void setVersion(String version) { this.version = version; } public List getEnvironments() { return environments; } public void addEnvironment(EnvironmentInfo environment) { this.environments.add(environment); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/vo/consumer/ConsumerCreateRequestVO.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.entity.vo.consumer; /** * @see com.ctrip.framework.apollo.openapi.entity.Consumer */ public class ConsumerCreateRequestVO { private String appId; private boolean allowCreateApplication; private String name; private String orgId; private String orgName; private String ownerName; private boolean rateLimitEnabled; private int rateLimit; public String getAppId() { return appId; } public void setAppId(String appId) { this.appId = appId; } public boolean isAllowCreateApplication() { return allowCreateApplication; } public void setAllowCreateApplication(boolean allowCreateApplication) { this.allowCreateApplication = allowCreateApplication; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getOrgId() { return orgId; } public void setOrgId(String orgId) { this.orgId = orgId; } public String getOrgName() { return orgName; } public void setOrgName(String orgName) { this.orgName = orgName; } public String getOwnerName() { return ownerName; } public void setOwnerName(String ownerName) { this.ownerName = ownerName; } public boolean isRateLimitEnabled() { return rateLimitEnabled; } public void setRateLimitEnabled(boolean rateLimitEnabled) { this.rateLimitEnabled = rateLimitEnabled; } public int getRateLimit() { return rateLimit; } public void setRateLimit(int rateLimit) { this.rateLimit = rateLimit; } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/vo/consumer/ConsumerInfo.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.entity.vo.consumer; /** * @see com.ctrip.framework.apollo.openapi.entity.Consumer * @see com.ctrip.framework.apollo.openapi.entity.ConsumerRole */ public class ConsumerInfo { private String appId; private String name; private String orgId; private String orgName; private String ownerName; private String ownerEmail; private long consumerId; private String token; private boolean allowCreateApplication; private Integer rateLimit; public String getAppId() { return appId; } public void setAppId(String appId) { this.appId = appId; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getOrgId() { return orgId; } public void setOrgId(String orgId) { this.orgId = orgId; } public String getOrgName() { return orgName; } public void setOrgName(String orgName) { this.orgName = orgName; } public String getOwnerName() { return ownerName; } public void setOwnerName(String ownerName) { this.ownerName = ownerName; } public String getOwnerEmail() { return ownerEmail; } public void setOwnerEmail(String ownerEmail) { this.ownerEmail = ownerEmail; } public long getConsumerId() { return consumerId; } public void setConsumerId(long consumerId) { this.consumerId = consumerId; } public String getToken() { return token; } public void setToken(String token) { this.token = token; } public boolean isAllowCreateApplication() { return allowCreateApplication; } public void setAllowCreateApplication(boolean allowCreateApplication) { this.allowCreateApplication = allowCreateApplication; } public Integer getRateLimit() { return rateLimit; } public void setRateLimit(Integer rateLimit) { this.rateLimit = rateLimit; } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/enums/ChangeType.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.enums; public enum ChangeType { ADDED, MODIFIED, DELETED } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/environment/DatabasePortalMetaServerProvider.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.environment; import com.ctrip.framework.apollo.portal.component.config.PortalConfig; import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * load meta server addressed from database. * PortalDB.ServerConfig */ class DatabasePortalMetaServerProvider implements PortalMetaServerProvider { private static final Logger logger = LoggerFactory.getLogger(DatabasePortalMetaServerProvider.class); /** * read config from database */ private final PortalConfig portalConfig; private volatile Map addresses; DatabasePortalMetaServerProvider(final PortalConfig portalConfig) { this.portalConfig = portalConfig; reload(); } @Override public String getMetaServerAddress(Env targetEnv) { return addresses.get(targetEnv); } @Override public boolean exists(Env targetEnv) { return addresses.containsKey(targetEnv); } @Override public void reload() { Map map = portalConfig.getMetaServers(); addresses = Env.transformToEnvMap(map); logger.info("Loaded meta server addresses from portal config: {}", addresses); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/environment/DefaultPortalMetaServerProvider.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.environment; import static com.ctrip.framework.apollo.portal.environment.Env.transformToEnvMap; import com.ctrip.framework.apollo.core.utils.ResourceUtils; import com.ctrip.framework.apollo.portal.util.KeyValueUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Map; import java.util.Properties; import java.util.concurrent.ConcurrentHashMap; /** * Only use in apollo-portal * load all meta server address from * - System Property [key ends with "_meta" (case insensitive)] * - OS environment variable [key ends with "_meta" (case insensitive)] * - user's configuration file [key ends with ".meta" (case insensitive)] * when apollo-portal start up. * @see com.ctrip.framework.apollo.core.internals.LegacyMetaServerProvider * @author wxq */ class DefaultPortalMetaServerProvider implements PortalMetaServerProvider { private static final Logger logger = LoggerFactory.getLogger(DefaultPortalMetaServerProvider.class); /** * environments and their meta server address * properties file path */ private static final String APOLLO_ENV_PROPERTIES_FILE_PATH = "apollo-env.properties"; private volatile Map domains; DefaultPortalMetaServerProvider() { reload(); } @Override public String getMetaServerAddress(Env targetEnv) { String metaServerAddress = domains.get(targetEnv); return metaServerAddress == null ? null : metaServerAddress.trim(); } @Override public boolean exists(Env targetEnv) { return domains.containsKey(targetEnv); } @Override public void reload() { domains = initializeDomains(); logger.info( "Loaded meta server addresses from system property, os environment and properties file: {}", domains); } /** * load all environment's meta address dynamically when this class loaded by JVM */ private Map initializeDomains() { // add to domain Map map = new ConcurrentHashMap<>(); // lower priority add first map.putAll(getDomainsFromPropertiesFile()); map.putAll(getDomainsFromOSEnvironment()); map.putAll(getDomainsFromSystemProperty()); // log all return map; } private Map getDomainsFromSystemProperty() { // find key-value from System Property which key ends with "_meta" (case insensitive) Map metaServerAddressesFromSystemProperty = KeyValueUtils.filterWithKeyIgnoreCaseEndsWith(System.getProperties(), "_meta"); // remove key's suffix "_meta" (case insensitive) metaServerAddressesFromSystemProperty = KeyValueUtils.removeKeySuffix(metaServerAddressesFromSystemProperty, "_meta".length()); return transformToEnvMap(metaServerAddressesFromSystemProperty); } private Map getDomainsFromOSEnvironment() { // find key-value from OS environment variable which key ends with "_meta" (case insensitive) Map metaServerAddressesFromOSEnvironment = KeyValueUtils.filterWithKeyIgnoreCaseEndsWith(System.getenv(), "_meta"); // remove key's suffix "_meta" (case insensitive) metaServerAddressesFromOSEnvironment = KeyValueUtils.removeKeySuffix(metaServerAddressesFromOSEnvironment, "_meta".length()); return transformToEnvMap(metaServerAddressesFromOSEnvironment); } private Map getDomainsFromPropertiesFile() { // find key-value from properties file which key ends with ".meta" (case insensitive) Properties properties = new Properties(); properties = ResourceUtils.readConfigFile(APOLLO_ENV_PROPERTIES_FILE_PATH, properties); Map metaServerAddressesFromPropertiesFile = KeyValueUtils.filterWithKeyIgnoreCaseEndsWith(properties, ".meta"); // remove key's suffix ".meta" (case insensitive) metaServerAddressesFromPropertiesFile = KeyValueUtils.removeKeySuffix(metaServerAddressesFromPropertiesFile, ".meta".length()); return transformToEnvMap(metaServerAddressesFromPropertiesFile); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/environment/Env.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.environment; import com.ctrip.framework.apollo.core.utils.StringUtils; import com.google.common.base.Preconditions; import java.util.Map; import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * This class provides functionalities to manage and hold all environments of the portal. By default * all the Env from {@link com.ctrip.framework.apollo.core.enums.Env} are included. * * @author wxq * @author Diego Krupitza(info@diegokrupitza.com) */ public class Env { private static final Logger logger = LoggerFactory.getLogger(Env.class); // use to cache Env private static final Map STRING_ENV_MAP = new ConcurrentHashMap<>(); // default environments public static final Env LOCAL = addEnvironment(com.ctrip.framework.apollo.core.enums.Env.LOCAL.name()); public static final Env DEV = addEnvironment(com.ctrip.framework.apollo.core.enums.Env.DEV.name()); public static final Env FAT = addEnvironment(com.ctrip.framework.apollo.core.enums.Env.FAT.name()); public static final Env FWS = addEnvironment(com.ctrip.framework.apollo.core.enums.Env.FWS.name()); public static final Env UAT = addEnvironment(com.ctrip.framework.apollo.core.enums.Env.UAT.name()); public static final Env LPT = addEnvironment(com.ctrip.framework.apollo.core.enums.Env.LPT.name()); public static final Env PRO = addEnvironment(com.ctrip.framework.apollo.core.enums.Env.PRO.name()); public static final Env TOOLS = addEnvironment(com.ctrip.framework.apollo.core.enums.Env.TOOLS.name()); public static final Env UNKNOWN = addEnvironment(com.ctrip.framework.apollo.core.enums.Env.UNKNOWN.name()); // name of environment, cannot be null private final String name; /** * Cannot create by other * * @param name */ private Env(String name) { this.name = name; } /** * add some change to environment name trim and to upper * * @param envName * @return */ private static String getWellFormName(String envName) { if (StringUtils.isBlank(envName)) { return ""; } String envWellFormName = envName.trim().toUpperCase(); // special case for production in case of typo if ("PROD".equals(envWellFormName)) { return Env.PRO.name; } // special case that FAT & FWS should map to FAT if ("FWS".equals(envWellFormName)) { return Env.FAT.name; } return envWellFormName; } /** * logic same as {@link com.ctrip.framework.apollo.core.enums.EnvUtils#transformEnv} * * @param envName the name we want to transform * @return the env object matching the envName */ public static Env transformEnv(String envName) { final String envWellFormName = getWellFormName(envName); if (Env.exists(envWellFormName)) { return Env.valueOf(envWellFormName); } // cannot be found or blank name return Env.UNKNOWN; } /** * a environment name exist or not * * @param name the name we want to check if it exists * @return does the env name exists or not */ public static boolean exists(String name) { name = getWellFormName(name); return STRING_ENV_MAP.containsKey(name); } /** * add an environment * * @param name the name of the environment to add * @return the newly created environment */ public static Env addEnvironment(String name) { if (StringUtils.isBlank(name)) { throw new RuntimeException("Cannot add a blank environment: " + "[" + name + "]"); } name = getWellFormName(name); if (STRING_ENV_MAP.containsKey(name)) { // has been existed logger.debug("{} already exists.", name); } else { // not existed STRING_ENV_MAP.put(name, new Env(name)); } return STRING_ENV_MAP.get(name); } /** * replace valueOf in enum But what would happened if environment not exist? * * @param name * @return * @throws IllegalArgumentException if this existed environment has no Env with the specified * name */ public static Env valueOf(String name) { name = getWellFormName(name); if (exists(name)) { return STRING_ENV_MAP.get(name); } else { throw new IllegalArgumentException(name + " not exist"); } } /** * Please use {@code Env.valueOf} instead this method * * @param env * @return */ @Deprecated public static Env fromString(String env) { Env environment = transformEnv(env); Preconditions.checkArgument(environment != UNKNOWN, String.format("Env %s is invalid", env)); return environment; } /** * conversion key from {@link String} to {@link Env} * * @param metaServerAddresses key is environment, value is environment's meta server address * @return relationship between {@link Env} and meta server address */ static Map transformToEnvMap(Map metaServerAddresses) { // add to domain Map map = new ConcurrentHashMap<>(); for (Map.Entry entry : metaServerAddresses.entrySet()) { // add new environment Env env = Env.addEnvironment(entry.getKey()); // get meta server address value String value = entry.getValue(); // put pair (Env, meta server address) map.put(env, value); } return map; } /** * Not just name in Env, the address of Env must be same, or it will throw {@code * RuntimeException} * * @param o * @return * @throws RuntimeException When same name but different address */ @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } Env env = (Env) o; if (getName().equals(env.getName())) { throw new RuntimeException(getName() + " is same environment name, but their Env not same"); } else { return false; } } @Override public int hashCode() { return Objects.hash(getName()); } /** * a Env convert to string, ie its name. * * @return */ @Override public String toString() { return name; } public String getName() { return name; } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/environment/PortalMetaDomainService.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.environment; import com.ctrip.framework.apollo.core.utils.ApolloThreadFactory; import com.ctrip.framework.apollo.core.utils.NetUtil; import com.ctrip.framework.apollo.portal.component.config.PortalConfig; import com.ctrip.framework.apollo.tracer.Tracer; import com.ctrip.framework.apollo.tracer.spi.Transaction; import com.google.common.base.Strings; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; /** * Only use in apollo-portal * Provider an available meta server url. * If there is no available meta server url for the given environment, * the default meta server url will be used(http://apollo.meta). * @see com.ctrip.framework.apollo.core.MetaDomainConsts * @author wxq */ @Service public class PortalMetaDomainService { private static final Logger logger = LoggerFactory.getLogger(PortalMetaDomainService.class); private static final long REFRESH_INTERVAL_IN_SECOND = 60;// 1 min static final String DEFAULT_META_URL = "http://apollo.meta"; private final Map metaServerAddressCache = Maps.newConcurrentMap(); /** * initialize meta server provider without cache. * Multiple {@link PortalMetaServerProvider} */ private final List portalMetaServerProviders = new ArrayList<>(); // env -> meta server address cache // comma separated meta server address -> selected single meta server address cache private final Map selectedMetaServerAddressCache = Maps.newConcurrentMap(); private final AtomicBoolean periodicRefreshStarted = new AtomicBoolean(false); PortalMetaDomainService(final PortalConfig portalConfig) { // high priority with data in database portalMetaServerProviders.add(new DatabasePortalMetaServerProvider(portalConfig)); // System properties, OS environment, configuration file portalMetaServerProviders.add(new DefaultPortalMetaServerProvider()); } /** * Return one meta server address. If multiple meta server addresses are configured, will select one. */ public String getDomain(Env env) { String metaServerAddress = getMetaServerAddress(env); // if there is more than one address, need to select one if (metaServerAddress.contains(",")) { return selectMetaServerAddress(metaServerAddress); } return metaServerAddress; } /** * Return meta server address. If multiple meta server addresses are configured, will return the comma separated string. */ public String getMetaServerAddress(Env env) { // in cache? if (!metaServerAddressCache.containsKey(env)) { // put it to cache metaServerAddressCache.put(env, getMetaServerAddressCacheValue(portalMetaServerProviders, env)); } // get from cache return metaServerAddressCache.get(env); } /** * Get the meta server from provider by given environment. * If there is no available meta server url for the given environment, * the default meta server url will be used(http://apollo.meta). * @param providers provide environment's meta server address * @param env environment * @return meta server address */ private String getMetaServerAddressCacheValue(Collection providers, Env env) { // null value String metaAddress = null; for (PortalMetaServerProvider portalMetaServerProvider : providers) { if (portalMetaServerProvider.exists(env)) { metaAddress = portalMetaServerProvider.getMetaServerAddress(env); logger.info("Located meta server address [{}] for env [{}]", metaAddress, env); break; } } // check find it or not if (Strings.isNullOrEmpty(metaAddress)) { // Fallback to default meta address metaAddress = DEFAULT_META_URL; logger.warn( "Meta server address fallback to [{}] for env [{}], because it is not available in MetaServerProvider", metaAddress, env); } return metaAddress.trim(); } /** * reload all {@link PortalMetaServerProvider}. * clear cache {@link this#metaServerAddressCache} */ public void reload() { for (PortalMetaServerProvider portalMetaServerProvider : portalMetaServerProviders) { portalMetaServerProvider.reload(); } metaServerAddressCache.clear(); } /** * Select one available meta server from the comma separated meta server addresses, e.g. * http://1.2.3.4:8080,http://2.3.4.5:8080 * *
* * In production environment, we still suggest using one single domain like http://config.xxx.com(backed by software * load balancers like nginx) instead of multiple ip addresses */ private String selectMetaServerAddress(String metaServerAddresses) { String metaAddressSelected = selectedMetaServerAddressCache.get(metaServerAddresses); if (metaAddressSelected == null) { // initialize if (periodicRefreshStarted.compareAndSet(false, true)) { schedulePeriodicRefresh(); } updateMetaServerAddresses(metaServerAddresses); metaAddressSelected = selectedMetaServerAddressCache.get(metaServerAddresses); } return metaAddressSelected; } private void updateMetaServerAddresses(String metaServerAddresses) { logger.debug("Selecting meta server address for: {}", metaServerAddresses); Transaction transaction = Tracer.newTransaction("Apollo.MetaService", "refreshMetaServerAddress"); transaction.addData("Url", metaServerAddresses); try { List metaServers = Lists.newArrayList(metaServerAddresses.split(",")); // random load balancing Collections.shuffle(metaServers); boolean serverAvailable = false; for (String address : metaServers) { address = address.trim(); // check whether /services/config is accessible if (NetUtil.pingUrl(address + "/services/config")) { // select the first available meta server selectedMetaServerAddressCache.put(metaServerAddresses, address); serverAvailable = true; logger.debug("Selected meta server address {} for {}", address, metaServerAddresses); break; } } // we need to make sure the map is not empty, e.g. the first update might be failed if (!selectedMetaServerAddressCache.containsKey(metaServerAddresses)) { selectedMetaServerAddressCache.put(metaServerAddresses, metaServers.get(0).trim()); } if (!serverAvailable) { logger.warn( "Could not find available meta server for configured meta server addresses: {}, fallback to: {}", metaServerAddresses, selectedMetaServerAddressCache.get(metaServerAddresses)); } transaction.setStatus(Transaction.SUCCESS); } catch (Throwable ex) { transaction.setStatus(ex); throw ex; } finally { transaction.complete(); } } private void schedulePeriodicRefresh() { ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1, ApolloThreadFactory.create("MetaServiceLocator", true)); scheduledExecutorService.scheduleAtFixedRate(() -> { try { for (String metaServerAddresses : selectedMetaServerAddressCache.keySet()) { updateMetaServerAddresses(metaServerAddresses); } } catch (Throwable ex) { logger.warn("Refreshing meta server address failed, will retry in {} seconds", REFRESH_INTERVAL_IN_SECOND, ex); } }, REFRESH_INTERVAL_IN_SECOND, REFRESH_INTERVAL_IN_SECOND, TimeUnit.SECONDS); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/environment/PortalMetaServerProvider.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.environment; /** * For the supporting of multiple meta server address providers. * From configuration file, * from OS environment, * From database, * ... * Just implement this interface * @author wxq */ public interface PortalMetaServerProvider { /** * @param targetEnv environment * @return meta server address matched environment */ String getMetaServerAddress(Env targetEnv); /** * @param targetEnv environment * @return environment's meta server address exists or not */ boolean exists(Env targetEnv); /** * reload the meta server address in runtime */ void reload(); } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/filter/PortalUserSessionFilter.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.filter; import java.io.IOException; import java.util.Arrays; import jakarta.servlet.Filter; import jakarta.servlet.FilterChain; import jakarta.servlet.FilterConfig; import jakarta.servlet.ServletException; import jakarta.servlet.ServletRequest; import jakarta.servlet.ServletResponse; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.core.env.Environment; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint; /** * Filter to handle Portal user session validation for OpenAPI requests. This filter runs before * ConsumerAuthenticationFilter to detect and handle: 1. Authenticated Portal users - allow them to * access OpenAPI 2. Expired Portal sessions - handle based on authentication mode: - auth/ldap: * redirect to /signin (form login page) - oidc: return 401 (let frontend handle or trigger OAuth2 * flow) - those three login methods' experiences are the same as before *

* This keeps the ConsumerAuthenticationFilter focused solely on Consumer Token validation. */ public class PortalUserSessionFilter implements Filter { private static final Logger logger = LoggerFactory.getLogger(PortalUserSessionFilter.class); private static final String SESSION_COOKIE_NAME = "SESSION"; private static final String OIDC_PROFILE = "oidc"; private static final String PORTAL_USER_AUTHENTICATED = "PORTAL_USER_AUTHENTICATED"; private static final LoginUrlAuthenticationEntryPoint LOGIN_ENTRY_POINT = new LoginUrlAuthenticationEntryPoint("/signin"); private final Environment environment; public PortalUserSessionFilter(Environment environment) { this.environment = environment; } @Override public void init(FilterConfig filterConfig) throws ServletException { // nothing } @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; // Check if the user is an authenticated Portal user if (isAuthenticatedPortalUser(request)) { // Portal user is authenticated, allow access to OpenAPI logger.debug("Authenticated portal user accessing OpenAPI: {}", request.getRequestURI()); request.setAttribute(PORTAL_USER_AUTHENTICATED, true); chain.doFilter(req, resp); return; } // Check if there's a session cookie but user is not authenticated // This indicates the session has expired if (hasSessionCookie(request)) { logger.info( "Request has SESSION cookie but user is not authenticated - session is expired. URI: {}", request.getRequestURI()); handleSessionExpired(request, response); return; } // Neither authenticated Portal user nor expired session // Continue to next filter (ConsumerAuthenticationFilter) for token validation chain.doFilter(req, resp); } @Override public void destroy() { // nothing } /** * Determines whether the current request is from an authenticated Portal user by checking Spring * Security's SecurityContext. * * @param request the HTTP request * @return true if authenticated Portal user, false otherwise */ private boolean isAuthenticatedPortalUser(HttpServletRequest request) { try { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); // Check if there is authentication information and it has been authenticated if (authentication != null && authentication.isAuthenticated()) { // Exclude anonymous users String principal = authentication.getName(); if (principal != null && !"anonymousUser".equals(principal)) { logger.debug("Authenticated portal user: {} accessing OpenAPI: {}", principal, request.getRequestURI()); return true; } } } catch (Exception e) { logger.debug("Failed to get authentication from SecurityContext", e); } return false; } /** * Checks if the request has a session cookie. This is used to detect expired sessions. * * @param request the HTTP request * @return true if SESSION cookie exists, false otherwise */ private boolean hasSessionCookie(HttpServletRequest request) { Cookie[] cookies = request.getCookies(); if (cookies != null) { for (Cookie cookie : cookies) { if (SESSION_COOKIE_NAME.equals(cookie.getName())) { return true; } } } return false; } /** * Handles expired session based on authentication mode. - auth/ldap: redirect to /signin (form * login page) - oidc: return 401 (maintains original behavior, frontend can handle) * * @param request the HTTP request * @param response the HTTP response */ private void handleSessionExpired(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { if (isOidcProfile()) { // OIDC mode: return 401 to maintain original behavior logger.debug("OIDC mode: returning 401 for expired session"); response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Session expired"); } else { // Auth/LDAP mode: reuse LoginUrlAuthenticationEntryPoint for consistent redirect handling logger.debug( "Auth/LDAP mode: delegating to LoginUrlAuthenticationEntryPoint for login redirect"); LOGIN_ENTRY_POINT.commence(request, response, null); } } /** * Checks if the current active profile is OIDC. * * @return true if OIDC profile is active, false otherwise */ private boolean isOidcProfile() { if (environment != null) { return Arrays.asList(environment.getActiveProfiles()).contains(OIDC_PROFILE); } return false; } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/filter/UserTypeResolverFilter.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.filter; import com.ctrip.framework.apollo.portal.component.UserIdentityContextHolder; import com.ctrip.framework.apollo.portal.constant.UserIdentityConstants; import java.io.IOException; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.security.authentication.AnonymousAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.filter.OncePerRequestFilter; import static com.ctrip.framework.apollo.openapi.util.ConsumerAuthUtil.CONSUMER_ID; public class UserTypeResolverFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String authType = resolve(request); UserIdentityContextHolder.setAuthType(authType); try { filterChain.doFilter(request, response); } finally { UserIdentityContextHolder.clear(); } } private String resolve(HttpServletRequest req) { if (req.getAttribute(CONSUMER_ID) != null) { return UserIdentityConstants.CONSUMER; } Authentication auth = SecurityContextHolder.getContext().getAuthentication(); if (auth != null && auth.isAuthenticated() && !(auth instanceof AnonymousAuthenticationToken)) { return UserIdentityConstants.USER; } return UserIdentityConstants.ANONYMOUS; } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/listener/AppCreationEvent.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.listener; import com.google.common.base.Preconditions; import com.ctrip.framework.apollo.common.entity.App; import org.springframework.context.ApplicationEvent; public class AppCreationEvent extends ApplicationEvent { public AppCreationEvent(Object source) { super(source); } public App getApp() { Preconditions.checkState(source != null); return (App) this.source; } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/listener/AppDeletionEvent.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.listener; import com.ctrip.framework.apollo.common.entity.App; import com.google.common.base.Preconditions; import org.springframework.context.ApplicationEvent; public class AppDeletionEvent extends ApplicationEvent { public AppDeletionEvent(Object source) { super(source); } public App getApp() { Preconditions.checkState(source != null); return (App) this.source; } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/listener/AppInfoChangedEvent.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.listener; import com.google.common.base.Preconditions; import com.ctrip.framework.apollo.common.entity.App; import org.springframework.context.ApplicationEvent; public class AppInfoChangedEvent extends ApplicationEvent { public AppInfoChangedEvent(Object source) { super(source); } public App getApp() { Preconditions.checkState(source != null); return (App) this.source; } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/listener/AppInfoChangedListener.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.listener; import com.ctrip.framework.apollo.common.dto.AppDTO; import com.ctrip.framework.apollo.common.utils.BeanUtils; import com.ctrip.framework.apollo.portal.environment.Env; import com.ctrip.framework.apollo.portal.api.AdminServiceAPI; import com.ctrip.framework.apollo.portal.component.PortalSettings; import com.ctrip.framework.apollo.tracer.Tracer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component; import java.util.List; @Component public class AppInfoChangedListener { private static final Logger logger = LoggerFactory.getLogger(AppInfoChangedListener.class); private final AdminServiceAPI.AppAPI appAPI; private final PortalSettings portalSettings; public AppInfoChangedListener(final AdminServiceAPI.AppAPI appAPI, final PortalSettings portalSettings) { this.appAPI = appAPI; this.portalSettings = portalSettings; } @EventListener public void onAppInfoChange(AppInfoChangedEvent event) { AppDTO appDTO = BeanUtils.transform(AppDTO.class, event.getApp()); String appId = appDTO.getAppId(); List envs = portalSettings.getActiveEnvs(); for (Env env : envs) { try { appAPI.updateApp(env, appDTO); } catch (Throwable e) { logger.error("Update app's info failed. Env = {}, AppId = {}", env, appId, e); Tracer.logError(String.format("Update app's info failed. Env = %s, AppId = %s", env, appId), e); } } } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/listener/AppNamespaceCreationEvent.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.listener; import com.google.common.base.Preconditions; import com.ctrip.framework.apollo.common.entity.AppNamespace; import org.springframework.context.ApplicationEvent; public class AppNamespaceCreationEvent extends ApplicationEvent { public AppNamespaceCreationEvent(Object source) { super(source); } public AppNamespace getAppNamespace() { Preconditions.checkState(source != null); return (AppNamespace) this.source; } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/listener/AppNamespaceDeletionEvent.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.listener; import com.ctrip.framework.apollo.common.entity.AppNamespace; import com.google.common.base.Preconditions; import org.springframework.context.ApplicationEvent; public class AppNamespaceDeletionEvent extends ApplicationEvent { public AppNamespaceDeletionEvent(Object source) { super(source); } public AppNamespace getAppNamespace() { Preconditions.checkState(source != null); return (AppNamespace) this.source; } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/listener/ConfigPublishEvent.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.listener; import com.ctrip.framework.apollo.portal.environment.Env; import org.springframework.context.ApplicationEvent; public class ConfigPublishEvent extends ApplicationEvent { private ConfigPublishInfo configPublishInfo; public ConfigPublishEvent(Object source) { super(source); configPublishInfo = (ConfigPublishInfo) source; } public static ConfigPublishEvent instance() { ConfigPublishInfo info = new ConfigPublishInfo(); return new ConfigPublishEvent(info); } public ConfigPublishInfo getConfigPublishInfo() { return configPublishInfo; } public ConfigPublishEvent withAppId(String appId) { configPublishInfo.setAppId(appId); return this; } public ConfigPublishEvent withCluster(String clusterName) { configPublishInfo.setClusterName(clusterName); return this; } public ConfigPublishEvent withNamespace(String namespaceName) { configPublishInfo.setNamespaceName(namespaceName); return this; } public ConfigPublishEvent withReleaseId(long releaseId) { configPublishInfo.setReleaseId(releaseId); return this; } public ConfigPublishEvent withPreviousReleaseId(long previousReleaseId) { configPublishInfo.setPreviousReleaseId(previousReleaseId); return this; } public ConfigPublishEvent setNormalPublishEvent(boolean isNormalPublishEvent) { configPublishInfo.setNormalPublishEvent(isNormalPublishEvent); return this; } public ConfigPublishEvent setGrayPublishEvent(boolean isGrayPublishEvent) { configPublishInfo.setGrayPublishEvent(isGrayPublishEvent); return this; } public ConfigPublishEvent setRollbackEvent(boolean isRollbackEvent) { configPublishInfo.setRollbackEvent(isRollbackEvent); return this; } public ConfigPublishEvent setMergeEvent(boolean isMergeEvent) { configPublishInfo.setMergeEvent(isMergeEvent); return this; } public ConfigPublishEvent setEnv(Env env) { configPublishInfo.setEnv(env); return this; } public static class ConfigPublishInfo { private String env; private String appId; private String clusterName; private String namespaceName; private long releaseId; private long previousReleaseId; private boolean isRollbackEvent; private boolean isMergeEvent; private boolean isNormalPublishEvent; private boolean isGrayPublishEvent; public Env getEnv() { return Env.valueOf(env); } public void setEnv(Env env) { this.env = env.toString(); } public String getAppId() { return appId; } public void setAppId(String appId) { this.appId = appId; } public String getClusterName() { return clusterName; } public void setClusterName(String clusterName) { this.clusterName = clusterName; } public String getNamespaceName() { return namespaceName; } public void setNamespaceName(String namespaceName) { this.namespaceName = namespaceName; } public long getReleaseId() { return releaseId; } public void setReleaseId(long releaseId) { this.releaseId = releaseId; } public long getPreviousReleaseId() { return previousReleaseId; } public void setPreviousReleaseId(long previousReleaseId) { this.previousReleaseId = previousReleaseId; } public boolean isRollbackEvent() { return isRollbackEvent; } public void setRollbackEvent(boolean rollbackEvent) { isRollbackEvent = rollbackEvent; } public boolean isMergeEvent() { return isMergeEvent; } public void setMergeEvent(boolean mergeEvent) { isMergeEvent = mergeEvent; } public boolean isNormalPublishEvent() { return isNormalPublishEvent; } public void setNormalPublishEvent(boolean normalPublishEvent) { isNormalPublishEvent = normalPublishEvent; } public boolean isGrayPublishEvent() { return isGrayPublishEvent; } public void setGrayPublishEvent(boolean grayPublishEvent) { isGrayPublishEvent = grayPublishEvent; } } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/listener/ConfigPublishListener.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.listener; import com.ctrip.framework.apollo.common.constants.ReleaseOperation; import com.ctrip.framework.apollo.portal.component.ConfigReleaseWebhookNotifier; import com.ctrip.framework.apollo.portal.environment.Env; import com.ctrip.framework.apollo.core.utils.ApolloThreadFactory; import com.ctrip.framework.apollo.portal.component.config.PortalConfig; import com.ctrip.framework.apollo.portal.component.emailbuilder.GrayPublishEmailBuilder; import com.ctrip.framework.apollo.portal.component.emailbuilder.MergeEmailBuilder; import com.ctrip.framework.apollo.portal.component.emailbuilder.NormalPublishEmailBuilder; import com.ctrip.framework.apollo.portal.component.emailbuilder.RollbackEmailBuilder; import com.ctrip.framework.apollo.portal.entity.bo.Email; import com.ctrip.framework.apollo.portal.entity.bo.ReleaseHistoryBO; import com.ctrip.framework.apollo.portal.service.ReleaseHistoryService; import com.ctrip.framework.apollo.portal.spi.EmailService; import com.ctrip.framework.apollo.portal.spi.MQService; import com.ctrip.framework.apollo.tracer.Tracer; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component; import jakarta.annotation.PostConstruct; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @Component public class ConfigPublishListener { private final ReleaseHistoryService releaseHistoryService; private final EmailService emailService; private final NormalPublishEmailBuilder normalPublishEmailBuilder; private final GrayPublishEmailBuilder grayPublishEmailBuilder; private final RollbackEmailBuilder rollbackEmailBuilder; private final MergeEmailBuilder mergeEmailBuilder; private final PortalConfig portalConfig; private final MQService mqService; private final ConfigReleaseWebhookNotifier configReleaseWebhookNotifier; private ExecutorService executorService; public ConfigPublishListener(final ReleaseHistoryService releaseHistoryService, final EmailService emailService, final NormalPublishEmailBuilder normalPublishEmailBuilder, final GrayPublishEmailBuilder grayPublishEmailBuilder, final RollbackEmailBuilder rollbackEmailBuilder, final MergeEmailBuilder mergeEmailBuilder, final PortalConfig portalConfig, final MQService mqService, final ConfigReleaseWebhookNotifier configReleaseWebhookNotifier) { this.releaseHistoryService = releaseHistoryService; this.emailService = emailService; this.normalPublishEmailBuilder = normalPublishEmailBuilder; this.grayPublishEmailBuilder = grayPublishEmailBuilder; this.rollbackEmailBuilder = rollbackEmailBuilder; this.mergeEmailBuilder = mergeEmailBuilder; this.portalConfig = portalConfig; this.mqService = mqService; this.configReleaseWebhookNotifier = configReleaseWebhookNotifier; } @PostConstruct public void init() { executorService = Executors.newSingleThreadExecutor(ApolloThreadFactory.create("ConfigPublishNotify", true)); } @EventListener public void onConfigPublish(ConfigPublishEvent event) { executorService.submit(new ConfigPublishNotifyTask(event.getConfigPublishInfo())); } private class ConfigPublishNotifyTask implements Runnable { private final ConfigPublishEvent.ConfigPublishInfo publishInfo; ConfigPublishNotifyTask(ConfigPublishEvent.ConfigPublishInfo publishInfo) { this.publishInfo = publishInfo; } @Override public void run() { ReleaseHistoryBO releaseHistory = getReleaseHistory(); if (releaseHistory == null) { Tracer.logError("Load release history failed", null); return; } this.sendPublishWebHook(releaseHistory); sendPublishEmail(releaseHistory); sendPublishMsg(releaseHistory); } private ReleaseHistoryBO getReleaseHistory() { Env env = publishInfo.getEnv(); int operation = publishInfo.isMergeEvent() ? ReleaseOperation.GRAY_RELEASE_MERGE_TO_MASTER : publishInfo.isRollbackEvent() ? ReleaseOperation.ROLLBACK : publishInfo.isNormalPublishEvent() ? ReleaseOperation.NORMAL_RELEASE : publishInfo.isGrayPublishEvent() ? ReleaseOperation.GRAY_RELEASE : -1; if (operation == -1) { return null; } if (publishInfo.isRollbackEvent()) { return releaseHistoryService.findLatestByPreviousReleaseIdAndOperation(env, publishInfo.getPreviousReleaseId(), operation); } return releaseHistoryService.findLatestByReleaseIdAndOperation(env, publishInfo.getReleaseId(), operation); } /** * webhook send * * @param releaseHistory {@link ReleaseHistoryBO} */ private void sendPublishWebHook(ReleaseHistoryBO releaseHistory) { Env env = publishInfo.getEnv(); String[] webHookUrls = portalConfig.webHookUrls(); if (!portalConfig.webHookSupportedEnvs().contains(env) || webHookUrls == null) { return; } configReleaseWebhookNotifier.notify(webHookUrls, env, releaseHistory); } private void sendPublishEmail(ReleaseHistoryBO releaseHistory) { Env env = publishInfo.getEnv(); if (!portalConfig.emailSupportedEnvs().contains(env)) { return; } int realOperation = releaseHistory.getOperation(); Email email = null; try { email = buildEmail(env, releaseHistory, realOperation); } catch (Throwable e) { Tracer.logError("build email failed.", e); } if (email != null) { emailService.send(email); } } private void sendPublishMsg(ReleaseHistoryBO releaseHistory) { mqService.sendPublishMsg(publishInfo.getEnv(), releaseHistory); } private Email buildEmail(Env env, ReleaseHistoryBO releaseHistory, int operation) { switch (operation) { case ReleaseOperation.GRAY_RELEASE: { return grayPublishEmailBuilder.build(env, releaseHistory); } case ReleaseOperation.NORMAL_RELEASE: { return normalPublishEmailBuilder.build(env, releaseHistory); } case ReleaseOperation.ROLLBACK: { return rollbackEmailBuilder.build(env, releaseHistory); } case ReleaseOperation.GRAY_RELEASE_MERGE_TO_MASTER: { return mergeEmailBuilder.build(env, releaseHistory); } default: return null; } } } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/listener/CreationListener.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.listener; import com.ctrip.framework.apollo.common.dto.AppDTO; import com.ctrip.framework.apollo.common.dto.AppNamespaceDTO; import com.ctrip.framework.apollo.common.utils.BeanUtils; import com.ctrip.framework.apollo.portal.environment.Env; import com.ctrip.framework.apollo.portal.api.AdminServiceAPI; import com.ctrip.framework.apollo.portal.component.PortalSettings; import com.ctrip.framework.apollo.tracer.Tracer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component; import java.util.List; @Component public class CreationListener { private static final Logger LOGGER = LoggerFactory.getLogger(CreationListener.class); private final PortalSettings portalSettings; private final AdminServiceAPI.AppAPI appAPI; private final AdminServiceAPI.NamespaceAPI namespaceAPI; public CreationListener(final PortalSettings portalSettings, final AdminServiceAPI.AppAPI appAPI, final AdminServiceAPI.NamespaceAPI namespaceAPI) { this.portalSettings = portalSettings; this.appAPI = appAPI; this.namespaceAPI = namespaceAPI; } @EventListener public void onAppCreationEvent(AppCreationEvent event) { AppDTO appDTO = BeanUtils.transform(AppDTO.class, event.getApp()); List envs = portalSettings.getActiveEnvs(); for (Env env : envs) { try { appAPI.createApp(env, appDTO); } catch (Throwable e) { LOGGER.error("Create app failed. appId = {}, env = {})", appDTO.getAppId(), env, e); Tracer.logError( String.format("Create app failed. appId = %s, env = %s", appDTO.getAppId(), env), e); } } } @EventListener public void onAppNamespaceCreationEvent(AppNamespaceCreationEvent event) { AppNamespaceDTO appNamespace = BeanUtils.transform(AppNamespaceDTO.class, event.getAppNamespace()); List envs = portalSettings.getActiveEnvs(); for (Env env : envs) { try { namespaceAPI.createAppNamespace(env, appNamespace); } catch (Throwable e) { LOGGER.error("Create appNamespace failed. appId = {}, env = {}", appNamespace.getAppId(), env, e); Tracer.logError(String.format("Create appNamespace failed. appId = %s, env = %s", appNamespace.getAppId(), env), e); } } } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/listener/DeletionListener.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.listener; import com.ctrip.framework.apollo.common.dto.AppDTO; import com.ctrip.framework.apollo.common.dto.AppNamespaceDTO; import com.ctrip.framework.apollo.common.utils.BeanUtils; import com.ctrip.framework.apollo.portal.environment.Env; import com.ctrip.framework.apollo.portal.api.AdminServiceAPI; import com.ctrip.framework.apollo.portal.component.PortalSettings; import com.ctrip.framework.apollo.tracer.Tracer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component; import java.util.List; @Component public class DeletionListener { private static final Logger logger = LoggerFactory.getLogger(DeletionListener.class); private final PortalSettings portalSettings; private final AdminServiceAPI.AppAPI appAPI; private final AdminServiceAPI.NamespaceAPI namespaceAPI; public DeletionListener(final PortalSettings portalSettings, final AdminServiceAPI.AppAPI appAPI, final AdminServiceAPI.NamespaceAPI namespaceAPI) { this.portalSettings = portalSettings; this.appAPI = appAPI; this.namespaceAPI = namespaceAPI; } @EventListener public void onAppDeletionEvent(AppDeletionEvent event) { AppDTO appDTO = BeanUtils.transform(AppDTO.class, event.getApp()); String appId = appDTO.getAppId(); String operator = appDTO.getDataChangeLastModifiedBy(); List envs = portalSettings.getActiveEnvs(); for (Env env : envs) { try { appAPI.deleteApp(env, appId, operator); } catch (Throwable e) { logger.error("Delete app failed. Env = {}, AppId = {}", env, appId, e); Tracer.logError(String.format("Delete app failed. Env = %s, AppId = %s", env, appId), e); } } } @EventListener public void onAppNamespaceDeletionEvent(AppNamespaceDeletionEvent event) { AppNamespaceDTO appNamespace = BeanUtils.transform(AppNamespaceDTO.class, event.getAppNamespace()); List envs = portalSettings.getActiveEnvs(); String appId = appNamespace.getAppId(); String namespaceName = appNamespace.getName(); String operator = appNamespace.getDataChangeLastModifiedBy(); for (Env env : envs) { try { namespaceAPI.deleteAppNamespace(env, appId, namespaceName, operator); } catch (Throwable e) { logger.error("Delete appNamespace failed. appId = {}, namespace = {}, env = {}", appId, namespaceName, env, e); Tracer.logError( String.format("Delete appNamespace failed. appId = %s, namespace = %s, env = %s", appId, namespaceName, env), e); } } } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/repository/AppNamespaceRepository.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.repository; import com.ctrip.framework.apollo.common.entity.AppNamespace; import java.util.List; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.repository.query.Param; public interface AppNamespaceRepository extends JpaRepository { AppNamespace findByAppIdAndName(String appId, String namespaceName); AppNamespace findByName(String namespaceName); List findByNameAndIsPublic(String namespaceName, boolean isPublic); List findByIsPublicTrue(); @Query("SELECT a.name FROM AppNamespace a WHERE a.isPublic = true AND a.isDeleted = false") List findNamesByIsPublicTrue(); List findByAppId(String appId); @Modifying @Query("UPDATE AppNamespace SET isDeleted = true, " + "deletedAt = :#{T(java.lang.System).currentTimeMillis()}, " + "dataChangeLastModifiedBy = :operator WHERE appId = :appId and isDeleted = false") int batchDeleteByAppId(@Param("appId") String appId, @Param("operator") String operator); @Modifying @Query("UPDATE AppNamespace SET isDeleted = true, " + "deletedAt = :#{T(java.lang.System).currentTimeMillis()}, " + "dataChangeLastModifiedBy = :operator WHERE appId = :appId and name = :namespaceName and isDeleted = false") int delete(@Param("appId") String appId, @Param("namespaceName") String namespaceName, @Param("operator") String operator); } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/repository/AppRepository.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.repository; import com.ctrip.framework.apollo.common.entity.App; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.repository.query.Param; import java.util.List; import java.util.Set; public interface AppRepository extends JpaRepository { App findByAppId(String appId); List findByOwnerName(String ownerName, Pageable page); List findByAppIdIn(Set appIds); List findByAppIdIn(Set appIds, Pageable pageable); Page findByAppIdContainingOrNameContaining(String appId, String name, Pageable pageable); @Modifying @Query("UPDATE App SET isDeleted = true, " + "deletedAt = :#{T(java.lang.System).currentTimeMillis()}, " + "dataChangeLastModifiedBy = :operator WHERE appId = :appId and isDeleted = false") int deleteApp(@Param("appId") String appId, @Param("operator") String operator); } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/repository/AuthorityRepository.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.repository; import com.ctrip.framework.apollo.portal.entity.po.Authority; import org.springframework.data.jpa.repository.JpaRepository; /** * @author lepdou 2022-01-20 */ public interface AuthorityRepository extends JpaRepository { } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/repository/FavoriteRepository.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.repository; import com.ctrip.framework.apollo.portal.entity.po.Favorite; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.repository.query.Param; import java.util.List; public interface FavoriteRepository extends JpaRepository { List findByUserIdOrderByPositionAscDataChangeCreatedTimeAsc(String userId, Pageable page); List findByAppIdOrderByPositionAscDataChangeCreatedTimeAsc(String appId, Pageable page); Favorite findFirstByUserIdOrderByPositionAscDataChangeCreatedTimeAsc(String userId); Favorite findByUserIdAndAppId(String userId, String appId); @Modifying @Query("UPDATE Favorite SET isDeleted = true, " + "deletedAt = :#{T(java.lang.System).currentTimeMillis()}, " + "dataChangeLastModifiedBy = :operator WHERE appId = :appId and isDeleted = false") int batchDeleteByAppId(@Param("appId") String appId, @Param("operator") String operator); } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/repository/PermissionRepository.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.repository; import com.ctrip.framework.apollo.portal.entity.po.Permission; import java.util.Collection; import java.util.List; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.repository.query.Param; /** * @author Jason Song(song_s@ctrip.com) */ public interface PermissionRepository extends JpaRepository { /** * find permission by permission type and targetId */ Permission findTopByPermissionTypeAndTargetId(String permissionType, String targetId); /** * find permissions by permission types and targetId */ List findByPermissionTypeInAndTargetId(Collection permissionTypes, String targetId); @Query("SELECT p.id from Permission p where p.targetId like ?1 or p.targetId like CONCAT(?1, '+%')") List findPermissionIdsByAppId(String appId); @Query("SELECT p.id from Permission p " + "where (" + "p.targetId like CONCAT(?1, '+', ?2) OR p.targetId like CONCAT(?1, '+', ?2, '+%')" + ") AND ( " + "p.permissionType = 'ModifyNamespace' OR p.permissionType = 'ReleaseNamespace'" + ")") List findPermissionIdsByAppIdAndNamespace(String appId, String namespaceName); @Modifying @Query("UPDATE Permission SET isDeleted = true, " + "deletedAt = :#{T(java.lang.System).currentTimeMillis()}, " + "dataChangeLastModifiedBy = :operator WHERE id in :permissionIds and isDeleted = false") Integer batchDelete(@Param("permissionIds") List permissionIds, @Param("operator") String operator); @Query("SELECT p.id from Permission p where p.targetId = CONCAT(?1, '+', ?2, '+', ?3)" + " AND ( p.permissionType = 'ModifyNamespacesInCluster' OR p.permissionType = 'ReleaseNamespacesInCluster')") List findPermissionIdsByAppIdAndEnvAndCluster(String appId, String env, String clusterName); @Query("SELECT DISTINCT p " + "FROM UserRole ur " + "JOIN RolePermission rp ON ur.roleId = rp.roleId " + "JOIN Permission p ON rp.permissionId = p.id " + "WHERE ur.userId = :userId " + "AND ur.isDeleted = false " + "AND rp.isDeleted = false " + "AND p.isDeleted = false") List findUserPermissions(@Param("userId") String userId); @Query("SELECT DISTINCT p " + "FROM ConsumerRole cr " + "JOIN RolePermission rp ON cr.roleId = rp.roleId " + "JOIN Permission p ON rp.permissionId = p.id " + "WHERE cr.consumerId = :consumerId " + "AND cr.isDeleted = false " + "AND rp.isDeleted = false " + "AND p.isDeleted = false") List findConsumerPermissions(@Param("consumerId") long consumerId); } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/repository/RolePermissionRepository.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.repository; import com.ctrip.framework.apollo.portal.entity.po.RolePermission; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.repository.query.Param; import java.util.Collection; import java.util.List; /** * @author Jason Song(song_s@ctrip.com) */ public interface RolePermissionRepository extends JpaRepository { /** * find role permissions by role ids */ List findByRoleIdIn(Collection roleId); @Modifying @Query("UPDATE RolePermission SET isDeleted = true, " + "deletedAt = :#{T(java.lang.System).currentTimeMillis()}, " + "dataChangeLastModifiedBy = :operator WHERE permissionId in :permissionIds and isDeleted = false") Integer batchDeleteByPermissionIds(@Param("permissionIds") List permissionIds, @Param("operator") String operator); } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/repository/RoleRepository.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.repository; import com.ctrip.framework.apollo.portal.entity.po.Role; import java.util.List; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.repository.query.Param; /** * @author Jason Song(song_s@ctrip.com) */ public interface RoleRepository extends JpaRepository { /** * find role by role name */ Role findTopByRoleName(String roleName); @Query("SELECT r.id from Role r where r.roleName like CONCAT('Master+', ?1) " + "OR r.roleName like CONCAT('ModifyNamespace+', ?1, '+%') " + "OR r.roleName like CONCAT('ReleaseNamespace+', ?1, '+%') " + "OR r.roleName like CONCAT('ManageAppMaster+', ?1) " + "OR r.roleName like CONCAT('ModifyNamespacesInCluster+', ?1, '+%')" + "OR r.roleName like CONCAT('ReleaseNamespacesInCluster+', ?1, '+%')") List findRoleIdsByAppId(String appId); @Query("SELECT r.id from Role r where r.roleName like CONCAT('ModifyNamespace+', ?1, '+', ?2) " + "OR r.roleName like CONCAT('ModifyNamespace+', ?1, '+', ?2, '+%') " + "OR r.roleName like CONCAT('ReleaseNamespace+', ?1, '+', ?2) " + "OR r.roleName like CONCAT('ReleaseNamespace+', ?1, '+', ?2, '+%')") List findRoleIdsByAppIdAndNamespace(String appId, String namespaceName); @Modifying @Query("UPDATE Role SET isDeleted = true, " + "deletedAt = :#{T(java.lang.System).currentTimeMillis()}, " + "dataChangeLastModifiedBy = :operator WHERE id in :roleIds and isDeleted = false") Integer batchDelete(@Param("roleIds") List roleIds, @Param("operator") String operator); @Query("SELECT r.id from Role r where r.roleName = CONCAT('ModifyNamespacesInCluster+', ?1, '+', ?2, '+', ?3) " + "OR r.roleName = CONCAT('ReleaseNamespacesInCluster+', ?1, '+', ?2, '+', ?3)") List findRoleIdsByCluster(String appId, String env, String clusterName); } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/repository/ServerConfigRepository.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.repository; import com.ctrip.framework.apollo.portal.entity.po.ServerConfig; import org.springframework.data.jpa.repository.JpaRepository; public interface ServerConfigRepository extends JpaRepository { ServerConfig findByKey(String key); } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/repository/UserRepository.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.repository; import com.ctrip.framework.apollo.portal.entity.po.UserPO; import org.springframework.data.jpa.repository.JpaRepository; import java.util.List; /** * @author lepdou 2017-04-08 */ public interface UserRepository extends JpaRepository { List findFirst20ByEnabled(int enabled); List findByUsernameLikeAndEnabled(String username, int enabled); List findByUsernameLike(String username); List findByUserDisplayNameLikeAndEnabled(String userDisplayName, int enabled); List findByUserDisplayNameLike(String userDisplayName); UserPO findByUsername(String username); List findByUsernameIn(List userNames); } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/repository/UserRoleRepository.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.repository; import com.ctrip.framework.apollo.portal.entity.po.UserRole; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.repository.query.Param; import java.util.Collection; import java.util.List; /** * @author Jason Song(song_s@ctrip.com) */ public interface UserRoleRepository extends JpaRepository { /** * find user roles by userId */ List findByUserId(String userId); /** * find user roles by roleId */ List findByRoleId(long roleId); /** * find user roles by userIds and roleId */ List findByUserIdInAndRoleId(Collection userId, long roleId); @Modifying @Query("UPDATE UserRole SET isDeleted = true, " + "deletedAt = :#{T(java.lang.System).currentTimeMillis()}, " + "dataChangeLastModifiedBy = :operator WHERE roleId in :roleIds and isDeleted = false") Integer batchDeleteByRoleIds(@Param("roleIds") List roleIds, @Param("operator") String operator); } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/AccessKeyService.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.service; import com.ctrip.framework.apollo.common.dto.AccessKeyDTO; import com.ctrip.framework.apollo.portal.api.AdminServiceAPI; import com.ctrip.framework.apollo.portal.api.AdminServiceAPI.AccessKeyAPI; import com.ctrip.framework.apollo.portal.constant.TracerEventType; import com.ctrip.framework.apollo.portal.environment.Env; import com.ctrip.framework.apollo.tracer.Tracer; import org.springframework.stereotype.Service; import java.util.List; @Service public class AccessKeyService { private final AdminServiceAPI.AccessKeyAPI accessKeyAPI; public AccessKeyService(AccessKeyAPI accessKeyAPI) { this.accessKeyAPI = accessKeyAPI; } public List findByAppId(Env env, String appId) { return accessKeyAPI.findByAppId(env, appId); } public AccessKeyDTO createAccessKey(Env env, AccessKeyDTO accessKey) { AccessKeyDTO accessKeyDTO = accessKeyAPI.create(env, accessKey); Tracer.logEvent(TracerEventType.CREATE_ACCESS_KEY, accessKey.getAppId()); return accessKeyDTO; } public void deleteAccessKey(Env env, String appId, long id, String operator) { accessKeyAPI.delete(env, appId, id, operator); } public void enable(Env env, String appId, long id, int mode, String operator) { accessKeyAPI.enable(env, appId, id, mode, operator); } public void disable(Env env, String appId, long id, String operator) { accessKeyAPI.disable(env, appId, id, operator); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/AdditionalUserInfoEnrichService.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.service; import com.ctrip.framework.apollo.portal.enricher.adapter.UserInfoEnrichedAdapter; import java.util.List; import java.util.function.Function; /** * @author vdisk */ public interface AdditionalUserInfoEnrichService { /** * enrich the additional user info for the object list * * @param list object with user id * @param mapper map the object in the list to {@link UserInfoEnrichedAdapter} */ void enrichAdditionalUserInfo(List list, Function mapper); } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/AdditionalUserInfoEnrichServiceImpl.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.service; import com.ctrip.framework.apollo.portal.enricher.AdditionalUserInfoEnricher; import com.ctrip.framework.apollo.portal.enricher.adapter.UserInfoEnrichedAdapter; import com.ctrip.framework.apollo.portal.entity.bo.UserInfo; import com.ctrip.framework.apollo.portal.spi.UserService; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; /** * @author vdisk */ @Service public class AdditionalUserInfoEnrichServiceImpl implements AdditionalUserInfoEnrichService { private final UserService userService; private final List enricherList; public AdditionalUserInfoEnrichServiceImpl(UserService userService, List enricherList) { this.userService = userService; this.enricherList = enricherList; } @Override public void enrichAdditionalUserInfo(List list, Function mapper) { if (CollectionUtils.isEmpty(list)) { return; } if (CollectionUtils.isEmpty(this.enricherList)) { return; } List adapterList = this.adapt(list, mapper); if (CollectionUtils.isEmpty(adapterList)) { return; } Set userIdSet = this.extractOperatorId(adapterList); if (CollectionUtils.isEmpty(userIdSet)) { return; } List userInfoList = this.userService.findByUserIds(new ArrayList<>(userIdSet)); if (CollectionUtils.isEmpty(userInfoList)) { return; } Map userInfoMap = userInfoList.stream().collect(Collectors.toMap(UserInfo::getUserId, Function.identity())); for (UserInfoEnrichedAdapter adapter : adapterList) { for (AdditionalUserInfoEnricher enricher : this.enricherList) { enricher.enrichAdditionalUserInfo(adapter, userInfoMap); } } } private List adapt(List dtoList, Function mapper) { List adapterList = new ArrayList<>(dtoList.size()); for (T dto : dtoList) { if (dto == null) { continue; } UserInfoEnrichedAdapter enrichedAdapter = mapper.apply(dto); adapterList.add(enrichedAdapter); } return adapterList; } private Set extractOperatorId(List adapterList) { Set operatorIdSet = new HashSet<>(); for (UserInfoEnrichedAdapter adapter : adapterList) { if (StringUtils.hasText(adapter.getFirstUserId())) { operatorIdSet.add(adapter.getFirstUserId()); } if (StringUtils.hasText(adapter.getSecondUserId())) { operatorIdSet.add(adapter.getSecondUserId()); } if (StringUtils.hasText(adapter.getThirdUserId())) { operatorIdSet.add(adapter.getThirdUserId()); } } return operatorIdSet; } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/AppNamespaceService.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.service; import com.ctrip.framework.apollo.audit.annotation.ApolloAuditLog; import com.ctrip.framework.apollo.audit.annotation.ApolloAuditLogDataInfluence; import com.ctrip.framework.apollo.audit.annotation.ApolloAuditLogDataInfluenceTable; import com.ctrip.framework.apollo.audit.annotation.ApolloAuditLogDataInfluenceTableField; import com.ctrip.framework.apollo.audit.annotation.OpType; import com.ctrip.framework.apollo.common.entity.App; import com.ctrip.framework.apollo.common.entity.AppNamespace; import com.ctrip.framework.apollo.common.exception.BadRequestException; import com.ctrip.framework.apollo.core.ConfigConsts; import com.ctrip.framework.apollo.core.enums.ConfigFileFormat; import com.ctrip.framework.apollo.core.utils.StringUtils; import com.ctrip.framework.apollo.portal.repository.AppNamespaceRepository; import com.ctrip.framework.apollo.portal.spi.UserInfoHolder; import com.google.common.base.Joiner; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import java.util.List; import java.util.Objects; import java.util.Set; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.CollectionUtils; @Service public class AppNamespaceService { private static final int PRIVATE_APP_NAMESPACE_NOTIFICATION_COUNT = 5; private static final Joiner APP_NAMESPACE_JOINER = Joiner.on(",").skipNulls(); private final UserInfoHolder userInfoHolder; private final AppNamespaceRepository appNamespaceRepository; private final RoleInitializationService roleInitializationService; private final AppService appService; private final RolePermissionService rolePermissionService; public AppNamespaceService(final UserInfoHolder userInfoHolder, final AppNamespaceRepository appNamespaceRepository, final RoleInitializationService roleInitializationService, final @Lazy AppService appService, final RolePermissionService rolePermissionService) { this.userInfoHolder = userInfoHolder; this.appNamespaceRepository = appNamespaceRepository; this.roleInitializationService = roleInitializationService; this.appService = appService; this.rolePermissionService = rolePermissionService; } /** * 公共的app ns,能被其它项目关联到的app ns */ public List findPublicAppNamespaces() { return appNamespaceRepository.findByIsPublicTrue(); } public List findPublicAppNamespaceNames() { return appNamespaceRepository.findNamesByIsPublicTrue(); } public AppNamespace findPublicAppNamespace(String namespaceName) { List appNamespaces = appNamespaceRepository.findByNameAndIsPublic(namespaceName, true); if (CollectionUtils.isEmpty(appNamespaces)) { return null; } return appNamespaces.get(0); } private List findAllPrivateAppNamespaces(String namespaceName) { return appNamespaceRepository.findByNameAndIsPublic(namespaceName, false); } public AppNamespace findByAppIdAndName(String appId, String namespaceName) { return appNamespaceRepository.findByAppIdAndName(appId, namespaceName); } public List findByAppId(String appId) { return appNamespaceRepository.findByAppId(appId); } public List findAll() { Iterable appNamespaces = appNamespaceRepository.findAll(); return Lists.newArrayList(appNamespaces); } @ApolloAuditLog(type = OpType.CREATE, name = "AppNamespace.create", description = "createDefaultAppNamespace") @Transactional public void createDefaultAppNamespace(String appId) { if (!isAppNamespaceNameUnique(appId, ConfigConsts.NAMESPACE_APPLICATION)) { throw new BadRequestException("App already has application namespace. AppId = %s", appId); } AppNamespace appNs = new AppNamespace(); appNs.setAppId(appId); appNs.setName(ConfigConsts.NAMESPACE_APPLICATION); appNs.setComment("default app namespace"); appNs.setFormat(ConfigFileFormat.Properties.getValue()); String userId = userInfoHolder.getUser().getUserId(); appNs.setDataChangeCreatedBy(userId); appNs.setDataChangeLastModifiedBy(userId); appNamespaceRepository.save(appNs); } public boolean isAppNamespaceNameUnique(String appId, String namespaceName) { Objects.requireNonNull(appId, "AppId must not be null"); Objects.requireNonNull(namespaceName, "Namespace must not be null"); return Objects.isNull(appNamespaceRepository.findByAppIdAndName(appId, namespaceName)); } @Transactional public AppNamespace createAppNamespaceInLocal(AppNamespace appNamespace) { return createAppNamespaceInLocal(appNamespace, true); } @Transactional @ApolloAuditLog(type = OpType.CREATE, name = "AppNamespace.create", description = "createAppNamespaceInLocal") public AppNamespace createAppNamespaceInLocal(AppNamespace appNamespace, boolean appendNamespacePrefix) { String appId = appNamespace.getAppId(); // add app org id as prefix App app = appService.load(appId); if (app == null) { throw BadRequestException.appNotExists(appId); } StringBuilder appNamespaceName = new StringBuilder(); // add prefix postfix appNamespaceName .append(appNamespace.isPublic() && appendNamespacePrefix ? app.getOrgId() + "." : "") .append(appNamespace.getName()) .append(appNamespace.formatAsEnum() == ConfigFileFormat.Properties ? "" : "." + appNamespace.getFormat()); appNamespace.setName(appNamespaceName.toString()); if (appNamespace.getComment() == null) { appNamespace.setComment(""); } if (!ConfigFileFormat.isValidFormat(appNamespace.getFormat())) { throw BadRequestException .invalidNamespaceFormat("format must be properties、json、yaml、yml、xml"); } String operator = appNamespace.getDataChangeCreatedBy(); if (StringUtils.isEmpty(operator)) { operator = userInfoHolder.getUser().getUserId(); appNamespace.setDataChangeCreatedBy(operator); } appNamespace.setDataChangeLastModifiedBy(operator); // globally uniqueness check for public app namespace if (appNamespace.isPublic()) { checkAppNamespaceGlobalUniqueness(appNamespace); } else { // check private app namespace if (appNamespaceRepository.findByAppIdAndName(appNamespace.getAppId(), appNamespace.getName()) != null) { throw new BadRequestException( "Private AppNamespace " + appNamespace.getName() + " already exists!"); } // should not have the same with public app namespace checkPublicAppNamespaceGlobalUniqueness(appNamespace); } AppNamespace createdAppNamespace = appNamespaceRepository.save(appNamespace); roleInitializationService.initNamespaceRoles(appNamespace.getAppId(), appNamespace.getName(), operator); roleInitializationService.initNamespaceEnvRoles(appNamespace.getAppId(), appNamespace.getName(), operator); return createdAppNamespace; } @Transactional public AppNamespace importAppNamespaceInLocal(AppNamespace appNamespace) { // globally uniqueness check for public app namespace if (appNamespace.isPublic()) { checkAppNamespaceGlobalUniqueness(appNamespace); } else { // check private app namespace if (appNamespaceRepository.findByAppIdAndName(appNamespace.getAppId(), appNamespace.getName()) != null) { throw new BadRequestException( "Private AppNamespace " + appNamespace.getName() + " already exists!"); } // should not have the same with public app namespace checkPublicAppNamespaceGlobalUniqueness(appNamespace); } AppNamespace createdAppNamespace = appNamespaceRepository.save(appNamespace); String operator = appNamespace.getDataChangeCreatedBy(); roleInitializationService.initNamespaceRoles(appNamespace.getAppId(), appNamespace.getName(), operator); roleInitializationService.initNamespaceEnvRoles(appNamespace.getAppId(), appNamespace.getName(), operator); return createdAppNamespace; } private void checkAppNamespaceGlobalUniqueness(AppNamespace appNamespace) { checkPublicAppNamespaceGlobalUniqueness(appNamespace); List privateAppNamespaces = findAllPrivateAppNamespaces(appNamespace.getName()); if (!CollectionUtils.isEmpty(privateAppNamespaces)) { Set appIds = Sets.newHashSet(); for (AppNamespace ans : privateAppNamespaces) { appIds.add(ans.getAppId()); if (appIds.size() == PRIVATE_APP_NAMESPACE_NOTIFICATION_COUNT) { break; } } throw new BadRequestException("Public AppNamespace " + appNamespace.getName() + " already exists as private AppNamespace in appId: " + APP_NAMESPACE_JOINER.join(appIds) + ", etc. Please select another name!"); } } private void checkPublicAppNamespaceGlobalUniqueness(AppNamespace appNamespace) { AppNamespace publicAppNamespace = findPublicAppNamespace(appNamespace.getName()); if (publicAppNamespace != null) { throw new BadRequestException("AppNamespace " + appNamespace.getName() + " already exists as public namespace in appId: " + publicAppNamespace.getAppId() + "!"); } } @ApolloAuditLog(type = OpType.DELETE, name = "AppNamespace.delete", description = "deleteAppNamespace") @Transactional public AppNamespace deleteAppNamespace(String appId, String namespaceName) { AppNamespace appNamespace = appNamespaceRepository.findByAppIdAndName(appId, namespaceName); if (appNamespace == null) { throw BadRequestException.appNamespaceNotExists(appId, namespaceName); } String operator = userInfoHolder.getUser().getUserId(); // this operator is passed to // com.ctrip.framework.apollo.portal.listener.DeletionListener.onAppNamespaceDeletionEvent appNamespace.setDataChangeLastModifiedBy(operator); // delete app namespace in portal db appNamespaceRepository.delete(appId, namespaceName, operator); // delete Permission and Role related data rolePermissionService.deleteRolePermissionsByAppIdAndNamespace(appId, namespaceName, operator); return appNamespace; } @ApolloAuditLog(type = OpType.DELETE, name = "AppNamespace.batchDeleteByAppId", description = "batchDeleteByAppId") public void batchDeleteByAppId( @ApolloAuditLogDataInfluence @ApolloAuditLogDataInfluenceTable(tableName = "AppNamespace") @ApolloAuditLogDataInfluenceTableField(fieldName = "AppId") String appId, String operator) { appNamespaceRepository.batchDeleteByAppId(appId, operator); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/AppService.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.service; import com.ctrip.framework.apollo.audit.annotation.ApolloAuditLog; import com.ctrip.framework.apollo.audit.annotation.OpType; import com.ctrip.framework.apollo.audit.api.ApolloAuditLogApi; import com.ctrip.framework.apollo.common.dto.AppDTO; import com.ctrip.framework.apollo.common.dto.PageDTO; import com.ctrip.framework.apollo.common.entity.App; import com.ctrip.framework.apollo.common.exception.BadRequestException; import com.ctrip.framework.apollo.common.utils.BeanUtils; import com.ctrip.framework.apollo.core.ConfigConsts; import com.ctrip.framework.apollo.core.utils.StringUtils; import com.ctrip.framework.apollo.portal.api.AdminServiceAPI.AppAPI; import com.ctrip.framework.apollo.portal.component.PortalSettings; import com.ctrip.framework.apollo.portal.environment.Env; import com.ctrip.framework.apollo.portal.api.AdminServiceAPI; import com.ctrip.framework.apollo.portal.constant.TracerEventType; import com.ctrip.framework.apollo.portal.entity.bo.UserInfo; import com.ctrip.framework.apollo.portal.entity.vo.EnvClusterInfo; import com.ctrip.framework.apollo.portal.listener.AppCreationEvent; import com.ctrip.framework.apollo.portal.repository.AppRepository; import com.ctrip.framework.apollo.portal.spi.UserInfoHolder; import com.ctrip.framework.apollo.portal.spi.UserService; import com.ctrip.framework.apollo.portal.util.RoleUtils; import com.ctrip.framework.apollo.tracer.Tracer; import com.google.common.collect.Lists; import java.util.Collections; import org.springframework.context.ApplicationEventPublisher; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; import java.util.Set; import org.springframework.util.CollectionUtils; @Service public class AppService { private final UserInfoHolder userInfoHolder; private final AdminServiceAPI.AppAPI appAPI; private final AppRepository appRepository; private final ClusterService clusterService; private final AppNamespaceService appNamespaceService; private final RoleInitializationService roleInitializationService; private final RolePermissionService rolePermissionService; private final FavoriteService favoriteService; private final UserService userService; private final ApolloAuditLogApi apolloAuditLogApi; private final PortalSettings portalSettings; private final ApplicationEventPublisher publisher; public AppService(final UserInfoHolder userInfoHolder, final AppAPI appAPI, final AppRepository appRepository, final ClusterService clusterService, final AppNamespaceService appNamespaceService, final RoleInitializationService roleInitializationService, final RolePermissionService rolePermissionService, final FavoriteService favoriteService, final UserService userService, ApplicationEventPublisher publisher, final ApolloAuditLogApi apolloAuditLogApi, PortalSettings portalSettings) { this.userInfoHolder = userInfoHolder; this.appAPI = appAPI; this.appRepository = appRepository; this.clusterService = clusterService; this.appNamespaceService = appNamespaceService; this.roleInitializationService = roleInitializationService; this.rolePermissionService = rolePermissionService; this.favoriteService = favoriteService; this.userService = userService; this.apolloAuditLogApi = apolloAuditLogApi; this.publisher = publisher; this.portalSettings = portalSettings; } public List findAll() { Iterable apps = appRepository.findAll(); return Lists.newArrayList(apps); } public PageDTO findAll(Pageable pageable) { Page apps = appRepository.findAll(pageable); return new PageDTO<>(apps.getContent(), pageable, apps.getTotalElements()); } public PageDTO searchByAppIdOrAppName(String query, Pageable pageable) { Page apps = appRepository.findByAppIdContainingOrNameContaining(query, query, pageable); return new PageDTO<>(apps.getContent(), pageable, apps.getTotalElements()); } public List findByAppIds(Set appIds) { return appRepository.findByAppIdIn(appIds); } public List findByAppIds(Set appIds, Pageable pageable) { return appRepository.findByAppIdIn(appIds, pageable); } public List findByOwnerName(String ownerName, Pageable page) { return appRepository.findByOwnerName(ownerName, page); } public App load(String appId) { return appRepository.findByAppId(appId); } public AppDTO load(Env env, String appId) { return appAPI.loadApp(env, appId); } public void createAppInRemote(Env env, App app) { if (StringUtils.isBlank(app.getDataChangeCreatedBy())) { String username = userInfoHolder.getUser().getUserId(); app.setDataChangeCreatedBy(username); app.setDataChangeLastModifiedBy(username); } AppDTO appDTO = BeanUtils.transform(AppDTO.class, app); appAPI.createApp(env, appDTO); roleInitializationService.initClusterNamespaceRoles(app.getAppId(), env.getName(), ConfigConsts.CLUSTER_NAME_DEFAULT, userInfoHolder.getUser().getUserId()); } private App createAppInLocal(App app) { String appId = app.getAppId(); App managedApp = appRepository.findByAppId(appId); if (managedApp != null) { throw BadRequestException.appAlreadyExists(appId); } UserInfo owner = userService.findByUserId(app.getOwnerName()); if (owner == null) { throw new BadRequestException("Application's owner not exist."); } app.setOwnerEmail(owner.getEmail()); String operator = userInfoHolder.getUser().getUserId(); app.setDataChangeCreatedBy(operator); app.setDataChangeLastModifiedBy(operator); App createdApp = appRepository.save(app); appNamespaceService.createDefaultAppNamespace(appId); roleInitializationService.initAppRoles(createdApp); List envs = portalSettings.getActiveEnvs(); for (Env env : envs) { roleInitializationService.initClusterNamespaceRoles(appId, env.getName(), ConfigConsts.CLUSTER_NAME_DEFAULT, userInfoHolder.getUser().getUserId()); } Tracer.logEvent(TracerEventType.CREATE_APP, appId); return createdApp; } @Transactional @ApolloAuditLog(type = OpType.CREATE, name = "App.create") public App createAppAndAddRolePermission(App app, Set admins) { App createdApp = this.createAppInLocal(app); publisher.publishEvent(new AppCreationEvent(createdApp)); if (!CollectionUtils.isEmpty(admins)) { rolePermissionService.assignRoleToUsers( RoleUtils.buildAppMasterRoleName(createdApp.getAppId()), admins, userInfoHolder.getUser().getUserId()); } return createdApp; } @Transactional public App importAppInLocal(App app) { String appId = app.getAppId(); App managedApp = appRepository.findByAppId(appId); if (managedApp != null) { return app; } app.setId(0); App createdApp = appRepository.save(app); roleInitializationService.initAppRoles(createdApp); Tracer.logEvent(TracerEventType.CREATE_APP, appId); return createdApp; } @Transactional @ApolloAuditLog(type = OpType.UPDATE, name = "App.update") public App updateAppInLocal(App app) { String appId = app.getAppId(); App managedApp = appRepository.findByAppId(appId); if (managedApp == null) { throw BadRequestException.appNotExists(appId); } managedApp.setName(app.getName()); managedApp.setOrgId(app.getOrgId()); managedApp.setOrgName(app.getOrgName()); String ownerName = app.getOwnerName(); UserInfo owner = userService.findByUserId(ownerName); if (owner == null) { throw new BadRequestException("App's owner not exists. owner = %s", ownerName); } managedApp.setOwnerName(owner.getUserId()); managedApp.setOwnerEmail(owner.getEmail()); String operator = userInfoHolder.getUser().getUserId(); managedApp.setDataChangeLastModifiedBy(operator); return appRepository.save(managedApp); } public EnvClusterInfo createEnvNavNode(Env env, String appId) { EnvClusterInfo node = new EnvClusterInfo(env); node.setClusters(clusterService.findClusters(env, appId)); return node; } @Transactional @ApolloAuditLog(type = OpType.DELETE, name = "App.delete") public App deleteAppInLocal(String appId) { App managedApp = appRepository.findByAppId(appId); if (managedApp == null) { throw BadRequestException.appNotExists(appId); } String operator = userInfoHolder.getUser().getUserId(); // this operator is passed to // com.ctrip.framework.apollo.portal.listener.DeletionListener.onAppDeletionEvent managedApp.setDataChangeLastModifiedBy(operator); // 删除portal数据库中的app appRepository.deleteApp(appId, operator); // append a deleted data influence should be bounded apolloAuditLogApi.appendDataInfluences(Collections.singletonList(managedApp), App.class); // 删除portal数据库中的appNamespace appNamespaceService.batchDeleteByAppId(appId, operator); // 删除portal数据库中的收藏表 favoriteService.batchDeleteByAppId(appId, operator); // 删除portal数据库中Permission、Role相关数据 rolePermissionService.deleteRolePermissionsByAppId(appId, operator); return managedApp; } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/ClusterService.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.service; import com.ctrip.framework.apollo.common.dto.ClusterDTO; import com.ctrip.framework.apollo.common.exception.BadRequestException; import com.ctrip.framework.apollo.portal.environment.Env; import com.ctrip.framework.apollo.portal.api.AdminServiceAPI; import com.ctrip.framework.apollo.portal.constant.TracerEventType; import com.ctrip.framework.apollo.portal.spi.UserInfoHolder; import com.ctrip.framework.apollo.tracer.Tracer; import org.springframework.stereotype.Service; import java.util.List; @Service public class ClusterService { private final UserInfoHolder userInfoHolder; private final AdminServiceAPI.ClusterAPI clusterAPI; private final RoleInitializationService roleInitializationService; private final RolePermissionService rolePermissionService; public ClusterService(final UserInfoHolder userInfoHolder, final AdminServiceAPI.ClusterAPI clusterAPI, RoleInitializationService roleInitializationService, RolePermissionService rolePermissionService) { this.userInfoHolder = userInfoHolder; this.clusterAPI = clusterAPI; this.roleInitializationService = roleInitializationService; this.rolePermissionService = rolePermissionService; } public List findClusters(Env env, String appId) { return clusterAPI.findClustersByApp(appId, env); } public ClusterDTO createCluster(Env env, ClusterDTO cluster) { if (!clusterAPI.isClusterUnique(cluster.getAppId(), env, cluster.getName())) { throw BadRequestException.clusterAlreadyExists(cluster.getName()); } ClusterDTO clusterDTO = clusterAPI.create(env, cluster); roleInitializationService.initClusterNamespaceRoles(cluster.getAppId(), env.getName(), cluster.getName(), userInfoHolder.getUser().getUserId()); Tracer.logEvent(TracerEventType.CREATE_CLUSTER, cluster.getAppId(), "0", cluster.getName()); return clusterDTO; } public void deleteCluster(Env env, String appId, String clusterName) { clusterAPI.delete(env, appId, clusterName, userInfoHolder.getUser().getUserId()); rolePermissionService.deleteRolePermissionsByCluster(appId, env.getName(), clusterName, userInfoHolder.getUser().getUserId()); } public ClusterDTO loadCluster(String appId, Env env, String clusterName) { return clusterAPI.loadCluster(appId, env, clusterName); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/CommitService.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.service; import com.ctrip.framework.apollo.common.dto.CommitDTO; import com.ctrip.framework.apollo.portal.enricher.adapter.BaseDtoUserInfoEnrichedAdapter; import com.ctrip.framework.apollo.portal.environment.Env; import com.ctrip.framework.apollo.portal.api.AdminServiceAPI; import org.springframework.stereotype.Service; import java.util.List; @Service public class CommitService { private final AdminServiceAPI.CommitAPI commitAPI; private final AdditionalUserInfoEnrichService additionalUserInfoEnrichService; public CommitService(final AdminServiceAPI.CommitAPI commitAPI, AdditionalUserInfoEnrichService additionalUserInfoEnrichService) { this.commitAPI = commitAPI; this.additionalUserInfoEnrichService = additionalUserInfoEnrichService; } public List find(String appId, Env env, String clusterName, String namespaceName, int page, int size) { List dtoList = commitAPI.find(appId, env, clusterName, namespaceName, page, size); this.additionalUserInfoEnrichService.enrichAdditionalUserInfo(dtoList, BaseDtoUserInfoEnrichedAdapter::new); return dtoList; } public List findByKey(String appId, Env env, String clusterName, String namespaceName, String key, int page, int size) { List dtoList = commitAPI.findByKey(appId, env, clusterName, namespaceName, key, page, size); this.additionalUserInfoEnrichService.enrichAdditionalUserInfo(dtoList, BaseDtoUserInfoEnrichedAdapter::new); return dtoList; } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/ConfigsExportService.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.service; import com.ctrip.framework.apollo.portal.component.UnifiedPermissionValidator; import com.google.gson.Gson; import com.ctrip.framework.apollo.common.dto.ClusterDTO; import com.ctrip.framework.apollo.common.entity.App; import com.ctrip.framework.apollo.common.entity.AppNamespace; import com.ctrip.framework.apollo.common.exception.BadRequestException; import com.ctrip.framework.apollo.common.exception.ServiceException; import com.ctrip.framework.apollo.core.enums.ConfigFileFormat; import com.ctrip.framework.apollo.portal.component.PortalSettings; import com.ctrip.framework.apollo.portal.entity.bo.ConfigBO; import com.ctrip.framework.apollo.portal.entity.bo.NamespaceBO; import com.ctrip.framework.apollo.portal.environment.Env; import com.ctrip.framework.apollo.portal.util.ConfigFileUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; import java.io.IOException; import java.io.OutputStream; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.function.Consumer; import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; @Service public class ConfigsExportService { private static final Logger logger = LoggerFactory.getLogger(ConfigsExportService.class); private final Gson gson = new Gson(); private final AppService appService; private final ClusterService clusterService; private final NamespaceService namespaceService; private final AppNamespaceService appNamespaceService; private final PortalSettings portalSettings; private final UnifiedPermissionValidator unifiedPermissionValidator; public ConfigsExportService(AppService appService, ClusterService clusterService, final @Lazy NamespaceService namespaceService, final AppNamespaceService appNamespaceService, PortalSettings portalSettings, UnifiedPermissionValidator unifiedPermissionValidator) { this.appService = appService; this.clusterService = clusterService; this.namespaceService = namespaceService; this.appNamespaceService = appNamespaceService; this.portalSettings = portalSettings; this.unifiedPermissionValidator = unifiedPermissionValidator; } /** * Export all application which current user own them. *

* File Struts: *

* * List * List -> List -> List -> List * -----------------> app.metadata * -------------------------------------------> List * * @param outputStream network file download stream to user */ public void exportData(OutputStream outputStream, List exportEnvs) { if (CollectionUtils.isEmpty(exportEnvs)) { exportEnvs = portalSettings.getActiveEnvs(); } exportApps(exportEnvs, outputStream); } /** * Export all configurations of an application in a specified environment and cluster *

* File Struts: *

* * List -> List -> List * * @param outputStream network file download stream to user */ public void exportAppConfigByEnvAndCluster(String appId, Env env, String clusterName, OutputStream outputStream) { App app = appService.load(appId); if (app == null) { throw new BadRequestException("App not found: " + appId); } ClusterDTO cluster = clusterService.loadCluster(appId, env, clusterName); if (cluster == null) { throw new BadRequestException( "The app does not exist in the specified environment and cluster."); } try (final ZipOutputStream zipOutputStream = new ZipOutputStream(outputStream)) { try { this.exportNamespaces(env, app, cluster, zipOutputStream, true); } catch (BadRequestException badRequestException) { // ignore } catch (Exception e) { logger.error("export namespace error. appId = {}, env = {}, cluster = {}", app.getAppId(), env.getName(), cluster.getName(), e); } } catch (IOException e) { logger.error("export app config error", e); throw new ServiceException("export app config error", e); } } private void exportApps(final Collection exportEnvs, OutputStream outputStream) { List hasPermissionApps = findHasPermissionApps(); if (CollectionUtils.isEmpty(hasPermissionApps)) { return; } try (final ZipOutputStream zipOutputStream = new ZipOutputStream(outputStream)) { // write app info to zip writeAppInfoToZip(hasPermissionApps, zipOutputStream); // export app namespace exportAppNamespaces(zipOutputStream); // export app's clusters exportEnvs.parallelStream().forEach(env -> { try { this.exportClusters(env, hasPermissionApps, zipOutputStream); } catch (Exception e) { logger.error("export cluster error. env = {}", env, e); } }); } catch (IOException e) { logger.error("export config error", e); throw new ServiceException("export config error", e); } } private List findHasPermissionApps() { // get all apps final List apps = appService.findAll(); if (CollectionUtils.isEmpty(apps)) { return Collections.emptyList(); } // permission check final Predicate isAppAdmin = app -> { try { return unifiedPermissionValidator.isAppAdmin(app.getAppId()); } catch (Exception e) { logger.error("permission check failed. app = {}", app); return false; } }; // app admin permission filter return apps.stream().filter(isAppAdmin).collect(Collectors.toList()); } private void writeAppInfoToZip(List apps, ZipOutputStream zipOutputStream) { logger.info("to import app size = {}", apps.size()); final Consumer appConsumer = app -> { try { synchronized (zipOutputStream) { String fileName = ConfigFileUtils.genAppInfoPath(app); String content = gson.toJson(app); writeToZip(fileName, content, zipOutputStream); } } catch (IOException e) { logger.error("Write error. {}", app); throw new ServiceException("Write app error. {}", e); } }; apps.forEach(appConsumer); } private void exportAppNamespaces(ZipOutputStream zipOutputStream) { List appNamespaces = appNamespaceService.findAll(); logger.info("to import appnamespace size = {}", appNamespaces.size()); Consumer appNamespaceConsumer = appNamespace -> { try { synchronized (zipOutputStream) { String fileName = ConfigFileUtils.genAppNamespaceInfoPath(appNamespace); String content = gson.toJson(appNamespace); writeToZip(fileName, content, zipOutputStream); } } catch (Exception e) { logger.error("Write appnamespace error. {}", appNamespace); throw new IllegalStateException(e); } }; appNamespaces.forEach(appNamespaceConsumer); } private void exportClusters(final Env env, final List exportApps, ZipOutputStream zipOutputStream) { exportApps.parallelStream().forEach(exportApp -> { try { this.exportCluster(env, exportApp, zipOutputStream); } catch (Exception e) { logger.error("export cluster error. appId = {}", exportApp.getAppId(), e); } }); } private void exportCluster(final Env env, final App exportApp, ZipOutputStream zipOutputStream) { final List exportClusters = clusterService.findClusters(env, exportApp.getAppId()); if (CollectionUtils.isEmpty(exportClusters)) { return; } // write cluster info to zip writeClusterInfoToZip(env, exportApp, exportClusters, zipOutputStream); // export namespaces exportClusters.parallelStream().forEach(cluster -> { try { this.exportNamespaces(env, exportApp, cluster, zipOutputStream, false); } catch (BadRequestException badRequestException) { // ignore } catch (Exception e) { logger.error("export namespace error. appId = {}, cluster = {}", exportApp.getAppId(), cluster, e); } }); } private void exportNamespaces(final Env env, final App exportApp, final ClusterDTO exportCluster, ZipOutputStream zipOutputStream, boolean ignoreUserDir) { String clusterName = exportCluster.getName(); List namespaceBOS = namespaceService.findNamespaceBOs(exportApp.getAppId(), env, clusterName, true, false); if (CollectionUtils.isEmpty(namespaceBOS)) { return; } Stream configBOStream = namespaceBOS.stream().map(namespaceBO -> new ConfigBO(env, exportApp.getOwnerName(), exportApp.getAppId(), clusterName, namespaceBO)); writeNamespacesToZip(configBOStream, zipOutputStream, ignoreUserDir); } private void writeNamespacesToZip(Stream configBOStream, ZipOutputStream zipOutputStream, boolean ignoreUserDir) { final Consumer configBOConsumer = configBO -> { try { synchronized (zipOutputStream) { String appId = configBO.getAppId(); String clusterName = configBO.getClusterName(); String namespace = configBO.getNamespace(); String configFileContent = configBO.getConfigFileContent(); ConfigFileFormat configFileFormat = configBO.getFormat(); String configFileName = ConfigFileUtils.toFilename(appId, clusterName, namespace, configFileFormat); String filePath = ignoreUserDir ? ConfigFileUtils.genNamespacePathIgnoreUser(appId, configBO.getEnv(), configFileName) : ConfigFileUtils.genNamespacePath(configBO.getOwnerName(), appId, configBO.getEnv(), configFileName); writeToZip(filePath, configFileContent, zipOutputStream); } } catch (IOException e) { logger.error("Write error. {}", configBO); throw new ServiceException("Write namespace error. {}", e); } }; configBOStream.forEach(configBOConsumer); } private void writeClusterInfoToZip(Env env, App app, List exportClusters, ZipOutputStream zipOutputStream) { final Consumer clusterConsumer = cluster -> { try { synchronized (zipOutputStream) { String fileName = ConfigFileUtils.genClusterInfoPath(app, env, cluster); String content = gson.toJson(cluster); writeToZip(fileName, content, zipOutputStream); } } catch (IOException e) { logger.error("Write error. {}", cluster); throw new ServiceException("Write error. {}", e); } }; exportClusters.forEach(clusterConsumer); } private void writeToZip(String filePath, String content, ZipOutputStream zipOutputStream) throws IOException { final ZipEntry zipEntry = new ZipEntry(filePath); try { zipOutputStream.putNextEntry(zipEntry); zipOutputStream.write(content.getBytes()); zipOutputStream.closeEntry(); } catch (IOException e) { String errorMsg = "write content to zip error. file = " + filePath + ", content = " + content; logger.error(errorMsg); throw new IOException(errorMsg, e); } } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/ConfigsImportService.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.service; import com.ctrip.framework.apollo.common.exception.BadRequestException; import com.google.common.collect.Lists; import com.google.gson.Gson; import com.ctrip.framework.apollo.common.constants.GsonType; import com.ctrip.framework.apollo.common.dto.ClusterDTO; import com.ctrip.framework.apollo.common.dto.ItemDTO; import com.ctrip.framework.apollo.common.dto.NamespaceDTO; import com.ctrip.framework.apollo.common.entity.App; import com.ctrip.framework.apollo.common.entity.AppNamespace; import com.ctrip.framework.apollo.common.exception.ServiceException; import com.ctrip.framework.apollo.core.ConfigConsts; import com.ctrip.framework.apollo.portal.environment.Env; import com.ctrip.framework.apollo.portal.listener.AppNamespaceCreationEvent; import com.ctrip.framework.apollo.portal.spi.UserInfoHolder; import com.ctrip.framework.apollo.portal.util.ConfigFileUtils; import com.ctrip.framework.apollo.portal.util.ConfigToFileUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.annotation.Lazy; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; import org.springframework.web.client.HttpStatusCodeException; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.Date; import java.util.List; import java.util.Objects; import java.util.concurrent.CountDownLatch; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; /** * @author wxq */ @Service public class ConfigsImportService { private static final Logger LOGGER = LoggerFactory.getLogger(ConfigsImportService.class); private Gson gson = new Gson(); private final ItemService itemService; private final AppService appService; private final ClusterService clusterService; private final NamespaceService namespaceService; private final AppNamespaceService appNamespaceService; private final ApplicationEventPublisher publisher; private final UserInfoHolder userInfoHolder; private final RoleInitializationService roleInitializationService; public ConfigsImportService(final ItemService itemService, final AppService appService, final ClusterService clusterService, final @Lazy NamespaceService namespaceService, final AppNamespaceService appNamespaceService, final ApplicationEventPublisher publisher, final UserInfoHolder userInfoHolder, final RoleInitializationService roleInitializationService) { this.itemService = itemService; this.appService = appService; this.clusterService = clusterService; this.namespaceService = namespaceService; this.appNamespaceService = appNamespaceService; this.publisher = publisher; this.userInfoHolder = userInfoHolder; this.roleInitializationService = roleInitializationService; } /** * force import, new items will overwrite existed items. */ public void forceImportNamespaceFromFile(final Env env, final String standardFilename, final InputStream zipInputStream) { String configText; try (InputStream in = zipInputStream) { configText = ConfigToFileUtils.fileToString(in); } catch (IOException e) { throw new ServiceException("Read config file errors:{}", e); } String operator = userInfoHolder.getUser().getUserId(); this.importNamespaceFromText(env, standardFilename, configText, false, operator); } /** * import all data include app、appnamespace、cluster、namespace、item */ public void importDataFromZipFile(List importEnvs, ZipInputStream dataZip, boolean ignoreConflictNamespace) throws IOException { List toImportApps = Lists.newArrayList(); List toImportAppNSs = Lists.newArrayList(); List toImportClusters = Lists.newArrayList(); List toImportNSs = Lists.newArrayList(); ZipEntry entry; while ((entry = dataZip.getNextEntry()) != null) { if (entry.isDirectory()) { continue; } String filePath = entry.getName(); String content = readContent(dataZip); String[] info = filePath.replace('\\', '/').split("/"); String fileName; if (info.length == 1) { // app namespace metadata file. path format : ${namespaceName}.appnamespace.metadata fileName = info[0]; if (fileName.endsWith(ConfigFileUtils.APP_NAMESPACE_METADATA_FILE_SUFFIX)) { toImportAppNSs.add(content); } } else if (info.length == 3) { fileName = info[2]; if (fileName.equals(ConfigFileUtils.APP_METADATA_FILENAME)) { // app metadata file. path format : apollo/${appId}/app.metadata toImportApps.add(content); } } else { String env = info[2]; fileName = info[3]; for (Env importEnv : importEnvs) { if (Objects.equals(importEnv.getName(), env)) { if (fileName.endsWith(ConfigFileUtils.CLUSTER_METADATA_FILE_SUFFIX)) { // cluster metadata file. path format : // apollo/${appId}/${env}/${clusterName}.cluster.metadata toImportClusters.add(new ImportClusterData(Env.transformEnv(env), content)); } else { // namespace file.path format : // apollo/${appId}/${env}/${appId}+${cluster}+${namespaceName} toImportNSs.add(new ImportNamespaceData(Env.valueOf(env), fileName, content, ignoreConflictNamespace)); } } } } } try { LOGGER.info("Import data. app = {}, appns = {}, cluster = {}, namespace = {}", toImportApps.size(), toImportAppNSs.size(), toImportClusters.size(), toImportNSs.size()); doImport(importEnvs, toImportApps, toImportAppNSs, toImportClusters, toImportNSs); } catch (Exception e) { LOGGER.error("import config error.", e); throw new ServiceException("import config error.", e); } } /** * import all configurations of an application in a specified environment and cluster */ public void importAppConfigFromZipFile(String appId, Env env, String clusterName, ZipInputStream dataZip, boolean ignoreConflictNamespace) throws IOException { ClusterDTO clusterDTO = clusterService.loadCluster(appId, env, clusterName); if (clusterDTO == null) { throw new BadRequestException( "The app does not exist in the specified environment and cluster."); } List toImportNSs = Lists.newArrayList(); ZipEntry entry; while ((entry = dataZip.getNextEntry()) != null) { if (entry.isDirectory()) { continue; } // file.path format : // ${appId}/${env}/${appId}+${cluster}+${namespaceName} String filePath = entry.getName(); String content = readContent(dataZip); if (content == null) { throw new BadRequestException("Failed to read file content."); } String[] info = filePath.replace('\\', '/').split("/"); if (info.length != 3) { throw new BadRequestException("Invalid file path in ZIP."); } String fileName = info[2]; String fileNamePrefix = String.format("%s+%s+", appId, clusterName); if (!info[0].equals(appId) || !info[1].equalsIgnoreCase(env.getName()) || !fileName.startsWith(fileNamePrefix)) { throw new BadRequestException("The content of the file to be imported is incorrect."); } if (!fileName.endsWith(ConfigFileUtils.CLUSTER_METADATA_FILE_SUFFIX)) { toImportNSs.add(new ImportNamespaceData(env, fileName, content, ignoreConflictNamespace)); } } if (CollectionUtils.isEmpty(toImportNSs)) { throw new BadRequestException("The configuration to be imported is empty."); } try { LOGGER.info("Import namespace. namespace = {}", toImportNSs.size()); doImport(Lists.newArrayList(), Lists.newArrayList(), Lists.newArrayList(), Lists.newArrayList(), toImportNSs); } catch (Exception e) { LOGGER.error("import app config error.", e); throw new ServiceException("import app config error.", e); } } private void doImport(List importEnvs, List toImportApps, List toImportAppNSs, List toImportClusters, List toImportNSs) throws InterruptedException { LOGGER.info("Start to import app. size = {}", toImportApps.size()); String operator = userInfoHolder.getUser().getUserId(); long startTime = System.currentTimeMillis(); CountDownLatch appLatch = new CountDownLatch(toImportApps.size()); toImportApps.parallelStream().forEach(app -> { try { importApp(app, importEnvs, operator); } catch (Exception e) { LOGGER.error("import app error. app = {}", app, e); } finally { appLatch.countDown(); } }); appLatch.await(); LOGGER.info("Finish to import app. duration = {}", System.currentTimeMillis() - startTime); LOGGER.info("Start to import appnamespace. size = {}", toImportAppNSs.size()); startTime = System.currentTimeMillis(); CountDownLatch appNSLatch = new CountDownLatch(toImportAppNSs.size()); toImportAppNSs.parallelStream().forEach(appNS -> { try { importAppNamespace(appNS, operator); } catch (Exception e) { LOGGER.error("import appnamespace error. appnamespace = {}", appNS, e); } finally { appNSLatch.countDown(); } }); appNSLatch.await(); LOGGER.info("Finish to import appnamespace. duration = {}", System.currentTimeMillis() - startTime); LOGGER.info("Start to import cluster. size = {}", toImportClusters.size()); startTime = System.currentTimeMillis(); CountDownLatch clusterLatch = new CountDownLatch(toImportClusters.size()); toImportClusters.parallelStream().forEach(cluster -> { try { importCluster(cluster, operator); } catch (Exception e) { LOGGER.error("import cluster error. cluster = {}", cluster, e); } finally { clusterLatch.countDown(); } }); clusterLatch.await(); LOGGER.info("Finish to import cluster. duration = {}", System.currentTimeMillis() - startTime); LOGGER.info("Start to import namespace. size = {}", toImportNSs.size()); startTime = System.currentTimeMillis(); CountDownLatch nsLatch = new CountDownLatch(toImportNSs.size()); toImportNSs.parallelStream().forEach(namespace -> { try { importNamespaceFromText(namespace.getEnv(), namespace.getFileName(), namespace.getContent(), namespace.isIgnoreConflictNamespace(), operator); } catch (Exception e) { LOGGER.error("import namespace error. namespace = {}", namespace, e); } finally { nsLatch.countDown(); } }); nsLatch.await(); LOGGER.info("Finish to import namespace. duration = {}", System.currentTimeMillis() - startTime); } private void importApp(String appInfo, List importEnvs, String operator) { App toImportApp = gson.fromJson(appInfo, App.class); String appId = toImportApp.getAppId(); toImportApp.setDataChangeCreatedBy(operator); toImportApp.setDataChangeLastModifiedBy(operator); toImportApp.setDataChangeCreatedTime(new Date()); toImportApp.setDataChangeLastModifiedTime(new Date()); App managedApp = appService.load(appId); if (managedApp == null) { appService.importAppInLocal(toImportApp); } importEnvs.parallelStream().forEach(env -> { try { appService.load(env, appId); } catch (Exception e) { // not existed appService.createAppInRemote(env, toImportApp); } }); } private void importAppNamespace(String appNamespace, String operator) { AppNamespace toImportPubAppNS = gson.fromJson(appNamespace, AppNamespace.class); String appId = toImportPubAppNS.getAppId(); String namespaceName = toImportPubAppNS.getName(); boolean isPublic = toImportPubAppNS.isPublic(); AppNamespace managedAppNamespace = isPublic ? appNamespaceService.findPublicAppNamespace(namespaceName) : appNamespaceService.findByAppIdAndName(appId, namespaceName); if (managedAppNamespace == null) { managedAppNamespace = new AppNamespace(); managedAppNamespace.setAppId(toImportPubAppNS.getAppId()); managedAppNamespace.setPublic(isPublic); managedAppNamespace.setFormat(toImportPubAppNS.getFormat()); managedAppNamespace.setComment(toImportPubAppNS.getComment()); managedAppNamespace.setDataChangeCreatedBy(operator); managedAppNamespace.setDataChangeLastModifiedBy(operator); managedAppNamespace.setName(namespaceName); AppNamespace createdAppNamespace = appNamespaceService.importAppNamespaceInLocal(managedAppNamespace); // application namespace will be auto created when creating app if (!ConfigConsts.NAMESPACE_APPLICATION.equals(namespaceName)) { publisher.publishEvent(new AppNamespaceCreationEvent(createdAppNamespace)); } } } private void importCluster(ImportClusterData importClusterData, String operator) { Env env = importClusterData.getEnv(); ClusterDTO toImportCluster = gson.fromJson(importClusterData.clusterInfo, ClusterDTO.class); toImportCluster.setDataChangeCreatedBy(operator); toImportCluster.setDataChangeLastModifiedBy(operator); toImportCluster.setDataChangeCreatedTime(new Date()); toImportCluster.setDataChangeLastModifiedTime(new Date()); String appId = toImportCluster.getAppId(); String clusterName = toImportCluster.getName(); try { clusterService.loadCluster(appId, env, clusterName); } catch (Exception e) { // not existed clusterService.createCluster(env, toImportCluster); } } /** * import a config file. the name of config file must be special like appId+cluster+namespace.format Example: *

   *   123456+default+application.properties (appId is 123456, cluster is default, namespace is application, format is properties)
   *   654321+north+password.yml (appId is 654321, cluster is north, namespace is password, format is yml)
   * 
* so we can get the information of appId, cluster, namespace, format from the file name. * * @param env environment * @param standardFilename appId+cluster+namespace.format * @param configText config content */ private void importNamespaceFromText(final Env env, final String standardFilename, final String configText, boolean ignoreConflictNamespace, String operator) { final String appId = ConfigFileUtils.getAppId(standardFilename); final String clusterName = ConfigFileUtils.getClusterName(standardFilename); final String namespace = ConfigFileUtils.getNamespace(standardFilename); final String format = ConfigFileUtils.getFormat(standardFilename); this.importNamespace(appId, env, clusterName, namespace, configText, format, ignoreConflictNamespace, operator); } private void importNamespace(final String appId, final Env env, final String clusterName, final String namespaceName, final String configText, final String format, boolean ignoreConflictNamespace, String operator) { NamespaceDTO namespaceDTO; try { namespaceDTO = namespaceService.loadNamespaceBaseInfo(appId, env, clusterName, namespaceName); } catch (Exception e) { // not existed namespaceDTO = null; } if (namespaceDTO == null) { namespaceDTO = new NamespaceDTO(); namespaceDTO.setAppId(appId); namespaceDTO.setClusterName(clusterName); namespaceDTO.setNamespaceName(namespaceName); namespaceDTO.setDataChangeCreatedBy(operator); namespaceDTO.setDataChangeLastModifiedBy(operator); namespaceDTO = namespaceService.createNamespace(env, namespaceDTO); roleInitializationService.initNamespaceRoles(appId, namespaceName, operator); roleInitializationService.initNamespaceEnvRoles(appId, namespaceName, operator); } List itemDTOS = itemService.findItems(appId, env, clusterName, namespaceName); // skip import if target namespace has existed items if (!CollectionUtils.isEmpty(itemDTOS) && ignoreConflictNamespace) { return; } importItems(appId, env, clusterName, namespaceName, configText, namespaceDTO, operator); } private void importItems(String appId, Env env, String clusterName, String namespaceName, String configText, NamespaceDTO namespaceDTO, String operator) { List toImportItems = gson.fromJson(configText, GsonType.ITEM_DTOS); toImportItems.parallelStream().forEach(newItem -> { String key = newItem.getKey(); newItem.setNamespaceId(namespaceDTO.getId()); newItem.setDataChangeCreatedBy(operator); newItem.setDataChangeLastModifiedBy(operator); newItem.setDataChangeCreatedTime(new Date()); newItem.setDataChangeLastModifiedTime(new Date()); if (StringUtils.hasText(key)) { // create or update normal item try { ItemDTO oldItem = itemService.loadItem(env, appId, clusterName, namespaceName, key); newItem.setId(oldItem.getId()); // existed itemService.updateItem(appId, env, clusterName, namespaceName, newItem); } catch (Exception e) { if (e instanceof HttpStatusCodeException && ((HttpStatusCodeException) e).getStatusCode().equals(HttpStatus.NOT_FOUND)) { // not existed itemService.createItem(appId, env, clusterName, namespaceName, newItem); } else { LOGGER.error( "Load or update item error. appId = {}, env = {}, cluster = {}, namespace = {}", appId, env, clusterName, namespaceDTO, e); } } } else if (StringUtils.hasText(newItem.getComment())) { // create comment item itemService.createCommentItem(appId, env, clusterName, namespaceName, newItem); } }); } private String readContent(ZipInputStream zipInputStream) { try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { byte[] buffer = new byte[1024]; int offset; while ((offset = zipInputStream.read(buffer)) != -1) { out.write(buffer, 0, offset); } return out.toString("UTF-8"); } catch (IOException e) { LOGGER.error("Read file content from zip error.", e); return null; } } static class ImportNamespaceData { private Env env; private String fileName; private String content; private boolean ignoreConflictNamespace; public ImportNamespaceData(Env env, String fileName, String content, boolean ignoreConflictNamespace) { this.env = env; this.fileName = fileName; this.content = content; this.ignoreConflictNamespace = ignoreConflictNamespace; } public Env getEnv() { return env; } public void setEnv(Env env) { this.env = env; } public String getFileName() { return fileName; } public void setFileName(String fileName) { this.fileName = fileName; } public String getContent() { return content; } public void setContent(String content) { this.content = content; } public boolean isIgnoreConflictNamespace() { return ignoreConflictNamespace; } public void setIgnoreConflictNamespace(boolean ignoreConflictNamespace) { this.ignoreConflictNamespace = ignoreConflictNamespace; } @Override public String toString() { return "NamespaceImportData{" + "env=" + env + ", fileName='" + fileName + '\'' + ", content='" + content + '\'' + ", ignoreConflictNamespace=" + ignoreConflictNamespace + '}'; } } static class ImportClusterData { private Env env; private String clusterInfo; public ImportClusterData(Env env, String clusterInfo) { this.env = env; this.clusterInfo = clusterInfo; } public Env getEnv() { return env; } public void setEnv(Env env) { this.env = env; } public String getClusterInfo() { return clusterInfo; } public void setClusterInfo(String clusterInfo) { this.clusterInfo = clusterInfo; } @Override public String toString() { return "ImportClusterData{" + "env=" + env + ", clusterName='" + clusterInfo + '\'' + '}'; } } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/FavoriteService.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.service; import com.ctrip.framework.apollo.common.exception.BadRequestException; import com.ctrip.framework.apollo.portal.entity.bo.UserInfo; import com.ctrip.framework.apollo.portal.entity.po.Favorite; import com.ctrip.framework.apollo.portal.repository.FavoriteRepository; import com.ctrip.framework.apollo.portal.spi.UserInfoHolder; import com.ctrip.framework.apollo.portal.spi.UserService; import com.google.common.base.Strings; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import java.util.Collections; import java.util.List; import java.util.Objects; @Service public class FavoriteService { public static final long POSITION_DEFAULT = 10000; private final UserInfoHolder userInfoHolder; private final FavoriteRepository favoriteRepository; private final UserService userService; public FavoriteService(final UserInfoHolder userInfoHolder, final FavoriteRepository favoriteRepository, final UserService userService) { this.userInfoHolder = userInfoHolder; this.favoriteRepository = favoriteRepository; this.userService = userService; } public Favorite addFavorite(Favorite favorite) { UserInfo user = userService.findByUserId(favorite.getUserId()); if (user == null) { throw BadRequestException.userNotExists(favorite.getUserId()); } UserInfo loginUser = userInfoHolder.getUser(); // user can only add himself favorite app if (!loginUser.equals(user)) { throw new BadRequestException( "add favorite fail. " + "because favorite's user is not current login user."); } Favorite checkedFavorite = favoriteRepository.findByUserIdAndAppId(loginUser.getUserId(), favorite.getAppId()); if (checkedFavorite != null) { return checkedFavorite; } favorite.setPosition(POSITION_DEFAULT); favorite.setDataChangeCreatedBy(user.getUserId()); favorite.setDataChangeLastModifiedBy(user.getUserId()); return favoriteRepository.save(favorite); } public List search(String userId, String appId, Pageable page) { boolean isUserIdEmpty = Strings.isNullOrEmpty(userId); boolean isAppIdEmpty = Strings.isNullOrEmpty(appId); if (isAppIdEmpty && isUserIdEmpty) { throw new BadRequestException("user id and app id can't be empty at the same time"); } if (!isUserIdEmpty) { UserInfo loginUser = userInfoHolder.getUser(); // user can only search his own favorite app if (!Objects.equals(loginUser.getUserId(), userId)) { userId = loginUser.getUserId(); } } // search by userId if (isAppIdEmpty) { return favoriteRepository.findByUserIdOrderByPositionAscDataChangeCreatedTimeAsc(userId, page); } // search by appId if (isUserIdEmpty) { return favoriteRepository.findByAppIdOrderByPositionAscDataChangeCreatedTimeAsc(appId, page); } // search by userId and appId return Collections.singletonList(favoriteRepository.findByUserIdAndAppId(userId, appId)); } public void deleteFavorite(long favoriteId) { Favorite favorite = favoriteRepository.findById(favoriteId).orElse(null); checkUserOperatePermission(favorite); favoriteRepository.delete(favorite); } public void adjustFavoriteToFirst(long favoriteId) { Favorite favorite = favoriteRepository.findById(favoriteId).orElse(null); checkUserOperatePermission(favorite); String userId = favorite.getUserId(); Favorite firstFavorite = favoriteRepository.findFirstByUserIdOrderByPositionAscDataChangeCreatedTimeAsc(userId); long minPosition = firstFavorite.getPosition(); favorite.setPosition(minPosition - 1); favoriteRepository.save(favorite); } private void checkUserOperatePermission(Favorite favorite) { if (favorite == null) { throw new BadRequestException("favorite not exist"); } if (!Objects.equals(userInfoHolder.getUser().getUserId(), favorite.getUserId())) { throw new BadRequestException("can not operate other person's favorite"); } } public void batchDeleteByAppId(String appId, String operator) { favoriteRepository.batchDeleteByAppId(appId, operator); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/GlobalSearchService.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.service; import com.ctrip.framework.apollo.common.dto.ItemInfoDTO; import com.ctrip.framework.apollo.common.dto.PageDTO; import com.ctrip.framework.apollo.common.http.SearchResponseEntity; import com.ctrip.framework.apollo.portal.api.AdminServiceAPI; import com.ctrip.framework.apollo.portal.component.PortalSettings; import com.ctrip.framework.apollo.portal.entity.vo.ItemInfo; import com.ctrip.framework.apollo.portal.environment.Env; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; @Service public class GlobalSearchService { private static final Logger LOGGER = LoggerFactory.getLogger(GlobalSearchService.class); private final AdminServiceAPI.ItemAPI itemAPI; private final PortalSettings portalSettings; public GlobalSearchService(AdminServiceAPI.ItemAPI itemAPI, PortalSettings portalSettings) { this.itemAPI = itemAPI; this.portalSettings = portalSettings; } public SearchResponseEntity> getAllEnvItemInfoBySearch(String key, String value, int page, int size) { List activeEnvs = portalSettings.getActiveEnvs(); List envBeyondLimit = new ArrayList<>(); AtomicBoolean hasMoreData = new AtomicBoolean(false); List allEnvItemInfos = new ArrayList<>(); activeEnvs.forEach(env -> { PageDTO perEnvItemInfoDTOs = itemAPI.getPerEnvItemInfoBySearch(env, key, value, page, size); if (!perEnvItemInfoDTOs.hasContent()) { return; } perEnvItemInfoDTOs.getContent().forEach(itemInfoDTO -> { try { ItemInfo itemInfo = new ItemInfo(itemInfoDTO.getAppId(), env.getName(), itemInfoDTO.getClusterName(), itemInfoDTO.getNamespaceName(), itemInfoDTO.getKey(), itemInfoDTO.getValue()); allEnvItemInfos.add(itemInfo); } catch (Exception e) { LOGGER.error("Error converting ItemInfoDTO to ItemInfo for item: {}", itemInfoDTO, e); } }); if (perEnvItemInfoDTOs.getTotal() > size) { envBeyondLimit.add(env.getName()); hasMoreData.set(true); } }); if (hasMoreData.get()) { return SearchResponseEntity.okWithMessage(allEnvItemInfos, String.format( "In %s , more than %d items found (Exceeded the maximum search quantity for a single environment). Please enter more precise criteria to narrow down the search scope.", String.join(" , ", envBeyondLimit), size)); } return SearchResponseEntity.ok(allEnvItemInfos); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/InstanceService.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.service; import com.ctrip.framework.apollo.common.dto.InstanceDTO; import com.ctrip.framework.apollo.common.dto.PageDTO; import com.ctrip.framework.apollo.portal.environment.Env; import com.ctrip.framework.apollo.portal.api.AdminServiceAPI; import org.springframework.stereotype.Service; import java.util.List; import java.util.Set; @Service public class InstanceService { private final AdminServiceAPI.InstanceAPI instanceAPI; public InstanceService(final AdminServiceAPI.InstanceAPI instanceAPI) { this.instanceAPI = instanceAPI; } public PageDTO getByRelease(Env env, long releaseId, int page, int size) { return instanceAPI.getByRelease(env, releaseId, page, size); } public PageDTO getByNamespace(Env env, String appId, String clusterName, String namespaceName, String instanceAppId, int page, int size) { return instanceAPI.getByNamespace(appId, env, clusterName, namespaceName, instanceAppId, page, size); } public int getInstanceCountByNamespace(String appId, Env env, String clusterName, String namespaceName) { return instanceAPI.getInstanceCountByNamespace(appId, env, clusterName, namespaceName); } public List getByReleasesNotIn(Env env, String appId, String clusterName, String namespaceName, Set releaseIds) { return instanceAPI.getByReleasesNotIn(appId, env, clusterName, namespaceName, releaseIds); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/ItemService.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.service; import com.ctrip.framework.apollo.common.constants.GsonType; import com.ctrip.framework.apollo.common.dto.*; import com.ctrip.framework.apollo.common.exception.BadRequestException; import com.ctrip.framework.apollo.common.exception.NotFoundException; import com.ctrip.framework.apollo.common.utils.BeanUtils; import com.ctrip.framework.apollo.core.enums.ConfigFileFormat; import com.ctrip.framework.apollo.openapi.utils.UrlUtils; import com.ctrip.framework.apollo.openapi.dto.OpenItemDTO; import com.ctrip.framework.apollo.portal.api.AdminServiceAPI.ItemAPI; import com.ctrip.framework.apollo.portal.api.AdminServiceAPI.NamespaceAPI; import com.ctrip.framework.apollo.portal.api.AdminServiceAPI.ReleaseAPI; import com.ctrip.framework.apollo.portal.environment.Env; import com.ctrip.framework.apollo.core.utils.StringUtils; import com.ctrip.framework.apollo.portal.api.AdminServiceAPI; import com.ctrip.framework.apollo.portal.component.txtresolver.ConfigTextResolver; import com.ctrip.framework.apollo.portal.constant.TracerEventType; import com.ctrip.framework.apollo.portal.entity.model.NamespaceTextModel; import com.ctrip.framework.apollo.portal.entity.vo.ItemDiffs; import com.ctrip.framework.apollo.portal.entity.vo.NamespaceIdentifier; import com.ctrip.framework.apollo.portal.spi.UserInfoHolder; import com.ctrip.framework.apollo.tracer.Tracer; import com.google.gson.Gson; import java.util.HashMap; import java.util.concurrent.atomic.AtomicInteger; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; import org.springframework.web.client.HttpClientErrorException; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @Service public class ItemService { private static final Gson GSON = new Gson(); private final UserInfoHolder userInfoHolder; private final AdminServiceAPI.NamespaceAPI namespaceAPI; private final AdminServiceAPI.ItemAPI itemAPI; private final AdminServiceAPI.ReleaseAPI releaseAPI; private final ConfigTextResolver fileTextResolver; private final ConfigTextResolver propertyResolver; public ItemService(final UserInfoHolder userInfoHolder, final NamespaceAPI namespaceAPI, final ItemAPI itemAPI, final ReleaseAPI releaseAPI, final @Qualifier("fileTextResolver") ConfigTextResolver fileTextResolver, final @Qualifier("propertyResolver") ConfigTextResolver propertyResolver) { this.userInfoHolder = userInfoHolder; this.namespaceAPI = namespaceAPI; this.itemAPI = itemAPI; this.releaseAPI = releaseAPI; this.fileTextResolver = fileTextResolver; this.propertyResolver = propertyResolver; } /** * parse config text and update config items * * @return parse result */ public void updateConfigItemByText(NamespaceTextModel model) { String appId = model.getAppId(); Env env = model.getEnv(); String clusterName = model.getClusterName(); String namespaceName = model.getNamespaceName(); NamespaceDTO namespace = namespaceAPI.loadNamespace(appId, env, clusterName, namespaceName); if (namespace == null) { throw BadRequestException.namespaceNotExists(appId, clusterName, namespaceName); } long namespaceId = namespace.getId(); // In case someone constructs an attack scenario if (model.getNamespaceId() != namespaceId) { throw BadRequestException.namespaceNotExists(); } String configText = model.getConfigText(); ConfigTextResolver resolver = model.getFormat() == ConfigFileFormat.Properties ? propertyResolver : fileTextResolver; ItemChangeSets changeSets = resolver.resolve(namespaceId, configText, itemAPI.findItems(appId, env, clusterName, namespaceName)); if (changeSets.isEmpty()) { return; } String operator = model.getOperator(); if (StringUtils.isBlank(operator)) { operator = userInfoHolder.getUser().getUserId(); } changeSets.setDataChangeLastModifiedBy(operator); updateItems(appId, env, clusterName, namespaceName, changeSets); Tracer.logEvent(TracerEventType.MODIFY_NAMESPACE_BY_TEXT, String.format("%s+%s+%s+%s", appId, env, clusterName, namespaceName)); Tracer.logEvent(TracerEventType.MODIFY_NAMESPACE, String.format("%s+%s+%s+%s", appId, env, clusterName, namespaceName)); } public void updateItems(String appId, Env env, String clusterName, String namespaceName, ItemChangeSets changeSets) { itemAPI.updateItemsByChangeSet(appId, env, clusterName, namespaceName, changeSets); } public ItemDTO createItem(String appId, Env env, String clusterName, String namespaceName, ItemDTO item) { NamespaceDTO namespace = namespaceAPI.loadNamespace(appId, env, clusterName, namespaceName); if (namespace == null) { throw BadRequestException.namespaceNotExists(appId, clusterName, namespaceName); } item.setNamespaceId(namespace.getId()); ItemDTO itemDTO = itemAPI.createItem(appId, env, clusterName, namespaceName, item); Tracer.logEvent(TracerEventType.MODIFY_NAMESPACE, String.format("%s+%s+%s+%s", appId, env, clusterName, namespaceName)); return itemDTO; } public ItemDTO createCommentItem(String appId, Env env, String clusterName, String namespaceName, ItemDTO item) { NamespaceDTO namespace = namespaceAPI.loadNamespace(appId, env, clusterName, namespaceName); if (namespace == null) { throw BadRequestException.namespaceNotExists(appId, clusterName, namespaceName); } item.setNamespaceId(namespace.getId()); return itemAPI.createCommentItem(appId, env, clusterName, namespaceName, item); } public void updateItem(String appId, Env env, String clusterName, String namespaceName, ItemDTO item) { itemAPI.updateItem(appId, env, clusterName, namespaceName, item.getId(), item); } public void deleteItem(Env env, long itemId, String userId) { itemAPI.deleteItem(env, itemId, userId); } public List findItems(String appId, Env env, String clusterName, String namespaceName) { return itemAPI.findItems(appId, env, clusterName, namespaceName); } public List findDeletedItems(String appId, Env env, String clusterName, String namespaceName) { return itemAPI.findDeletedItems(appId, env, clusterName, namespaceName); } public ItemDTO loadItem(Env env, String appId, String clusterName, String namespaceName, String key) { if (UrlUtils.hasIllegalChar(key)) { return itemAPI.loadItemByEncodeKey(env, appId, clusterName, namespaceName, key); } return itemAPI.loadItem(env, appId, clusterName, namespaceName, key); } public ItemDTO loadItemById(Env env, long itemId) { ItemDTO item = itemAPI.loadItemById(env, itemId); if (item == null) { throw NotFoundException.itemNotFound(itemId); } return item; } public void syncItems(List comparedNamespaces, List sourceItems) { List itemDiffs = compare(comparedNamespaces, sourceItems); for (ItemDiffs itemDiff : itemDiffs) { NamespaceIdentifier namespaceIdentifier = itemDiff.getNamespace(); ItemChangeSets changeSets = itemDiff.getDiffs(); changeSets.setDataChangeLastModifiedBy(userInfoHolder.getUser().getUserId()); String appId = namespaceIdentifier.getAppId(); Env env = namespaceIdentifier.getEnv(); String clusterName = namespaceIdentifier.getClusterName(); String namespaceName = namespaceIdentifier.getNamespaceName(); itemAPI.updateItemsByChangeSet(appId, env, clusterName, namespaceName, changeSets); Tracer.logEvent(TracerEventType.SYNC_NAMESPACE, String.format("%s+%s+%s+%s", appId, env, clusterName, namespaceName)); } } public void revokeItem(String appId, Env env, String clusterName, String namespaceName) { NamespaceDTO namespace = namespaceAPI.loadNamespace(appId, env, clusterName, namespaceName); if (namespace == null) { throw BadRequestException.namespaceNotExists(appId, clusterName, namespaceName); } long namespaceId = namespace.getId(); Map releaseItemDTOs = new HashMap<>(); ReleaseDTO latestRelease = releaseAPI.loadLatestRelease(appId, env, clusterName, namespaceName); if (latestRelease != null) { releaseItemDTOs = GSON.fromJson(latestRelease.getConfigurations(), GsonType.CONFIG); } List baseItems = itemAPI.findItems(appId, env, clusterName, namespaceName); Map oldKeyMapItem = BeanUtils.mapByKey("key", baseItems); // remove comment and blank item map. oldKeyMapItem.remove(""); // deleted items for comment Map deletedItemDTOs = findDeletedItems(appId, env, clusterName, namespaceName) .stream().filter(itemDTO -> !StringUtils.isEmpty(itemDTO.getKey())) .collect(Collectors.toMap(ItemDTO::getKey, v -> v, (v1, v2) -> v2)); ItemChangeSets changeSets = new ItemChangeSets(); AtomicInteger lineNum = new AtomicInteger(1); releaseItemDTOs.forEach((key, value) -> { ItemDTO oldItem = oldKeyMapItem.get(key); if (oldItem == null) { ItemDTO deletedItemDto = deletedItemDTOs.computeIfAbsent(key, k -> new ItemDTO()); int newLineNum = 0 == deletedItemDto.getLineNum() ? lineNum.get() : deletedItemDto.getLineNum(); changeSets.addCreateItem( buildNormalItem(0L, namespaceId, key, value, deletedItemDto.getComment(), newLineNum)); } else if (!StringUtils.equals(oldItem.getValue(), value) || lineNum.get() != oldItem.getLineNum()) { changeSets.addUpdateItem(buildNormalItem(oldItem.getId(), namespaceId, key, value, oldItem.getComment(), oldItem.getLineNum())); } oldKeyMapItem.remove(key); lineNum.set(lineNum.get() + 1); }); oldKeyMapItem.forEach((key, value) -> changeSets.addDeleteItem(value)); changeSets.setDataChangeLastModifiedBy(userInfoHolder.getUser().getUserId()); updateItems(appId, env, clusterName, namespaceName, changeSets); String formatStr = String.format("%s+%s+%s+%s", appId, env, clusterName, namespaceName); Tracer.logEvent(TracerEventType.MODIFY_NAMESPACE_BY_TEXT, formatStr); Tracer.logEvent(TracerEventType.MODIFY_NAMESPACE, formatStr); } public List compare(List comparedNamespaces, List sourceItems) { List result = new LinkedList<>(); for (NamespaceIdentifier namespace : comparedNamespaces) { ItemDiffs itemDiffs = new ItemDiffs(namespace); try { itemDiffs.setDiffs(parseChangeSets(namespace, sourceItems)); } catch (BadRequestException e) { itemDiffs.setDiffs(new ItemChangeSets()); itemDiffs.setExtInfo("该集群下没有名为 " + namespace.getNamespaceName() + " 的namespace"); } result.add(itemDiffs); } return result; } public PageDTO findItemsByNamespace(String appId, Env env, String clusterName, String namespaceName, int page, int size) { return itemAPI.findItemsByNamespace(appId, env, clusterName, namespaceName, page, size); } private long getNamespaceId(NamespaceIdentifier namespaceIdentifier) { String appId = namespaceIdentifier.getAppId(); String clusterName = namespaceIdentifier.getClusterName(); String namespaceName = namespaceIdentifier.getNamespaceName(); Env env = namespaceIdentifier.getEnv(); NamespaceDTO namespaceDTO; try { namespaceDTO = namespaceAPI.loadNamespace(appId, env, clusterName, namespaceName); } catch (HttpClientErrorException e) { if (e.getStatusCode() == HttpStatus.NOT_FOUND) { throw BadRequestException.namespaceNotExists(appId, clusterName, namespaceName); } throw e; } return namespaceDTO.getId(); } private ItemChangeSets parseChangeSets(NamespaceIdentifier namespace, List sourceItems) { ItemChangeSets changeSets = new ItemChangeSets(); List targetItems = itemAPI.findItems(namespace.getAppId(), namespace.getEnv(), namespace.getClusterName(), namespace.getNamespaceName()); long namespaceId = getNamespaceId(namespace); if (CollectionUtils.isEmpty(targetItems)) {// all source items is added int lineNum = 1; for (ItemDTO sourceItem : sourceItems) { changeSets.addCreateItem(buildItem(namespaceId, lineNum++, sourceItem)); } } else { Map targetItemMap = BeanUtils.mapByKey("key", targetItems); String key, sourceValue, sourceComment; ItemDTO targetItem; int maxLineNum = targetItems.size();// append to last for (ItemDTO sourceItem : sourceItems) { key = sourceItem.getKey(); sourceValue = sourceItem.getValue(); sourceComment = sourceItem.getComment(); targetItem = targetItemMap.get(key); if (targetItem == null) {// added items changeSets.addCreateItem(buildItem(namespaceId, ++maxLineNum, sourceItem)); } else if (isModified(sourceValue, targetItem.getValue(), sourceComment, targetItem.getComment())) {// modified items targetItem.setValue(sourceValue); targetItem.setComment(sourceComment); changeSets.addUpdateItem(targetItem); } } } return changeSets; } private ItemDTO buildItem(long namespaceId, int lineNum, ItemDTO sourceItem) { ItemDTO createdItem = new ItemDTO(); BeanUtils.copyEntityProperties(sourceItem, createdItem); createdItem.setLineNum(lineNum); createdItem.setNamespaceId(namespaceId); return createdItem; } private ItemDTO buildNormalItem(Long id, Long namespaceId, String key, String value, String comment, int lineNum) { ItemDTO item = new ItemDTO(key, value, comment, lineNum); item.setId(id); item.setNamespaceId(namespaceId); return item; } private boolean isModified(String sourceValue, String targetValue, String sourceComment, String targetComment) { if (!sourceValue.equals(targetValue)) { return true; } if (sourceComment == null) { return !StringUtils.isEmpty(targetComment); } if (targetComment != null) { return !sourceComment.equals(targetComment); } return false; } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/NamespaceBranchService.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.service; import com.ctrip.framework.apollo.common.dto.GrayReleaseRuleDTO; import com.ctrip.framework.apollo.common.dto.ItemChangeSets; import com.ctrip.framework.apollo.common.dto.ItemDTO; import com.ctrip.framework.apollo.common.dto.NamespaceDTO; import com.ctrip.framework.apollo.common.dto.ReleaseDTO; import com.ctrip.framework.apollo.common.exception.BadRequestException; import com.ctrip.framework.apollo.portal.environment.Env; import com.ctrip.framework.apollo.portal.api.AdminServiceAPI; import com.ctrip.framework.apollo.portal.component.ItemsComparator; import com.ctrip.framework.apollo.portal.constant.TracerEventType; import com.ctrip.framework.apollo.portal.entity.bo.NamespaceBO; import com.ctrip.framework.apollo.portal.spi.UserInfoHolder; import com.ctrip.framework.apollo.tracer.Tracer; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.Collections; import java.util.List; @Service public class NamespaceBranchService { private final ItemsComparator itemsComparator; private final UserInfoHolder userInfoHolder; private final NamespaceService namespaceService; private final ItemService itemService; private final AdminServiceAPI.NamespaceBranchAPI namespaceBranchAPI; private final ReleaseService releaseService; public NamespaceBranchService(final ItemsComparator itemsComparator, final UserInfoHolder userInfoHolder, final NamespaceService namespaceService, final ItemService itemService, final AdminServiceAPI.NamespaceBranchAPI namespaceBranchAPI, final ReleaseService releaseService) { this.itemsComparator = itemsComparator; this.userInfoHolder = userInfoHolder; this.namespaceService = namespaceService; this.itemService = itemService; this.namespaceBranchAPI = namespaceBranchAPI; this.releaseService = releaseService; } @Transactional public NamespaceDTO createBranch(String appId, Env env, String parentClusterName, String namespaceName) { String operator = userInfoHolder.getUser().getUserId(); return createBranch(appId, env, parentClusterName, namespaceName, operator); } @Transactional public NamespaceDTO createBranch(String appId, Env env, String parentClusterName, String namespaceName, String operator) { NamespaceDTO createdBranch = namespaceBranchAPI.createBranch(appId, env, parentClusterName, namespaceName, operator); Tracer.logEvent(TracerEventType.CREATE_GRAY_RELEASE, String.format("%s+%s+%s+%s", appId, env, parentClusterName, namespaceName)); return createdBranch; } public GrayReleaseRuleDTO findBranchGrayRules(String appId, Env env, String clusterName, String namespaceName, String branchName) { return namespaceBranchAPI.findBranchGrayRules(appId, env, clusterName, namespaceName, branchName); } public void updateBranchGrayRules(String appId, Env env, String clusterName, String namespaceName, String branchName, GrayReleaseRuleDTO rules) { String operator = userInfoHolder.getUser().getUserId(); updateBranchGrayRules(appId, env, clusterName, namespaceName, branchName, rules, operator); } public void updateBranchGrayRules(String appId, Env env, String clusterName, String namespaceName, String branchName, GrayReleaseRuleDTO rules, String operator) { rules.setDataChangeCreatedBy(operator); rules.setDataChangeLastModifiedBy(operator); namespaceBranchAPI.updateBranchGrayRules(appId, env, clusterName, namespaceName, branchName, rules); Tracer.logEvent(TracerEventType.UPDATE_GRAY_RELEASE_RULE, String.format("%s+%s+%s+%s", appId, env, clusterName, namespaceName)); } public void deleteBranch(String appId, Env env, String clusterName, String namespaceName, String branchName) { String operator = userInfoHolder.getUser().getUserId(); deleteBranch(appId, env, clusterName, namespaceName, branchName, operator); } public void deleteBranch(String appId, Env env, String clusterName, String namespaceName, String branchName, String operator) { namespaceBranchAPI.deleteBranch(appId, env, clusterName, namespaceName, branchName, operator); Tracer.logEvent(TracerEventType.DELETE_GRAY_RELEASE, String.format("%s+%s+%s+%s", appId, env, clusterName, namespaceName)); } public ReleaseDTO merge(String appId, Env env, String clusterName, String namespaceName, String branchName, String title, String comment, boolean isEmergencyPublish, boolean deleteBranch) { String operator = userInfoHolder.getUser().getUserId(); return merge(appId, env, clusterName, namespaceName, branchName, title, comment, isEmergencyPublish, deleteBranch, operator); } public ReleaseDTO merge(String appId, Env env, String clusterName, String namespaceName, String branchName, String title, String comment, boolean isEmergencyPublish, boolean deleteBranch, String operator) { ItemChangeSets changeSets = calculateBranchChangeSet(appId, env, clusterName, namespaceName, branchName, operator); ReleaseDTO mergedResult = releaseService.updateAndPublish(appId, env, clusterName, namespaceName, title, comment, branchName, isEmergencyPublish, deleteBranch, changeSets); Tracer.logEvent(TracerEventType.MERGE_GRAY_RELEASE, String.format("%s+%s+%s+%s", appId, env, clusterName, namespaceName)); return mergedResult; } private ItemChangeSets calculateBranchChangeSet(String appId, Env env, String clusterName, String namespaceName, String branchName, String operator) { NamespaceBO parentNamespace = namespaceService.loadNamespaceBO(appId, env, clusterName, namespaceName); if (parentNamespace == null) { throw BadRequestException.namespaceNotExists(appId, clusterName, namespaceName); } if (parentNamespace.getItemModifiedCnt() > 0) { throw new BadRequestException("Merge operation failed. Because master has modified items"); } List masterItems = itemService.findItems(appId, env, clusterName, namespaceName); List branchItems = itemService.findItems(appId, env, branchName, namespaceName); ItemChangeSets changeSets = itemsComparator.compareIgnoreBlankAndCommentItem( parentNamespace.getBaseInfo().getId(), masterItems, branchItems); changeSets.setDeleteItems(Collections.emptyList()); changeSets.setDataChangeLastModifiedBy(operator); return changeSets; } public NamespaceDTO findBranchBaseInfo(String appId, Env env, String clusterName, String namespaceName) { return namespaceBranchAPI.findBranch(appId, env, clusterName, namespaceName); } public NamespaceBO findBranch(String appId, Env env, String clusterName, String namespaceName) { NamespaceDTO namespaceDTO = findBranchBaseInfo(appId, env, clusterName, namespaceName); if (namespaceDTO == null) { return null; } return namespaceService.loadNamespaceBO(appId, env, namespaceDTO.getClusterName(), namespaceName); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/NamespaceLockService.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.service; import com.ctrip.framework.apollo.common.dto.NamespaceLockDTO; import com.ctrip.framework.apollo.portal.environment.Env; import com.ctrip.framework.apollo.portal.api.AdminServiceAPI; import com.ctrip.framework.apollo.portal.component.config.PortalConfig; import com.ctrip.framework.apollo.portal.entity.vo.LockInfo; import org.springframework.stereotype.Service; @Service public class NamespaceLockService { private final AdminServiceAPI.NamespaceLockAPI namespaceLockAPI; private final PortalConfig portalConfig; public NamespaceLockService(final AdminServiceAPI.NamespaceLockAPI namespaceLockAPI, final PortalConfig portalConfig) { this.namespaceLockAPI = namespaceLockAPI; this.portalConfig = portalConfig; } public NamespaceLockDTO getNamespaceLock(String appId, Env env, String clusterName, String namespaceName) { return namespaceLockAPI.getNamespaceLockOwner(appId, env, clusterName, namespaceName); } public LockInfo getNamespaceLockInfo(String appId, Env env, String clusterName, String namespaceName) { LockInfo lockInfo = new LockInfo(); NamespaceLockDTO namespaceLockDTO = namespaceLockAPI.getNamespaceLockOwner(appId, env, clusterName, namespaceName); String lockOwner = namespaceLockDTO == null ? "" : namespaceLockDTO.getDataChangeCreatedBy(); lockInfo.setLockOwner(lockOwner); lockInfo.setEmergencyPublishAllowed(portalConfig.isEmergencyPublishAllowed(env)); return lockInfo; } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/NamespaceService.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.service; import com.ctrip.framework.apollo.common.constants.GsonType; import com.ctrip.framework.apollo.common.dto.ClusterDTO; import com.ctrip.framework.apollo.common.dto.ItemDTO; import com.ctrip.framework.apollo.common.dto.NamespaceDTO; import com.ctrip.framework.apollo.common.dto.PageDTO; import com.ctrip.framework.apollo.common.dto.ReleaseDTO; import com.ctrip.framework.apollo.common.entity.AppNamespace; import com.ctrip.framework.apollo.common.exception.BadRequestException; import com.ctrip.framework.apollo.common.utils.BeanUtils; import com.ctrip.framework.apollo.core.enums.ConfigFileFormat; import com.ctrip.framework.apollo.core.utils.ApolloThreadFactory; import com.ctrip.framework.apollo.core.utils.StringUtils; import com.ctrip.framework.apollo.portal.api.AdminServiceAPI; import com.ctrip.framework.apollo.portal.api.AdminServiceAPI.NamespaceAPI; import com.ctrip.framework.apollo.portal.component.PortalSettings; import com.ctrip.framework.apollo.portal.component.config.PortalConfig; import com.ctrip.framework.apollo.portal.constant.RoleType; import com.ctrip.framework.apollo.portal.constant.TracerEventType; import com.ctrip.framework.apollo.portal.enricher.adapter.BaseDtoUserInfoEnrichedAdapter; import com.ctrip.framework.apollo.portal.entity.bo.ItemBO; import com.ctrip.framework.apollo.portal.entity.bo.NamespaceBO; import com.ctrip.framework.apollo.portal.entity.vo.NamespaceUsage; import com.ctrip.framework.apollo.portal.environment.Env; import com.ctrip.framework.apollo.portal.spi.UserInfoHolder; import com.ctrip.framework.apollo.portal.util.RoleUtils; import com.ctrip.framework.apollo.tracer.Tracer; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.google.gson.Gson; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Lazy; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service public class NamespaceService { private static final Logger LOGGER = LoggerFactory.getLogger(NamespaceService.class); private static final Gson GSON = new Gson(); private static final ExecutorService executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 2, ApolloThreadFactory.create("NamespaceService", true)); private final PortalConfig portalConfig; private final PortalSettings portalSettings; private final UserInfoHolder userInfoHolder; private final AdminServiceAPI.NamespaceAPI namespaceAPI; private final ItemService itemService; private final ReleaseService releaseService; private final AppNamespaceService appNamespaceService; private final InstanceService instanceService; private final NamespaceBranchService branchService; private final RolePermissionService rolePermissionService; private final AdditionalUserInfoEnrichService additionalUserInfoEnrichService; private final ClusterService clusterService; public NamespaceService(final PortalConfig portalConfig, final PortalSettings portalSettings, final UserInfoHolder userInfoHolder, final NamespaceAPI namespaceAPI, final ItemService itemService, final ReleaseService releaseService, final AppNamespaceService appNamespaceService, final InstanceService instanceService, final @Lazy NamespaceBranchService branchService, final RolePermissionService rolePermissionService, final AdditionalUserInfoEnrichService additionalUserInfoEnrichService, ClusterService clusterService) { this.portalConfig = portalConfig; this.portalSettings = portalSettings; this.userInfoHolder = userInfoHolder; this.namespaceAPI = namespaceAPI; this.itemService = itemService; this.releaseService = releaseService; this.appNamespaceService = appNamespaceService; this.instanceService = instanceService; this.branchService = branchService; this.rolePermissionService = rolePermissionService; this.additionalUserInfoEnrichService = additionalUserInfoEnrichService; this.clusterService = clusterService; } public NamespaceDTO createNamespace(Env env, NamespaceDTO namespace) { if (StringUtils.isEmpty(namespace.getDataChangeCreatedBy())) { namespace.setDataChangeCreatedBy(userInfoHolder.getUser().getUserId()); } if (StringUtils.isEmpty(namespace.getDataChangeLastModifiedBy())) { namespace.setDataChangeLastModifiedBy(userInfoHolder.getUser().getUserId()); } NamespaceDTO createdNamespace = namespaceAPI.createNamespace(env, namespace); Tracer.logEvent(TracerEventType.CREATE_NAMESPACE, String.format("%s+%s+%s+%s", namespace.getAppId(), env, namespace.getClusterName(), namespace.getNamespaceName())); return createdNamespace; } public List getNamespaceUsageByAppId(String appId, String namespaceName) { List envs = portalSettings.getActiveEnvs(); AppNamespace appNamespace = appNamespaceService.findByAppIdAndName(appId, namespaceName); List usages = new ArrayList<>(); for (Env env : envs) { List clusters = clusterService.findClusters(env, appId); for (ClusterDTO cluster : clusters) { String clusterName = cluster.getName(); NamespaceUsage usage = this.getNamespaceUsageByEnv(appId, namespaceName, env, clusterName); if (appNamespace != null && appNamespace.isPublic()) { int associatedNamespace = this.getPublicAppNamespaceHasAssociatedNamespace(namespaceName, env); usage.setLinkedNamespaceCount(associatedNamespace); } if (usage.getLinkedNamespaceCount() > 0 || usage.getBranchInstanceCount() > 0 || usage.getInstanceCount() > 0) { usages.add(usage); } } } return usages; } public NamespaceUsage getNamespaceUsageByEnv(String appId, String namespaceName, Env env, String clusterName) { NamespaceUsage namespaceUsage = new NamespaceUsage(namespaceName, appId, clusterName, env.getName()); int instanceCount = instanceService.getInstanceCountByNamespace(appId, env, clusterName, namespaceName); namespaceUsage.setInstanceCount(instanceCount); NamespaceDTO branchNamespace = branchService.findBranchBaseInfo(appId, env, clusterName, namespaceName); if (branchNamespace != null) { String branchClusterName = branchNamespace.getClusterName(); int branchInstanceCount = instanceService.getInstanceCountByNamespace(appId, env, branchClusterName, namespaceName); namespaceUsage.setBranchInstanceCount(branchInstanceCount); } return namespaceUsage; } @Transactional public void deleteNamespace(String appId, Env env, String clusterName, String namespaceName) { String operator = userInfoHolder.getUser().getUserId(); namespaceAPI.deleteNamespace(env, appId, clusterName, namespaceName, operator); } public NamespaceDTO loadNamespaceBaseInfo(String appId, Env env, String clusterName, String namespaceName) { NamespaceDTO namespace = namespaceAPI.loadNamespace(appId, env, clusterName, namespaceName); if (namespace == null) { throw BadRequestException.namespaceNotExists(appId, clusterName, namespaceName); } return namespace; } /** * load cluster all namespace info with items */ public List findNamespaceBOs(String appId, Env env, String clusterName, boolean fillItemDetail, boolean includeDeletedItems) { List namespaces = namespaceAPI.findNamespaceByCluster(appId, env, clusterName); if (namespaces == null || namespaces.isEmpty()) { throw BadRequestException.namespaceNotExists(); } List namespaceBOs = Collections.synchronizedList(new LinkedList<>()); List exceptionNamespaces = Collections.synchronizedList(new LinkedList<>()); CountDownLatch latch = new CountDownLatch(namespaces.size()); for (NamespaceDTO namespace : namespaces) { executorService.submit(() -> { NamespaceBO namespaceBO; try { namespaceBO = transformNamespace2BO(env, namespace, fillItemDetail, includeDeletedItems); namespaceBOs.add(namespaceBO); } catch (Exception e) { LOGGER.error("parse namespace error. app id:{}, env:{}, clusterName:{}, namespace:{}", appId, env, clusterName, namespace.getNamespaceName(), e); exceptionNamespaces.add(namespace.getNamespaceName()); } finally { latch.countDown(); } }); } try { latch.await(); } catch (InterruptedException e) { // ignore } if (namespaceBOs.size() != namespaces.size()) { throw new RuntimeException(String.format( "Parse namespaces error, expected: %s, but actual: %s, cannot get those namespaces: %s", namespaces.size(), namespaceBOs.size(), exceptionNamespaces)); } return namespaceBOs.stream().sorted(Comparator.comparing(o -> o.getBaseInfo().getId())) .collect(Collectors.toList()); } public List findNamespaceBOs(String appId, Env env, String clusterName) { return findNamespaceBOs(appId, env, clusterName, true, true); } public List findNamespaces(String appId, Env env, String clusterName) { return namespaceAPI.findNamespaceByCluster(appId, env, clusterName); } /** * the returned content's size is not fixed. so please carefully used. */ public PageDTO findNamespacesByItem(Env env, String itemKey, Pageable pageable) { return namespaceAPI.findByItem(env, itemKey, pageable.getPageNumber(), pageable.getPageSize()); } public List getPublicAppNamespaceAllNamespaces(Env env, String publicNamespaceName, int page, int size) { return namespaceAPI.getPublicAppNamespaceAllNamespaces(env, publicNamespaceName, page, size); } public NamespaceBO loadNamespaceBO(String appId, Env env, String clusterName, String namespaceName, boolean fillItemDetail, boolean includeDeletedItems) { NamespaceDTO namespace = namespaceAPI.loadNamespace(appId, env, clusterName, namespaceName); if (namespace == null) { throw BadRequestException.namespaceNotExists(appId, clusterName, namespaceName); } return transformNamespace2BO(env, namespace, fillItemDetail, includeDeletedItems); } public NamespaceBO loadNamespaceBO(String appId, Env env, String clusterName, String namespaceName) { return loadNamespaceBO(appId, env, clusterName, namespaceName, true, true); } public boolean publicAppNamespaceHasAssociatedNamespace(String publicNamespaceName, Env env) { return getPublicAppNamespaceHasAssociatedNamespace(publicNamespaceName, env) > 0; } public int getPublicAppNamespaceHasAssociatedNamespace(String publicNamespaceName, Env env) { return namespaceAPI.countPublicAppNamespaceAssociatedNamespaces(env, publicNamespaceName); } public NamespaceBO findPublicNamespaceForAssociatedNamespace(Env env, String appId, String clusterName, String namespaceName) { NamespaceDTO namespace = namespaceAPI.findPublicNamespaceForAssociatedNamespace(env, appId, clusterName, namespaceName); return transformNamespace2BO(env, namespace); } public Map> getNamespacesPublishInfo(String appId) { Map> result = Maps.newHashMap(); Set envs = portalConfig.publishTipsSupportedEnvs(); for (Env env : envs) { if (portalSettings.isEnvActive(env)) { result.put(env.toString(), namespaceAPI.getNamespacePublishInfo(env, appId)); } } return result; } private NamespaceBO transformNamespace2BO(Env env, NamespaceDTO namespace, boolean fillItemDetail, boolean includeDeletedItems) { NamespaceBO namespaceBO = new NamespaceBO(); namespaceBO.setBaseInfo(namespace); String appId = namespace.getAppId(); String clusterName = namespace.getClusterName(); String namespaceName = namespace.getNamespaceName(); fillAppNamespaceProperties(namespaceBO); List itemBOs = new LinkedList<>(); namespaceBO.setItems(itemBOs); if (!fillItemDetail) { return namespaceBO; } // latest Release ReleaseDTO latestRelease; Map releaseItems = new HashMap<>(); latestRelease = releaseService.loadLatestRelease(appId, env, clusterName, namespaceName); if (latestRelease != null) { releaseItems = GSON.fromJson(latestRelease.getConfigurations(), GsonType.CONFIG); } // not Release config items List items = itemService.findItems(appId, env, clusterName, namespaceName); additionalUserInfoEnrichService.enrichAdditionalUserInfo(items, BaseDtoUserInfoEnrichedAdapter::new); int modifiedItemCnt = 0; for (ItemDTO itemDTO : items) { ItemBO itemBO = transformItem2BO(itemDTO, releaseItems); if (itemBO.isModified()) { modifiedItemCnt++; } itemBOs.add(itemBO); } if (includeDeletedItems) { // deleted items Map deletedItemDTOs = itemService.findDeletedItems(appId, env, clusterName, namespaceName).stream() .filter(itemDTO -> !StringUtils.isEmpty(itemDTO.getKey())) .collect(Collectors.toMap(ItemDTO::getKey, v -> v, (v1, v2) -> v2)); List deletedItems = parseDeletedItems(items, releaseItems, deletedItemDTOs); itemBOs.addAll(deletedItems); modifiedItemCnt += deletedItems.size(); } namespaceBO.setItemModifiedCnt(modifiedItemCnt); return namespaceBO; } private NamespaceBO transformNamespace2BO(Env env, NamespaceDTO namespace) { return transformNamespace2BO(env, namespace, true, true); } private void fillAppNamespaceProperties(NamespaceBO namespace) { final NamespaceDTO namespaceDTO = namespace.getBaseInfo(); final String appId = namespaceDTO.getAppId(); final String clusterName = namespaceDTO.getClusterName(); final String namespaceName = namespaceDTO.getNamespaceName(); // 先从当前appId下面找,包含私有的和公共的 AppNamespace appNamespace = appNamespaceService.findByAppIdAndName(appId, namespaceName); // 再从公共的app namespace里面找 if (appNamespace == null) { appNamespace = appNamespaceService.findPublicAppNamespace(namespaceName); } final String format; final boolean isPublic; if (appNamespace == null) { // dirty data LOGGER.warn( "Dirty data, cannot find appNamespace by namespaceName [{}], appId = {}, cluster = {}, set it format to {}, make public", namespaceName, appId, clusterName, ConfigFileFormat.Properties.getValue()); format = ConfigFileFormat.Properties.getValue(); isPublic = true; // set to true, because public namespace allowed to delete by user } else { format = appNamespace.getFormat(); isPublic = appNamespace.isPublic(); namespace.setParentAppId(appNamespace.getAppId()); namespace.setComment(appNamespace.getComment()); } namespace.setFormat(format); namespace.setPublic(isPublic); } private List parseDeletedItems(List newItems, Map releaseItems, Map deletedItemDTOs) { Map newItemMap = BeanUtils.mapByKey("key", newItems); // remove comment and blank item map. newItemMap.remove(""); List deletedItems = new LinkedList<>(); for (Map.Entry entry : releaseItems.entrySet()) { String key = entry.getKey(); if (newItemMap.get(key) == null) { ItemBO deletedItem = new ItemBO(); deletedItem.setDeleted(true); ItemDTO deletedItemDto = deletedItemDTOs.computeIfAbsent(key, k -> new ItemDTO()); deletedItemDto.setKey(key); String oldValue = entry.getValue(); deletedItem.setItem(deletedItemDto); deletedItemDto.setValue(oldValue); deletedItem.setModified(true); deletedItem.setOldValue(oldValue); deletedItem.setNewValue(""); deletedItems.add(deletedItem); } } return deletedItems; } private ItemBO transformItem2BO(ItemDTO itemDTO, Map releaseItems) { String key = itemDTO.getKey(); ItemBO itemBO = new ItemBO(); itemBO.setItem(itemDTO); String newValue = itemDTO.getValue(); String oldValue = releaseItems.get(key); // new item or modified if (!StringUtils.isEmpty(key) && (!newValue.equals(oldValue))) { itemBO.setModified(true); itemBO.setNewlyAdded(!releaseItems.containsKey(key)); itemBO.setOldValue(oldValue == null ? "" : oldValue); itemBO.setNewValue(newValue); } return itemBO; } public void assignNamespaceRoleToOperator(String appId, String namespaceName, String operator) { // default assign modify、release namespace role to namespace creator rolePermissionService.assignRoleToUsers( RoleUtils.buildNamespaceRoleName(appId, namespaceName, RoleType.MODIFY_NAMESPACE), Sets.newHashSet(operator), operator); rolePermissionService.assignRoleToUsers( RoleUtils.buildNamespaceRoleName(appId, namespaceName, RoleType.RELEASE_NAMESPACE), Sets.newHashSet(operator), operator); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/PortalDBPropertySource.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.service; import com.google.common.collect.Maps; import com.ctrip.framework.apollo.common.config.RefreshablePropertySource; import com.ctrip.framework.apollo.portal.entity.po.ServerConfig; import com.ctrip.framework.apollo.portal.repository.ServerConfigRepository; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.env.Environment; import org.springframework.core.env.Profiles; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import org.springframework.jdbc.datasource.init.DatabasePopulatorUtils; import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator; import org.springframework.stereotype.Component; import jakarta.annotation.PostConstruct; import javax.sql.DataSource; import java.util.Objects; /** * @author Jason Song(song_s@ctrip.com) */ @Component public class PortalDBPropertySource extends RefreshablePropertySource { private static final Logger logger = LoggerFactory.getLogger(PortalDBPropertySource.class); private final ServerConfigRepository serverConfigRepository; private final DataSource dataSource; private final Environment env; @Autowired public PortalDBPropertySource(final ServerConfigRepository serverConfigRepository, DataSource dataSource, final Environment env) { super("DBConfig", Maps.newConcurrentMap()); this.serverConfigRepository = serverConfigRepository; this.dataSource = dataSource; this.env = env; } @PostConstruct public void runSqlScript() throws Exception { if (env.acceptsProfiles(Profiles.of("h2")) && !env.acceptsProfiles(Profiles.of("assembly"))) { Resource resource = new ClassPathResource("jpa/portaldb.init.h2.sql"); if (resource.exists()) { DatabasePopulatorUtils.execute(new ResourceDatabasePopulator(resource), dataSource); } } } @Override protected void refresh() { Iterable dbConfigs = serverConfigRepository.findAll(); for (ServerConfig config : dbConfigs) { String key = config.getKey(); Object value = config.getValue(); if (this.source.isEmpty()) { logger.info("Load config from DB : {} = {}", key, value); } else if (!Objects.equals(this.source.get(key), value)) { logger.info("Load config from DB : {} = {}. Old value = {}", key, value, this.source.get(key)); } this.source.put(key, value); } } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/ReleaseHistoryService.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.service; import com.ctrip.framework.apollo.common.constants.GsonType; import com.ctrip.framework.apollo.common.dto.PageDTO; import com.ctrip.framework.apollo.common.dto.ReleaseDTO; import com.ctrip.framework.apollo.common.dto.ReleaseHistoryDTO; import com.ctrip.framework.apollo.common.entity.EntityPair; import com.ctrip.framework.apollo.common.utils.BeanUtils; import com.ctrip.framework.apollo.portal.api.AdminServiceAPI.ReleaseHistoryAPI; import com.ctrip.framework.apollo.portal.enricher.adapter.BaseDtoUserInfoEnrichedAdapter; import com.ctrip.framework.apollo.portal.environment.Env; import com.ctrip.framework.apollo.portal.api.AdminServiceAPI; import com.ctrip.framework.apollo.portal.entity.bo.ReleaseHistoryBO; import com.ctrip.framework.apollo.portal.util.RelativeDateFormat; import com.google.gson.Gson; import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.springframework.util.CollectionUtils; @Service public class ReleaseHistoryService { private final static Gson GSON = new Gson(); private final AdminServiceAPI.ReleaseHistoryAPI releaseHistoryAPI; private final ReleaseService releaseService; private final AdditionalUserInfoEnrichService additionalUserInfoEnrichService; public ReleaseHistoryService(final ReleaseHistoryAPI releaseHistoryAPI, final ReleaseService releaseService, AdditionalUserInfoEnrichService additionalUserInfoEnrichService) { this.releaseHistoryAPI = releaseHistoryAPI; this.releaseService = releaseService; this.additionalUserInfoEnrichService = additionalUserInfoEnrichService; } public ReleaseHistoryBO findLatestByReleaseIdAndOperation(Env env, long releaseId, int operation) { PageDTO pageDTO = releaseHistoryAPI.findByReleaseIdAndOperation(env, releaseId, operation, 0, 1); if (pageDTO != null && pageDTO.hasContent()) { ReleaseHistoryDTO releaseHistory = pageDTO.getContent().get(0); ReleaseDTO release = releaseService.findReleaseById(env, releaseHistory.getReleaseId()); return transformReleaseHistoryDTO2BO(releaseHistory, release); } return null; } public ReleaseHistoryBO findLatestByPreviousReleaseIdAndOperation(Env env, long previousReleaseId, int operation) { PageDTO pageDTO = releaseHistoryAPI.findByPreviousReleaseIdAndOperation(env, previousReleaseId, operation, 0, 1); if (pageDTO != null && pageDTO.hasContent()) { ReleaseHistoryDTO releaseHistory = pageDTO.getContent().get(0); ReleaseDTO release = releaseService.findReleaseById(env, releaseHistory.getReleaseId()); return transformReleaseHistoryDTO2BO(releaseHistory, release); } return null; } public List findNamespaceReleaseHistory(String appId, Env env, String clusterName, String namespaceName, int page, int size) { PageDTO result = releaseHistoryAPI.findReleaseHistoriesByNamespace(appId, env, clusterName, namespaceName, page, size); if (result == null || !result.hasContent()) { return Collections.emptyList(); } List content = result.getContent(); Set releaseIds = new HashSet<>(); for (ReleaseHistoryDTO releaseHistoryDTO : content) { long releaseId = releaseHistoryDTO.getReleaseId(); if (releaseId != 0) { releaseIds.add(releaseId); } } List releases = releaseService.findReleaseByIds(env, releaseIds); return transformReleaseHistoryDTO2BO(content, releases); } private List transformReleaseHistoryDTO2BO(List source, List releases) { if (CollectionUtils.isEmpty(source)) { return Collections.emptyList(); } this.additionalUserInfoEnrichService.enrichAdditionalUserInfo(source, BaseDtoUserInfoEnrichedAdapter::new); Map releasesMap = BeanUtils.mapByKey("id", releases); List bos = new ArrayList<>(source.size()); for (ReleaseHistoryDTO dto : source) { ReleaseDTO release = releasesMap.get(dto.getReleaseId()); bos.add(transformReleaseHistoryDTO2BO(dto, release)); } return bos; } private ReleaseHistoryBO transformReleaseHistoryDTO2BO(ReleaseHistoryDTO dto, ReleaseDTO release) { ReleaseHistoryBO bo = new ReleaseHistoryBO(); bo.setId(dto.getId()); bo.setAppId(dto.getAppId()); bo.setClusterName(dto.getClusterName()); bo.setNamespaceName(dto.getNamespaceName()); bo.setBranchName(dto.getBranchName()); bo.setReleaseId(dto.getReleaseId()); bo.setPreviousReleaseId(dto.getPreviousReleaseId()); bo.setOperator(dto.getDataChangeCreatedBy()); bo.setOperatorDisplayName(dto.getDataChangeCreatedByDisplayName()); bo.setOperation(dto.getOperation()); Date releaseTime = dto.getDataChangeLastModifiedTime(); bo.setReleaseTime(releaseTime); bo.setReleaseTimeFormatted(RelativeDateFormat.format(releaseTime)); bo.setOperationContext(dto.getOperationContext()); // set release info setReleaseInfoToReleaseHistoryBO(bo, release); return bo; } private void setReleaseInfoToReleaseHistoryBO(ReleaseHistoryBO bo, ReleaseDTO release) { if (release != null) { bo.setReleaseTitle(release.getName()); bo.setReleaseComment(release.getComment()); bo.setReleaseAbandoned(release.isAbandoned()); Map configuration = GSON.fromJson(release.getConfigurations(), GsonType.CONFIG); List> items = new ArrayList<>(configuration.size()); for (Map.Entry entry : configuration.entrySet()) { EntityPair entityPair = new EntityPair<>(entry.getKey(), entry.getValue()); items.add(entityPair); } bo.setConfiguration(items); } else { bo.setReleaseTitle("no release information"); bo.setConfiguration(null); } } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/ReleaseService.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.service; import com.ctrip.framework.apollo.common.constants.GsonType; import com.ctrip.framework.apollo.common.dto.ItemChangeSets; import com.ctrip.framework.apollo.common.dto.ReleaseDTO; import com.ctrip.framework.apollo.portal.environment.Env; import com.ctrip.framework.apollo.core.utils.StringUtils; import com.ctrip.framework.apollo.portal.api.AdminServiceAPI; import com.ctrip.framework.apollo.portal.constant.TracerEventType; import com.ctrip.framework.apollo.portal.entity.bo.KVEntity; import com.ctrip.framework.apollo.portal.entity.bo.ReleaseBO; import com.ctrip.framework.apollo.portal.entity.model.NamespaceGrayDelReleaseModel; import com.ctrip.framework.apollo.portal.entity.model.NamespaceReleaseModel; import com.ctrip.framework.apollo.portal.entity.vo.ReleaseCompareResult; import com.ctrip.framework.apollo.portal.enums.ChangeType; import com.ctrip.framework.apollo.portal.spi.UserInfoHolder; import com.ctrip.framework.apollo.tracer.Tracer; import com.google.common.base.Objects; import com.google.gson.Gson; import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; @Service public class ReleaseService { private static final Gson GSON = new Gson(); private final UserInfoHolder userInfoHolder; private final AdminServiceAPI.ReleaseAPI releaseAPI; public ReleaseService(final UserInfoHolder userInfoHolder, final AdminServiceAPI.ReleaseAPI releaseAPI) { this.userInfoHolder = userInfoHolder; this.releaseAPI = releaseAPI; } public ReleaseDTO publish(NamespaceReleaseModel model) { Env env = model.getEnv(); boolean isEmergencyPublish = model.isEmergencyPublish(); String appId = model.getAppId(); String clusterName = model.getClusterName(); String namespaceName = model.getNamespaceName(); String releaseBy = StringUtils.isEmpty(model.getReleasedBy()) ? userInfoHolder.getUser().getUserId() : model.getReleasedBy(); ReleaseDTO releaseDTO = releaseAPI.createRelease(appId, env, clusterName, namespaceName, model.getReleaseTitle(), model.getReleaseComment(), releaseBy, isEmergencyPublish); Tracer.logEvent(TracerEventType.RELEASE_NAMESPACE, String.format("%s+%s+%s+%s", appId, env, clusterName, namespaceName)); return releaseDTO; } // gray deletion release public ReleaseDTO publish(NamespaceGrayDelReleaseModel model, String releaseBy) { Env env = model.getEnv(); boolean isEmergencyPublish = model.isEmergencyPublish(); String appId = model.getAppId(); String clusterName = model.getClusterName(); String namespaceName = model.getNamespaceName(); ReleaseDTO releaseDTO = releaseAPI.createGrayDeletionRelease(appId, env, clusterName, namespaceName, model.getReleaseTitle(), model.getReleaseComment(), releaseBy, isEmergencyPublish, model.getGrayDelKeys()); Tracer.logEvent(TracerEventType.RELEASE_NAMESPACE, String.format("%s+%s+%s+%s", appId, env, clusterName, namespaceName)); return releaseDTO; } public ReleaseDTO updateAndPublish(String appId, Env env, String clusterName, String namespaceName, String releaseTitle, String releaseComment, String branchName, boolean isEmergencyPublish, boolean deleteBranch, ItemChangeSets changeSets) { return releaseAPI.updateAndPublish(appId, env, clusterName, namespaceName, releaseTitle, releaseComment, branchName, isEmergencyPublish, deleteBranch, changeSets); } public List findAllReleases(String appId, Env env, String clusterName, String namespaceName, int page, int size) { List releaseDTOs = releaseAPI.findAllReleases(appId, env, clusterName, namespaceName, page, size); if (CollectionUtils.isEmpty(releaseDTOs)) { return Collections.emptyList(); } List releases = new LinkedList<>(); for (ReleaseDTO releaseDTO : releaseDTOs) { ReleaseBO release = new ReleaseBO(); release.setBaseInfo(releaseDTO); Set kvEntities = new LinkedHashSet<>(); Map configurations = GSON.fromJson(releaseDTO.getConfigurations(), GsonType.CONFIG); Set> entries = configurations.entrySet(); for (Map.Entry entry : entries) { kvEntities.add(new KVEntity(entry.getKey(), entry.getValue())); } release.setItems(kvEntities); // 为了减少数据量 releaseDTO.setConfigurations(""); releases.add(release); } return releases; } public List findActiveReleases(String appId, Env env, String clusterName, String namespaceName, int page, int size) { return releaseAPI.findActiveReleases(appId, env, clusterName, namespaceName, page, size); } public ReleaseDTO findReleaseById(Env env, long releaseId) { Set releaseIds = new HashSet<>(1); releaseIds.add(releaseId); List releases = findReleaseByIds(env, releaseIds); if (CollectionUtils.isEmpty(releases)) { return null; } return releases.get(0); } public List findReleaseByIds(Env env, Set releaseIds) { return releaseAPI.findReleaseByIds(env, releaseIds); } public ReleaseDTO loadLatestRelease(String appId, Env env, String clusterName, String namespaceName) { return releaseAPI.loadLatestRelease(appId, env, clusterName, namespaceName); } public void rollback(Env env, long releaseId, String operator) { releaseAPI.rollback(env, releaseId, operator); } public void rollbackTo(Env env, long releaseId, long toReleaseId, String operator) { releaseAPI.rollbackTo(env, releaseId, toReleaseId, operator); } public ReleaseCompareResult compare(Env env, long baseReleaseId, long toCompareReleaseId) { ReleaseDTO baseRelease = null; ReleaseDTO toCompareRelease = null; if (baseReleaseId != 0) { baseRelease = releaseAPI.loadRelease(env, baseReleaseId); } if (toCompareReleaseId != 0) { toCompareRelease = releaseAPI.loadRelease(env, toCompareReleaseId); } return compare(baseRelease, toCompareRelease); } public ReleaseCompareResult compare(ReleaseDTO baseRelease, ReleaseDTO toCompareRelease) { Map baseReleaseConfiguration = baseRelease == null ? new HashMap<>() : GSON.fromJson(baseRelease.getConfigurations(), GsonType.CONFIG); Map toCompareReleaseConfiguration = toCompareRelease == null ? new HashMap<>() : GSON.fromJson(toCompareRelease.getConfigurations(), GsonType.CONFIG); ReleaseCompareResult compareResult = new ReleaseCompareResult(); // added and modified in firstRelease for (Map.Entry entry : baseReleaseConfiguration.entrySet()) { String key = entry.getKey(); String firstValue = entry.getValue(); String secondValue = toCompareReleaseConfiguration.get(key); // added if (secondValue == null) { compareResult.addEntityPair(ChangeType.DELETED, new KVEntity(key, firstValue), new KVEntity(key, null)); } else if (!Objects.equal(firstValue, secondValue)) { compareResult.addEntityPair(ChangeType.MODIFIED, new KVEntity(key, firstValue), new KVEntity(key, secondValue)); } } // deleted in firstRelease for (Map.Entry entry : toCompareReleaseConfiguration.entrySet()) { String key = entry.getKey(); String value = entry.getValue(); if (baseReleaseConfiguration.get(key) == null) { compareResult.addEntityPair(ChangeType.ADDED, new KVEntity(key, ""), new KVEntity(key, value)); } } return compareResult; } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/RoleInitializationService.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.service; import com.ctrip.framework.apollo.common.entity.App; public interface RoleInitializationService { void initAppRoles(App app); void initNamespaceRoles(String appId, String namespaceName, String operator); void initNamespaceEnvRoles(String appId, String namespaceName, String operator); void initNamespaceSpecificEnvRoles(String appId, String namespaceName, String env, String operator); void initCreateAppRole(); void initManageAppMasterRole(String appId, String operator); void initClusterNamespaceRoles(String appId, String env, String clusterName, String operator); } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/RolePermissionService.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.service; import com.ctrip.framework.apollo.portal.entity.bo.UserInfo; import com.ctrip.framework.apollo.portal.entity.po.Permission; import com.ctrip.framework.apollo.portal.entity.po.Role; import java.util.List; import java.util.Set; /** * @author Jason Song(song_s@ctrip.com) */ public interface RolePermissionService { /** * Create role with permissions, note that role name should be unique */ Role createRoleWithPermissions(Role role, Set permissionIds); /** * Assign role to users * * @return the users assigned roles */ Set assignRoleToUsers(String roleName, Set userIds, String operatorUserId); /** * Remove role from users */ void removeRoleFromUsers(String roleName, Set userIds, String operatorUserId); /** * Query users with role */ Set queryUsersWithRole(String roleName); /** * Find role by role name, note that roleName should be unique */ Role findRoleByRoleName(String roleName); /** * Check whether user has the permission */ boolean userHasPermission(String userId, String permissionType, String targetId); /** * Find the user's roles */ List findUserRoles(String userId); boolean isSuperAdmin(String userId); /** * Create permission, note that permissionType + targetId should be unique */ Permission createPermission(Permission permission); /** * Create permissions, note that permissionType + targetId should be unique */ Set createPermissions(Set permissions); /** * delete permissions when delete app. */ void deleteRolePermissionsByAppId(String appId, String operator); /** * delete permissions when delete app namespace. */ void deleteRolePermissionsByAppIdAndNamespace(String appId, String namespaceName, String operator); /** * delete permissions when delete cluster. */ void deleteRolePermissionsByCluster(String appId, String env, String clusterName, String operator); /** * Check if user has any of the given permissions */ boolean hasAnyPermission(String userId, List permissions); } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/ServerConfigService.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.service; import com.ctrip.framework.apollo.common.utils.BeanUtils; import com.ctrip.framework.apollo.portal.api.AdminServiceAPI; import com.ctrip.framework.apollo.portal.api.AdminServiceAPI.ServerConfigAPI; import com.ctrip.framework.apollo.portal.entity.po.ServerConfig; import com.ctrip.framework.apollo.portal.environment.Env; import com.ctrip.framework.apollo.portal.repository.ServerConfigRepository; import com.ctrip.framework.apollo.portal.spi.UserInfoHolder; import com.google.common.collect.Lists; import java.util.List; import java.util.Objects; import jakarta.transaction.Transactional; import org.springframework.stereotype.Service; @Service public class ServerConfigService { private final ServerConfigRepository serverConfigRepository; private final AdminServiceAPI.ServerConfigAPI serverConfigAPI; private final UserInfoHolder userInfoHolder; public ServerConfigService(final ServerConfigRepository serverConfigRepository, ServerConfigAPI serverConfigAPI, UserInfoHolder userInfoHolder) { this.serverConfigRepository = serverConfigRepository; this.serverConfigAPI = serverConfigAPI; this.userInfoHolder = userInfoHolder; } public List findAllPortalDBConfig() { Iterable serverConfigs = serverConfigRepository.findAll(); return Lists.newArrayList(serverConfigs); } public List findAllConfigDBConfig(Env env) { return serverConfigAPI.findAllConfigDBConfig(env); } @Transactional public ServerConfig createOrUpdatePortalDBConfig(ServerConfig serverConfig) { String modifiedBy = userInfoHolder.getUser().getUserId(); ServerConfig storedConfig = serverConfigRepository.findByKey(serverConfig.getKey()); if (Objects.isNull(storedConfig)) {// create serverConfig.setDataChangeCreatedBy(modifiedBy); serverConfig.setDataChangeLastModifiedBy(modifiedBy); serverConfig.setId(0L);// 为空,设置ID 为0,jpa执行新增操作 return serverConfigRepository.save(serverConfig); } // update BeanUtils.copyEntityProperties(serverConfig, storedConfig); storedConfig.setDataChangeLastModifiedBy(modifiedBy); return serverConfigRepository.save(storedConfig); } @Transactional public ServerConfig createOrUpdateConfigDBConfig(Env env, ServerConfig serverConfig) { String modifiedBy = userInfoHolder.getUser().getUserId(); serverConfig.setDataChangeCreatedBy(modifiedBy); serverConfig.setDataChangeLastModifiedBy(modifiedBy); return serverConfigAPI.createOrUpdateConfigDBConfig(env, serverConfig); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/SystemRoleManagerService.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.service; import com.ctrip.framework.apollo.portal.component.config.PortalConfig; import com.ctrip.framework.apollo.portal.constant.PermissionType; import com.ctrip.framework.apollo.portal.util.RoleUtils; import jakarta.annotation.PostConstruct; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; @Service public class SystemRoleManagerService { public static final Logger logger = LoggerFactory.getLogger(SystemRoleManagerService.class); public static final String SYSTEM_PERMISSION_TARGET_ID = "SystemRole"; public static final String CREATE_APPLICATION_ROLE_NAME = RoleUtils.buildCreateApplicationRoleName(PermissionType.CREATE_APPLICATION, SYSTEM_PERMISSION_TARGET_ID); public static final String CREATE_APPLICATION_LIMIT_SWITCH_KEY = "role.create-application.enabled"; public static final String MANAGE_APP_MASTER_LIMIT_SWITCH_KEY = "role.manage-app-master.enabled"; private final RolePermissionService rolePermissionService; private final PortalConfig portalConfig; private final RoleInitializationService roleInitializationService; public SystemRoleManagerService(final RolePermissionService rolePermissionService, final PortalConfig portalConfig, final RoleInitializationService roleInitializationService) { this.rolePermissionService = rolePermissionService; this.portalConfig = portalConfig; this.roleInitializationService = roleInitializationService; } @PostConstruct private void init() { roleInitializationService.initCreateAppRole(); } private boolean isCreateApplicationPermissionEnabled() { return portalConfig.isCreateApplicationPermissionEnabled(); } public boolean isManageAppMasterPermissionEnabled() { return portalConfig.isManageAppMasterPermissionEnabled(); } public boolean hasCreateApplicationPermission(String userId) { if (!isCreateApplicationPermissionEnabled()) { return true; } return rolePermissionService.userHasPermission(userId, PermissionType.CREATE_APPLICATION, SYSTEM_PERMISSION_TARGET_ID); } public boolean hasManageAppMasterPermission(String userId, String appId) { if (!isManageAppMasterPermissionEnabled()) { return true; } return rolePermissionService.userHasPermission(userId, PermissionType.MANAGE_APP_MASTER, appId); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/EmailService.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.spi; import com.ctrip.framework.apollo.portal.entity.bo.Email; public interface EmailService { void send(Email email); } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/LogoutHandler.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.spi; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; public interface LogoutHandler { void logout(HttpServletRequest request, HttpServletResponse response); } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/MQService.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.spi; import com.ctrip.framework.apollo.portal.environment.Env; import com.ctrip.framework.apollo.portal.entity.bo.ReleaseHistoryBO; public interface MQService { void sendPublishMsg(Env env, ReleaseHistoryBO releaseHistory); } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/SsoHeartbeatHandler.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.spi; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; /** * @author Jason Song(song_s@ctrip.com) */ public interface SsoHeartbeatHandler { void doHeartbeat(HttpServletRequest request, HttpServletResponse response); } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/UserInfoHolder.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.spi; import com.ctrip.framework.apollo.portal.entity.bo.UserInfo; /** * Get access to the user's information, * different companies should have a different implementation */ public interface UserInfoHolder { UserInfo getUser(); } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/UserService.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.spi; import com.ctrip.framework.apollo.portal.entity.bo.UserInfo; import java.util.List; /** * @author Jason Song(song_s@ctrip.com) */ public interface UserService { List searchUsers(String keyword, int offset, int limit, boolean includeInactiveUsers); UserInfo findByUserId(String userId); List findByUserIds(List userIds); } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/configuration/AuthConfiguration.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.spi.configuration; import com.ctrip.framework.apollo.common.condition.ConditionalOnMissingProfile; import com.ctrip.framework.apollo.core.utils.StringUtils; import com.ctrip.framework.apollo.portal.repository.AuthorityRepository; import com.ctrip.framework.apollo.portal.repository.UserRepository; import com.ctrip.framework.apollo.portal.spi.LogoutHandler; import com.ctrip.framework.apollo.portal.spi.SsoHeartbeatHandler; import com.ctrip.framework.apollo.portal.spi.UserInfoHolder; import com.ctrip.framework.apollo.portal.spi.UserService; import com.ctrip.framework.apollo.portal.spi.defaultimpl.DefaultLogoutHandler; import com.ctrip.framework.apollo.portal.spi.defaultimpl.DefaultSsoHeartbeatHandler; import com.ctrip.framework.apollo.portal.spi.defaultimpl.DefaultUserInfoHolder; import com.ctrip.framework.apollo.portal.spi.defaultimpl.DefaultUserService; import com.ctrip.framework.apollo.portal.spi.ldap.ApolloLdapAuthenticationProvider; import com.ctrip.framework.apollo.portal.spi.ldap.FilterLdapByGroupUserSearch; import com.ctrip.framework.apollo.portal.spi.ldap.LdapUserService; import com.ctrip.framework.apollo.portal.spi.oidc.ExcludeClientCredentialsClientRegistrationRepository; import com.ctrip.framework.apollo.portal.spi.oidc.OidcAuthenticationSuccessEventListener; import com.ctrip.framework.apollo.portal.spi.oidc.OidcLocalUserService; import com.ctrip.framework.apollo.portal.spi.oidc.OidcLocalUserServiceImpl; import com.ctrip.framework.apollo.portal.spi.oidc.OidcLogoutHandler; import com.ctrip.framework.apollo.portal.spi.oidc.OidcUserInfoHolder; import com.ctrip.framework.apollo.portal.spi.springsecurity.ApolloPasswordEncoderFactory; import com.ctrip.framework.apollo.portal.spi.springsecurity.SpringSecurityUserInfoHolder; import com.ctrip.framework.apollo.portal.spi.springsecurity.SpringSecurityUserService; import java.text.MessageFormat; import java.util.Collections; import jakarta.persistence.EntityManagerFactory; import javax.sql.DataSource; import org.hibernate.dialect.Dialect; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties; import org.springframework.boot.autoconfigure.security.oauth2.resource.OAuth2ResourceServerProperties; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.DependsOn; import org.springframework.context.annotation.Profile; import org.springframework.core.annotation.Order; import org.springframework.core.env.Environment; import org.springframework.ldap.core.ContextSource; import org.springframework.ldap.core.LdapOperations; import org.springframework.ldap.core.LdapTemplate; import org.springframework.ldap.core.support.LdapContextSource; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.ldap.authentication.BindAuthenticator; import org.springframework.security.ldap.authentication.LdapAuthenticationProvider; import org.springframework.security.ldap.search.FilterBasedLdapUserSearch; import org.springframework.security.ldap.userdetails.DefaultLdapAuthoritiesPopulator; import org.springframework.security.oauth2.client.oidc.web.logout.OidcClientInitiatedLogoutSuccessHandler; import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository; import org.springframework.security.provisioning.JdbcUserDetailsManager; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint; @Configuration public class AuthConfiguration { private static final String[] BY_PASS_URLS = {"/prometheus/**", "/metrics/**", "/openapi/**", "/vendor/**", "/styles/**", "/scripts/**", "/views/**", "/img/**", "/i18n/**", "/prefix-path", "/health", "/signin", "/login.html"}; /** * spring.profiles.active = auth */ @Configuration @Profile("auth") static class SpringSecurityAuthAutoConfiguration { @Bean @ConditionalOnMissingBean(SsoHeartbeatHandler.class) public SsoHeartbeatHandler defaultSsoHeartbeatHandler() { return new DefaultSsoHeartbeatHandler(); } @Bean @ConditionalOnMissingBean(PasswordEncoder.class) public static PasswordEncoder passwordEncoder() { return ApolloPasswordEncoderFactory.createDelegatingPasswordEncoder(); } @Bean @ConditionalOnMissingBean(UserInfoHolder.class) public UserInfoHolder springSecurityUserInfoHolder( ObjectProvider userServiceProvider) { return new SpringSecurityUserInfoHolder(userServiceProvider); } @Bean @ConditionalOnMissingBean(LogoutHandler.class) public LogoutHandler logoutHandler() { return new DefaultLogoutHandler(); } @Bean public static JdbcUserDetailsManager jdbcUserDetailsManager(DataSource datasource, EntityManagerFactory entityManagerFactory) throws Exception { char openQuote = '`'; char closeQuote = '`'; try { SessionFactoryImplementor sessionFactory = entityManagerFactory.unwrap(SessionFactoryImplementor.class); Dialect dialect = sessionFactory.getJdbcServices().getDialect(); openQuote = dialect.openQuote(); closeQuote = dialect.closeQuote(); } catch (Throwable ex) { // ignore } JdbcUserDetailsManager jdbcUserDetailsManager = new JdbcUserDetailsManager(datasource); jdbcUserDetailsManager.setUsersByUsernameQuery(MessageFormat.format( "SELECT {0}Username{1}, {0}Password{1}, {0}Enabled{1} FROM {0}Users{1} WHERE {0}Username{1} = ?", openQuote, closeQuote)); jdbcUserDetailsManager.setAuthoritiesByUsernameQuery(MessageFormat.format( "SELECT {0}Username{1}, {0}Authority{1} FROM {0}Authorities{1} WHERE {0}Username{1} = ?", openQuote, closeQuote)); jdbcUserDetailsManager.setUserExistsSql( MessageFormat.format("SELECT {0}Username{1} FROM {0}Users{1} WHERE {0}Username{1} = ?", openQuote, closeQuote)); jdbcUserDetailsManager.setCreateUserSql(MessageFormat.format( "INSERT INTO {0}Users{1} ({0}Username{1}, {0}Password{1}, {0}Enabled{1}) values (?,?,?)", openQuote, closeQuote)); jdbcUserDetailsManager.setUpdateUserSql(MessageFormat.format( "UPDATE {0}Users{1} SET {0}Password{1} = ?, {0}Enabled{1} = ? WHERE {0}Id{1} = (SELECT u.{0}Id{1} FROM (SELECT {0}Id{1} FROM {0}Users{1} WHERE {0}Username{1} = ?) AS u)", openQuote, closeQuote)); jdbcUserDetailsManager.setDeleteUserSql(MessageFormat.format( "DELETE FROM {0}Users{1} WHERE {0}Id{1} = (SELECT u.{0}Id{1} FROM (SELECT {0}Id{1} FROM {0}Users{1} WHERE {0}Username{1} = ?) AS u)", openQuote, closeQuote)); jdbcUserDetailsManager.setCreateAuthoritySql(MessageFormat.format( "INSERT INTO {0}Authorities{1} ({0}Username{1}, {0}Authority{1}) values (?,?)", openQuote, closeQuote)); jdbcUserDetailsManager.setDeleteUserAuthoritiesSql(MessageFormat.format( "DELETE FROM {0}Authorities{1} WHERE {0}Id{1} in (SELECT a.{0}Id{1} FROM (SELECT {0}Id{1} FROM {0}Authorities{1} WHERE {0}Username{1} = ?) AS a)", openQuote, closeQuote)); jdbcUserDetailsManager.setChangePasswordSql(MessageFormat.format( "UPDATE {0}Users{1} SET {0}Password{1} = ? WHERE {0}Id{1} = (SELECT u.{0}Id{1} FROM (SELECT {0}Id{1} FROM {0}Users{1} WHERE {0}Username{1} = ?) AS u)", openQuote, closeQuote)); return jdbcUserDetailsManager; } @Bean @DependsOn("jdbcUserDetailsManager") @ConditionalOnMissingBean(UserService.class) public UserService springSecurityUserService(PasswordEncoder passwordEncoder, UserRepository userRepository, AuthorityRepository authorityRepository) { return new SpringSecurityUserService(passwordEncoder, userRepository, authorityRepository); } } @Profile("auth") @Configuration @EnableWebSecurity @EnableMethodSecurity(prePostEnabled = true) static class SpringSecurityConfigurer { public static final String USER_ROLE = "user"; @Bean @Order(99) public SecurityFilterChain authSecurityFilterChain(HttpSecurity http) throws Exception { http.csrf(csrf -> csrf.disable()); http.headers(headers -> headers.frameOptions(frameOptions -> frameOptions.sameOrigin())); http.authorizeHttpRequests(authorizeHttpRequests -> authorizeHttpRequests .requestMatchers(BY_PASS_URLS).permitAll().anyRequest().hasAnyRole(USER_ROLE)); http.formLogin(formLogin -> formLogin.loginPage("/signin").defaultSuccessUrl("/", true) .permitAll().failureUrl("/signin?#/error")); http.httpBasic(Customizer.withDefaults()); http.logout(logout -> logout.logoutUrl("/user/logout").invalidateHttpSession(true) .clearAuthentication(true).logoutSuccessUrl("/signin?#/logout")); http.exceptionHandling(exceptionHandling -> exceptionHandling .authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/signin"))); return http.build(); } } /** * spring.profiles.active = ldap */ @Configuration @Profile("ldap") @EnableConfigurationProperties({LdapProperties.class, LdapExtendProperties.class}) static class SpringSecurityLDAPAuthAutoConfiguration { private final LdapProperties properties; private final Environment environment; public SpringSecurityLDAPAuthAutoConfiguration(final LdapProperties properties, final Environment environment) { this.properties = properties; this.environment = environment; } @Bean @ConditionalOnMissingBean(SsoHeartbeatHandler.class) public SsoHeartbeatHandler defaultSsoHeartbeatHandler() { return new DefaultSsoHeartbeatHandler(); } @Bean @ConditionalOnMissingBean(UserInfoHolder.class) public UserInfoHolder springSecurityUserInfoHolder( ObjectProvider userServiceProvider) { return new SpringSecurityUserInfoHolder(userServiceProvider); } @Bean @ConditionalOnMissingBean(LogoutHandler.class) public LogoutHandler logoutHandler() { return new DefaultLogoutHandler(); } @Bean @ConditionalOnMissingBean(UserService.class) public UserService springSecurityUserService(LdapTemplate ldapTemplate) { return new LdapUserService(ldapTemplate); } @Bean @ConditionalOnMissingBean public ContextSource ldapContextSource() { LdapContextSource source = new LdapContextSource(); source.setUserDn(this.properties.getUsername()); source.setPassword(this.properties.getPassword()); source.setAnonymousReadOnly(this.properties.getAnonymousReadOnly()); source.setBase(this.properties.getBase()); source.setUrls(this.properties.determineUrls(this.environment)); source.setBaseEnvironmentProperties( Collections.unmodifiableMap(this.properties.getBaseEnvironment())); return source; } @Bean @ConditionalOnMissingBean(LdapOperations.class) public LdapTemplate ldapTemplate(ContextSource contextSource) { LdapTemplate ldapTemplate = new LdapTemplate(contextSource); ldapTemplate.setIgnorePartialResultException(true); return ldapTemplate; } } @Profile("ldap") @Configuration @EnableWebSecurity @EnableMethodSecurity(prePostEnabled = true) static class SpringSecurityLDAPConfigurer { private final LdapProperties ldapProperties; private final LdapContextSource ldapContextSource; private final LdapExtendProperties ldapExtendProperties; public SpringSecurityLDAPConfigurer(final LdapProperties ldapProperties, final LdapContextSource ldapContextSource, final LdapExtendProperties ldapExtendProperties) { this.ldapProperties = ldapProperties; this.ldapContextSource = ldapContextSource; this.ldapExtendProperties = ldapExtendProperties; } @Bean public FilterBasedLdapUserSearch userSearch() { if (ldapExtendProperties.getGroup() == null || StringUtils.isBlank(ldapExtendProperties.getGroup().getGroupSearch())) { FilterBasedLdapUserSearch filterBasedLdapUserSearch = new FilterBasedLdapUserSearch("", ldapProperties.getSearchFilter(), ldapContextSource); filterBasedLdapUserSearch.setSearchSubtree(true); return filterBasedLdapUserSearch; } FilterLdapByGroupUserSearch filterLdapByGroupUserSearch = new FilterLdapByGroupUserSearch(ldapProperties.getBase(), ldapProperties.getSearchFilter(), ldapExtendProperties.getGroup().getGroupBase(), ldapContextSource, ldapExtendProperties.getGroup().getGroupSearch(), ldapExtendProperties.getMapping().getRdnKey(), ldapExtendProperties.getGroup().getGroupMembership(), ldapExtendProperties.getMapping().getLoginId()); filterLdapByGroupUserSearch.setSearchSubtree(true); return filterLdapByGroupUserSearch; } @Bean public LdapAuthenticationProvider ldapAuthProvider() { BindAuthenticator bindAuthenticator = new BindAuthenticator(ldapContextSource); bindAuthenticator.setUserSearch(userSearch()); DefaultLdapAuthoritiesPopulator defaultAuthAutoConfiguration = new DefaultLdapAuthoritiesPopulator(ldapContextSource, null); defaultAuthAutoConfiguration.setIgnorePartialResultException(true); defaultAuthAutoConfiguration.setSearchSubtree(true); // Rewrite the logic of LdapAuthenticationProvider with ApolloLdapAuthenticationProvider, // use userId in LDAP system instead of userId input by user. return new ApolloLdapAuthenticationProvider(bindAuthenticator, defaultAuthAutoConfiguration, ldapExtendProperties); } @Bean @Order(99) public SecurityFilterChain ldapSecurityFilterChain(HttpSecurity http, LdapAuthenticationProvider ldapAuthenticationProvider) throws Exception { http.authenticationProvider(ldapAuthenticationProvider); http.csrf(csrf -> csrf.disable()); http.headers(headers -> headers.frameOptions(frameOptions -> frameOptions.sameOrigin())); http.authorizeHttpRequests(authorizeHttpRequests -> authorizeHttpRequests .requestMatchers(BY_PASS_URLS).permitAll().anyRequest().authenticated()); http.formLogin(formLogin -> formLogin.loginPage("/signin").defaultSuccessUrl("/", true) .permitAll().failureUrl("/signin?#/error")); http.httpBasic(Customizer.withDefaults()); http.logout(logout -> logout.logoutUrl("/user/logout").invalidateHttpSession(true) .clearAuthentication(true).logoutSuccessUrl("/signin?#/logout")); http.exceptionHandling(exceptionHandling -> exceptionHandling .authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/signin"))); return http.build(); } } @Profile("oidc") @EnableConfigurationProperties({OAuth2ClientProperties.class, OAuth2ResourceServerProperties.class, OidcExtendProperties.class}) @Configuration static class OidcAuthAutoConfiguration { @Bean @ConditionalOnMissingBean(SsoHeartbeatHandler.class) public SsoHeartbeatHandler defaultSsoHeartbeatHandler() { return new DefaultSsoHeartbeatHandler(); } @Bean @ConditionalOnMissingBean(UserInfoHolder.class) public UserInfoHolder oidcUserInfoHolder(UserService userService, OidcExtendProperties oidcExtendProperties) { return new OidcUserInfoHolder(userService, oidcExtendProperties); } @Bean @ConditionalOnMissingBean(LogoutHandler.class) public LogoutHandler oidcLogoutHandler() { return new OidcLogoutHandler(); } @Bean @ConditionalOnMissingBean(PasswordEncoder.class) public PasswordEncoder passwordEncoder() { return SpringSecurityAuthAutoConfiguration.passwordEncoder(); } @Bean @ConditionalOnMissingBean(JdbcUserDetailsManager.class) public JdbcUserDetailsManager jdbcUserDetailsManager(DataSource datasource, EntityManagerFactory entityManagerFactory) throws Exception { return SpringSecurityAuthAutoConfiguration.jdbcUserDetailsManager(datasource, entityManagerFactory); } @Bean @ConditionalOnMissingBean(UserService.class) public OidcLocalUserService oidcLocalUserService(JdbcUserDetailsManager userDetailsManager, UserRepository userRepository) { return new OidcLocalUserServiceImpl(userDetailsManager, userRepository); } @Bean public OidcAuthenticationSuccessEventListener oidcAuthenticationSuccessEventListener( OidcLocalUserService oidcLocalUserService, OidcExtendProperties oidcExtendProperties) { return new OidcAuthenticationSuccessEventListener(oidcLocalUserService, oidcExtendProperties); } } @Profile("oidc") @EnableWebSecurity @EnableMethodSecurity(prePostEnabled = true) @Configuration static class OidcWebSecurityConfigurerAdapter { private final InMemoryClientRegistrationRepository clientRegistrationRepository; private final OAuth2ResourceServerProperties oauth2ResourceServerProperties; public OidcWebSecurityConfigurerAdapter( InMemoryClientRegistrationRepository clientRegistrationRepository, OAuth2ResourceServerProperties oauth2ResourceServerProperties) { this.clientRegistrationRepository = clientRegistrationRepository; this.oauth2ResourceServerProperties = oauth2ResourceServerProperties; } @Bean public SecurityFilterChain oidcSecurityFilterChain(HttpSecurity http) throws Exception { http.csrf(csrf -> csrf.disable()); http.authorizeHttpRequests(requests -> requests.requestMatchers(BY_PASS_URLS).permitAll() .anyRequest().authenticated()); http.oauth2Login(configure -> configure .clientRegistrationRepository(new ExcludeClientCredentialsClientRegistrationRepository( this.clientRegistrationRepository))); http.oauth2Client(); http.logout(configure -> { configure.logoutUrl("/user/logout"); OidcClientInitiatedLogoutSuccessHandler logoutSuccessHandler = new OidcClientInitiatedLogoutSuccessHandler(this.clientRegistrationRepository); logoutSuccessHandler.setPostLogoutRedirectUri("{baseUrl}"); configure.logoutSuccessHandler(logoutSuccessHandler); }); // make jwt optional String jwtIssuerUri = this.oauth2ResourceServerProperties.getJwt().getIssuerUri(); if (!StringUtils.isBlank(jwtIssuerUri)) { http.oauth2ResourceServer( oauth2ResourceServer -> oauth2ResourceServer.jwt(Customizer.withDefaults())); } return http.build(); } } /** * default profile */ @Configuration @ConditionalOnMissingProfile({"ctrip", "auth", "ldap", "oidc"}) static class DefaultAuthAutoConfiguration { @Bean @ConditionalOnMissingBean(SsoHeartbeatHandler.class) public SsoHeartbeatHandler defaultSsoHeartbeatHandler() { return new DefaultSsoHeartbeatHandler(); } @Bean @ConditionalOnMissingBean(UserInfoHolder.class) public DefaultUserInfoHolder defaultUserInfoHolder() { return new DefaultUserInfoHolder(); } @Bean @ConditionalOnMissingBean(LogoutHandler.class) public DefaultLogoutHandler logoutHandler() { return new DefaultLogoutHandler(); } @Bean @ConditionalOnMissingBean(UserService.class) public UserService defaultUserService() { return new DefaultUserService(); } } @ConditionalOnMissingProfile({"auth", "ldap", "oidc"}) @Configuration @EnableWebSecurity @EnableMethodSecurity(prePostEnabled = true) static class DefaultWebSecurityConfig { @Bean public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception { http.csrf(csrf -> csrf.disable()); http.headers(headers -> headers.frameOptions(frameOptions -> frameOptions.sameOrigin())); return http.build(); } } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/configuration/AuthFilterConfiguration.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.spi.configuration; import com.ctrip.framework.apollo.openapi.filter.ConsumerAuthenticationFilter; import com.ctrip.framework.apollo.openapi.util.ConsumerAuditUtil; import com.ctrip.framework.apollo.openapi.util.ConsumerAuthUtil; import com.ctrip.framework.apollo.portal.filter.PortalUserSessionFilter; import com.ctrip.framework.apollo.portal.filter.UserTypeResolverFilter; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.env.Environment; @Configuration public class AuthFilterConfiguration { private static final int OPEN_API_AUTH_ORDER = -98; @Bean public FilterRegistrationBean openApiAuthenticationFilter( ConsumerAuthUtil consumerAuthUtil, ConsumerAuditUtil consumerAuditUtil) { FilterRegistrationBean openApiFilter = new FilterRegistrationBean<>(); openApiFilter.setFilter(new ConsumerAuthenticationFilter(consumerAuthUtil, consumerAuditUtil)); openApiFilter.addUrlPatterns("/openapi/*"); openApiFilter.setOrder(OPEN_API_AUTH_ORDER); return openApiFilter; } @Bean public FilterRegistrationBean authTypeResolverFilter() { FilterRegistrationBean authTypeResolverFilter = new FilterRegistrationBean<>(); authTypeResolverFilter.setFilter(new UserTypeResolverFilter()); authTypeResolverFilter.addUrlPatterns("/*"); authTypeResolverFilter.setOrder(OPEN_API_AUTH_ORDER + 1); return authTypeResolverFilter; } /** * Portal user session filter for OpenAPI requests. This filter runs BEFORE * ConsumerAuthenticationFilter to: 1. Allow authenticated Portal users to access OpenAPI 2. * Redirect expired Portal sessions to login page (consistent with Portal endpoints) *

* Order: OPEN_API_AUTH_ORDER - 1 (runs first) */ @Bean public FilterRegistrationBean portalUserSessionFilter( Environment environment) { FilterRegistrationBean filter = new FilterRegistrationBean<>(); filter.setFilter(new PortalUserSessionFilter(environment)); filter.addUrlPatterns("/openapi/*"); filter.setOrder(OPEN_API_AUTH_ORDER - 1); // Run before ConsumerAuthenticationFilter after // springSecurityFilterChain return filter; } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/configuration/EmailConfiguration.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.spi.configuration; import com.ctrip.framework.apollo.portal.spi.EmailService; import com.ctrip.framework.apollo.portal.spi.defaultimpl.DefaultEmailService; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class EmailConfiguration { @Bean @ConditionalOnMissingBean(EmailService.class) public EmailService defaultEmailService() { return new DefaultEmailService(); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/configuration/LdapExtendProperties.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.spi.configuration; import org.springframework.boot.context.properties.ConfigurationProperties; /** * the LdapExtendProperties description. * * @author wuzishu */ @ConfigurationProperties(prefix = "ldap") public class LdapExtendProperties { private LdapMappingProperties mapping; private LdapGroupProperties group; public LdapMappingProperties getMapping() { return mapping; } public void setMapping(LdapMappingProperties mapping) { this.mapping = mapping; } public LdapGroupProperties getGroup() { return group; } public void setGroup(LdapGroupProperties group) { this.group = group; } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/configuration/LdapGroupProperties.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.spi.configuration; /** * the LdapGroupProperties description. * * @author wuzishu */ public class LdapGroupProperties { /** * group search base */ private String groupBase; /** * group search filter */ private String groupSearch; /** * group membership prop */ private String groupMembership; public String getGroupBase() { return groupBase; } public void setGroupBase(String groupBase) { this.groupBase = groupBase; } public String getGroupSearch() { return groupSearch; } public void setGroupSearch(String groupSearch) { this.groupSearch = groupSearch; } public String getGroupMembership() { return groupMembership; } public void setGroupMembership(String groupMembership) { this.groupMembership = groupMembership; } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/configuration/LdapMappingProperties.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.spi.configuration; /** * the LdapMappingProperties description. * * @author wuzishu */ public class LdapMappingProperties { /** * user ldap objectClass */ private String objectClass; /** * user login Id */ private String loginId; /** * user rdn key */ private String rdnKey; /** * user display name */ private String userDisplayName; /** * email */ private String email; public String getObjectClass() { return objectClass; } public void setObjectClass(String objectClass) { this.objectClass = objectClass; } public String getLoginId() { return loginId; } public void setLoginId(String loginId) { this.loginId = loginId; } public String getRdnKey() { return rdnKey; } public void setRdnKey(String rdnKey) { this.rdnKey = rdnKey; } public String getUserDisplayName() { return userDisplayName; } public void setUserDisplayName(String userDisplayName) { this.userDisplayName = userDisplayName; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/configuration/LdapProperties.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.spi.configuration; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.core.env.Environment; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import java.util.HashMap; import java.util.Map; /** * @author xm.lin xm.lin@anxincloud.com * @Description * @date 18-8-9 下午4:36 */ @ConfigurationProperties(prefix = "spring.ldap") public class LdapProperties { private static final int DEFAULT_PORT = 389; /** * LDAP URLs of the server. */ private String[] urls; /** * Base suffix from which all operations should originate. */ private String base; /** * Login username of the server. */ private String username; /** * Login password of the server. */ private String password; /** * Whether read-only operations should use an anonymous environment. */ private boolean anonymousReadOnly; /** * User search filter */ private String searchFilter; /** * LDAP specification settings. */ private final Map baseEnvironment = new HashMap<>(); public String[] getUrls() { return this.urls; } public void setUrls(String[] urls) { this.urls = urls; } public String getBase() { return this.base; } public void setBase(String base) { this.base = base; } public String getUsername() { return this.username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return this.password; } public void setPassword(String password) { this.password = password; } public boolean getAnonymousReadOnly() { return this.anonymousReadOnly; } public void setAnonymousReadOnly(boolean anonymousReadOnly) { this.anonymousReadOnly = anonymousReadOnly; } public String getSearchFilter() { return searchFilter; } public void setSearchFilter(String searchFilter) { this.searchFilter = searchFilter; } public Map getBaseEnvironment() { return this.baseEnvironment; } public String[] determineUrls(Environment environment) { if (ObjectUtils.isEmpty(this.urls)) { return new String[] {"ldap://localhost:" + determinePort(environment)}; } return this.urls; } private int determinePort(Environment environment) { Assert.notNull(environment, "Environment must not be null"); String localPort = environment.getProperty("local.ldap.port"); if (localPort != null) { return Integer.parseInt(localPort); } return DEFAULT_PORT; } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/configuration/MQConfiguration.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.spi.configuration; import com.ctrip.framework.apollo.portal.spi.defaultimpl.DefaultMQService; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class MQConfiguration { @Bean public DefaultMQService mqService() { return new DefaultMQService(); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/configuration/OidcExtendProperties.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.spi.configuration; import com.ctrip.framework.apollo.portal.entity.po.UserPO; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.security.oauth2.core.oidc.StandardClaimNames; @ConfigurationProperties(prefix = "spring.security.oidc") public class OidcExtendProperties { /** * claim name of the userDisplayName {@link UserPO#getUserDisplayName()}. default to * {@link StandardClaimNames#PREFERRED_USERNAME} or {@link StandardClaimNames#NAME} */ private String userDisplayNameClaimName; /** * jwt claim name of the userDisplayName {@link UserPO#getUserDisplayName()} */ private String jwtUserDisplayNameClaimName; public String getUserDisplayNameClaimName() { return userDisplayNameClaimName; } public void setUserDisplayNameClaimName(String userDisplayNameClaimName) { this.userDisplayNameClaimName = userDisplayNameClaimName; } public String getJwtUserDisplayNameClaimName() { return jwtUserDisplayNameClaimName; } public void setJwtUserDisplayNameClaimName(String jwtUserDisplayNameClaimName) { this.jwtUserDisplayNameClaimName = jwtUserDisplayNameClaimName; } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/configuration/RoleConfiguration.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.spi.configuration; import com.ctrip.framework.apollo.openapi.repository.ConsumerRoleRepository; import com.ctrip.framework.apollo.portal.component.config.PortalConfig; import com.ctrip.framework.apollo.portal.repository.PermissionRepository; import com.ctrip.framework.apollo.portal.repository.RolePermissionRepository; import com.ctrip.framework.apollo.portal.repository.RoleRepository; import com.ctrip.framework.apollo.portal.repository.UserRoleRepository; import com.ctrip.framework.apollo.portal.service.RoleInitializationService; import com.ctrip.framework.apollo.portal.service.RolePermissionService; import com.ctrip.framework.apollo.portal.spi.UserService; import com.ctrip.framework.apollo.portal.spi.defaultimpl.DefaultRoleInitializationService; import com.ctrip.framework.apollo.portal.spi.defaultimpl.DefaultRolePermissionService; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * @author Timothy Liu(timothy.liu@cvte.com) */ @Configuration public class RoleConfiguration { private final RoleRepository roleRepository; private final RolePermissionRepository rolePermissionRepository; private final UserRoleRepository userRoleRepository; private final PermissionRepository permissionRepository; private final PortalConfig portalConfig; private final ConsumerRoleRepository consumerRoleRepository; private final UserService userService; public RoleConfiguration(final RoleRepository roleRepository, final RolePermissionRepository rolePermissionRepository, final UserRoleRepository userRoleRepository, final PermissionRepository permissionRepository, final PortalConfig portalConfig, final ConsumerRoleRepository consumerRoleRepository, final UserService userService) { this.roleRepository = roleRepository; this.rolePermissionRepository = rolePermissionRepository; this.userRoleRepository = userRoleRepository; this.permissionRepository = permissionRepository; this.portalConfig = portalConfig; this.consumerRoleRepository = consumerRoleRepository; this.userService = userService; } @Bean public RoleInitializationService roleInitializationService() { return new DefaultRoleInitializationService(rolePermissionService(), portalConfig, permissionRepository); } @Bean public RolePermissionService rolePermissionService() { return new DefaultRolePermissionService(roleRepository, rolePermissionRepository, userRoleRepository, permissionRepository, portalConfig, consumerRoleRepository, userService); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/defaultimpl/DefaultEmailService.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.spi.defaultimpl; import com.ctrip.framework.apollo.portal.component.config.PortalConfig; import com.ctrip.framework.apollo.portal.entity.bo.Email; import com.ctrip.framework.apollo.portal.spi.EmailService; import com.ctrip.framework.apollo.tracer.Tracer; import com.sun.mail.smtp.SMTPTransport; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.Properties; import jakarta.activation.DataHandler; import jakarta.activation.DataSource; import jakarta.annotation.Resource; import jakarta.mail.Message; import jakarta.mail.Session; import jakarta.mail.internet.InternetAddress; import jakarta.mail.internet.MimeMessage; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class DefaultEmailService implements EmailService { private final Logger logger = LoggerFactory.getLogger(DefaultEmailService.class); @Resource private PortalConfig portalConfig; @Override public void send(Email email) { if (!portalConfig.isEmailEnabled()) { return; } SMTPTransport t = null; try { Properties prop = System.getProperties(); Session session = Session.getInstance(prop, null); Message msg = new MimeMessage(session); msg.setFrom(new InternetAddress(email.getSenderEmailAddress())); msg.setRecipients(Message.RecipientType.TO, InternetAddress.parse(email.getRecipientsString(), false)); msg.setSubject(email.getSubject()); msg.setDataHandler(new DataHandler(new HTMLDataSource(email.getBody()))); String host = portalConfig.emailConfigHost(); String user = portalConfig.emailConfigUser(); String password = portalConfig.emailConfigPassword(); t = (SMTPTransport) session.getTransport("smtp"); t.connect(host, user, password); msg.saveChanges(); t.sendMessage(msg, msg.getAllRecipients()); logger.debug("email response: {}", t.getLastServerResponse()); } catch (Exception e) { logger.error("send email failed.", e); Tracer.logError("send email failed.", e); } finally { if (t != null) { try { t.close(); } catch (Exception e) { // nothing } } } } static class HTMLDataSource implements DataSource { private final String html; HTMLDataSource(String htmlString) { html = htmlString; } @Override public InputStream getInputStream() throws IOException { if (html == null) { throw new IOException("html message is null!"); } return new ByteArrayInputStream(html.getBytes()); } @Override public OutputStream getOutputStream() throws IOException { throw new IOException("This DataHandler cannot write HTML"); } @Override public String getContentType() { return "text/html"; } @Override public String getName() { return "HTMLDataSource"; } } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/defaultimpl/DefaultLogoutHandler.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.spi.defaultimpl; import com.ctrip.framework.apollo.portal.spi.LogoutHandler; import java.io.IOException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; public class DefaultLogoutHandler implements LogoutHandler { @Override public void logout(HttpServletRequest request, HttpServletResponse response) { try { response.sendRedirect("/"); } catch (IOException e) { throw new RuntimeException(e); } } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/defaultimpl/DefaultMQService.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.spi.defaultimpl; import com.ctrip.framework.apollo.portal.environment.Env; import com.ctrip.framework.apollo.portal.entity.bo.ReleaseHistoryBO; import com.ctrip.framework.apollo.portal.spi.MQService; public class DefaultMQService implements MQService { @Override public void sendPublishMsg(Env env, ReleaseHistoryBO releaseHistory) { // do nothing } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/defaultimpl/DefaultRoleInitializationService.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.spi.defaultimpl; import com.ctrip.framework.apollo.common.entity.App; import com.ctrip.framework.apollo.common.entity.BaseEntity; import com.ctrip.framework.apollo.core.ConfigConsts; import com.ctrip.framework.apollo.portal.environment.Env; import com.ctrip.framework.apollo.portal.component.config.PortalConfig; import com.ctrip.framework.apollo.portal.constant.PermissionType; import com.ctrip.framework.apollo.portal.constant.RoleType; import com.ctrip.framework.apollo.portal.entity.po.Permission; import com.ctrip.framework.apollo.portal.entity.po.Role; import com.ctrip.framework.apollo.portal.repository.PermissionRepository; import com.ctrip.framework.apollo.portal.service.RoleInitializationService; import com.ctrip.framework.apollo.portal.service.RolePermissionService; import com.ctrip.framework.apollo.portal.service.SystemRoleManagerService; import com.ctrip.framework.apollo.portal.util.RoleUtils; import com.google.common.collect.Sets; import org.springframework.transaction.annotation.Transactional; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; /** * Created by timothy on 2017/4/26. */ public class DefaultRoleInitializationService implements RoleInitializationService { private final RolePermissionService rolePermissionService; private final PortalConfig portalConfig; private final PermissionRepository permissionRepository; public DefaultRoleInitializationService(final RolePermissionService rolePermissionService, final PortalConfig portalConfig, final PermissionRepository permissionRepository) { this.rolePermissionService = rolePermissionService; this.portalConfig = portalConfig; this.permissionRepository = permissionRepository; } @Transactional @Override public void initAppRoles(App app) { String appId = app.getAppId(); String appMasterRoleName = RoleUtils.buildAppMasterRoleName(appId); // has created before if (rolePermissionService.findRoleByRoleName(appMasterRoleName) != null) { return; } String operator = app.getDataChangeCreatedBy(); // create app permissions createAppMasterRole(appId, operator); // create manageAppMaster permission createManageAppMasterRole(appId, operator); // assign master role to user rolePermissionService.assignRoleToUsers(RoleUtils.buildAppMasterRoleName(appId), Sets.newHashSet(app.getOwnerName()), operator); initNamespaceRoles(appId, ConfigConsts.NAMESPACE_APPLICATION, operator); initNamespaceEnvRoles(appId, ConfigConsts.NAMESPACE_APPLICATION, operator); // assign modify、release namespace role to user rolePermissionService.assignRoleToUsers(RoleUtils.buildNamespaceRoleName(appId, ConfigConsts.NAMESPACE_APPLICATION, RoleType.MODIFY_NAMESPACE), Sets.newHashSet(app.getOwnerName()), operator); rolePermissionService.assignRoleToUsers(RoleUtils.buildNamespaceRoleName(appId, ConfigConsts.NAMESPACE_APPLICATION, RoleType.RELEASE_NAMESPACE), Sets.newHashSet(app.getOwnerName()), operator); } @Transactional @Override public void initNamespaceRoles(String appId, String namespaceName, String operator) { String modifyNamespaceRoleName = RoleUtils.buildModifyNamespaceRoleName(appId, namespaceName); if (rolePermissionService.findRoleByRoleName(modifyNamespaceRoleName) == null) { createNamespaceRole(appId, namespaceName, PermissionType.MODIFY_NAMESPACE, modifyNamespaceRoleName, operator); } String releaseNamespaceRoleName = RoleUtils.buildReleaseNamespaceRoleName(appId, namespaceName); if (rolePermissionService.findRoleByRoleName(releaseNamespaceRoleName) == null) { createNamespaceRole(appId, namespaceName, PermissionType.RELEASE_NAMESPACE, releaseNamespaceRoleName, operator); } } @Transactional @Override public void initNamespaceEnvRoles(String appId, String namespaceName, String operator) { List portalEnvs = portalConfig.portalSupportedEnvs(); for (Env env : portalEnvs) { initNamespaceSpecificEnvRoles(appId, namespaceName, env.toString(), operator); } } @Transactional @Override public void initNamespaceSpecificEnvRoles(String appId, String namespaceName, String env, String operator) { String modifyNamespaceEnvRoleName = RoleUtils.buildModifyNamespaceRoleName(appId, namespaceName, env); if (rolePermissionService.findRoleByRoleName(modifyNamespaceEnvRoleName) == null) { createNamespaceEnvRole(appId, namespaceName, PermissionType.MODIFY_NAMESPACE, env, modifyNamespaceEnvRoleName, operator); } String releaseNamespaceEnvRoleName = RoleUtils.buildReleaseNamespaceRoleName(appId, namespaceName, env); if (rolePermissionService.findRoleByRoleName(releaseNamespaceEnvRoleName) == null) { createNamespaceEnvRole(appId, namespaceName, PermissionType.RELEASE_NAMESPACE, env, releaseNamespaceEnvRoleName, operator); } } @Transactional @Override public void initCreateAppRole() { if (rolePermissionService .findRoleByRoleName(SystemRoleManagerService.CREATE_APPLICATION_ROLE_NAME) != null) { return; } Permission createAppPermission = permissionRepository.findTopByPermissionTypeAndTargetId( PermissionType.CREATE_APPLICATION, SystemRoleManagerService.SYSTEM_PERMISSION_TARGET_ID); if (createAppPermission == null) { // create application permission init createAppPermission = createPermission(SystemRoleManagerService.SYSTEM_PERMISSION_TARGET_ID, PermissionType.CREATE_APPLICATION, "apollo"); rolePermissionService.createPermission(createAppPermission); } // create application role init Role createAppRole = createRole(SystemRoleManagerService.CREATE_APPLICATION_ROLE_NAME, "apollo"); rolePermissionService.createRoleWithPermissions(createAppRole, Sets.newHashSet(createAppPermission.getId())); } @Transactional public void createManageAppMasterRole(String appId, String operator) { Permission permission = createPermission(appId, PermissionType.MANAGE_APP_MASTER, operator); rolePermissionService.createPermission(permission); Role role = createRole(RoleUtils.buildAppRoleName(appId, PermissionType.MANAGE_APP_MASTER), operator); Set permissionIds = new HashSet<>(); permissionIds.add(permission.getId()); rolePermissionService.createRoleWithPermissions(role, permissionIds); } // fix historical data @Transactional @Override public void initManageAppMasterRole(String appId, String operator) { String manageAppMasterRoleName = RoleUtils.buildAppRoleName(appId, PermissionType.MANAGE_APP_MASTER); if (rolePermissionService.findRoleByRoleName(manageAppMasterRoleName) != null) { return; } synchronized (DefaultRoleInitializationService.class) { createManageAppMasterRole(appId, operator); } } @Transactional @Override public void initClusterNamespaceRoles(String appId, String env, String clusterName, String operator) { String modifyNamespacesInClusterRoleName = RoleUtils.buildModifyNamespacesInClusterRoleName(appId, env, clusterName); if (rolePermissionService.findRoleByRoleName(modifyNamespacesInClusterRoleName) == null) { createClusterRole(appId, env, clusterName, PermissionType.MODIFY_NAMESPACES_IN_CLUSTER, modifyNamespacesInClusterRoleName, operator); } String releaseNamespacesInClusterRoleName = RoleUtils.buildReleaseNamespacesInClusterRoleName(appId, env, clusterName); if (rolePermissionService.findRoleByRoleName(releaseNamespacesInClusterRoleName) == null) { createClusterRole(appId, env, clusterName, PermissionType.RELEASE_NAMESPACES_IN_CLUSTER, releaseNamespacesInClusterRoleName, operator); } } private void createAppMasterRole(String appId, String operator) { Set appPermissions = Stream .of(PermissionType.CREATE_CLUSTER, PermissionType.CREATE_NAMESPACE, PermissionType.ASSIGN_ROLE) .map(permissionType -> createPermission(appId, permissionType, operator)) .collect(Collectors.toSet()); Set createdAppPermissions = rolePermissionService.createPermissions(appPermissions); Set appPermissionIds = createdAppPermissions.stream().map(BaseEntity::getId).collect(Collectors.toSet()); // create app master role Role appMasterRole = createRole(RoleUtils.buildAppMasterRoleName(appId), operator); rolePermissionService.createRoleWithPermissions(appMasterRole, appPermissionIds); } private Permission createPermission(String targetId, String permissionType, String operator) { Permission permission = new Permission(); permission.setPermissionType(permissionType); permission.setTargetId(targetId); permission.setDataChangeCreatedBy(operator); permission.setDataChangeLastModifiedBy(operator); return permission; } private Role createRole(String roleName, String operator) { Role role = new Role(); role.setRoleName(roleName); role.setDataChangeCreatedBy(operator); role.setDataChangeLastModifiedBy(operator); return role; } private void createNamespaceRole(String appId, String namespaceName, String permissionType, String roleName, String operator) { Permission permission = createPermission(RoleUtils.buildNamespaceTargetId(appId, namespaceName), permissionType, operator); Permission createdPermission = rolePermissionService.createPermission(permission); Role role = createRole(roleName, operator); rolePermissionService.createRoleWithPermissions(role, Sets.newHashSet(createdPermission.getId())); } private void createNamespaceEnvRole(String appId, String namespaceName, String permissionType, String env, String roleName, String operator) { Permission permission = createPermission( RoleUtils.buildNamespaceTargetId(appId, namespaceName, env), permissionType, operator); Permission createdPermission = rolePermissionService.createPermission(permission); Role role = createRole(roleName, operator); rolePermissionService.createRoleWithPermissions(role, Sets.newHashSet(createdPermission.getId())); } private void createClusterRole(String appId, String env, String clusterName, String permissionType, String roleName, String operator) { Permission permission = createPermission( RoleUtils.buildClusterTargetId(appId, env, clusterName), permissionType, operator); Permission createdPermission = rolePermissionService.createPermission(permission); Role role = createRole(roleName, operator); rolePermissionService.createRoleWithPermissions(role, Sets.newHashSet(createdPermission.getId())); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/defaultimpl/DefaultRolePermissionService.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.spi.defaultimpl; import com.ctrip.framework.apollo.audit.annotation.ApolloAuditLog; import com.ctrip.framework.apollo.audit.annotation.ApolloAuditLogDataInfluence; import com.ctrip.framework.apollo.audit.annotation.ApolloAuditLogDataInfluenceTable; import com.ctrip.framework.apollo.audit.annotation.ApolloAuditLogDataInfluenceTableField; import com.ctrip.framework.apollo.audit.annotation.OpType; import com.ctrip.framework.apollo.openapi.repository.ConsumerRoleRepository; import com.ctrip.framework.apollo.portal.component.config.PortalConfig; import com.ctrip.framework.apollo.portal.entity.bo.UserInfo; import com.ctrip.framework.apollo.portal.entity.po.Permission; import com.ctrip.framework.apollo.portal.entity.po.Role; import com.ctrip.framework.apollo.portal.entity.po.RolePermission; import com.ctrip.framework.apollo.portal.entity.po.UserRole; import com.ctrip.framework.apollo.portal.repository.PermissionRepository; import com.ctrip.framework.apollo.portal.repository.RolePermissionRepository; import com.ctrip.framework.apollo.portal.repository.RoleRepository; import com.ctrip.framework.apollo.portal.repository.UserRoleRepository; import com.ctrip.framework.apollo.portal.service.RolePermissionService; import com.ctrip.framework.apollo.portal.spi.UserService; import com.google.common.base.Preconditions; import com.google.common.collect.HashMultimap; import com.google.common.collect.Lists; import com.google.common.collect.Multimap; import com.google.common.collect.Sets; import java.util.Comparator; import java.util.LinkedHashSet; import org.springframework.data.jpa.repository.query.EscapeCharacter; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.CollectionUtils; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.StreamSupport; /** * Created by timothy on 2017/4/26. */ public class DefaultRolePermissionService implements RolePermissionService { private final RoleRepository roleRepository; private final RolePermissionRepository rolePermissionRepository; private final UserRoleRepository userRoleRepository; private final PermissionRepository permissionRepository; private final PortalConfig portalConfig; private final ConsumerRoleRepository consumerRoleRepository; private final UserService userService; public DefaultRolePermissionService(final RoleRepository roleRepository, final RolePermissionRepository rolePermissionRepository, final UserRoleRepository userRoleRepository, final PermissionRepository permissionRepository, final PortalConfig portalConfig, final ConsumerRoleRepository consumerRoleRepository, final UserService userService) { this.roleRepository = roleRepository; this.rolePermissionRepository = rolePermissionRepository; this.userRoleRepository = userRoleRepository; this.permissionRepository = permissionRepository; this.portalConfig = portalConfig; this.consumerRoleRepository = consumerRoleRepository; this.userService = userService; } /** * Create role with permissions, note that role name should be unique */ @Transactional @Override public Role createRoleWithPermissions(Role role, Set permissionIds) { Role current = findRoleByRoleName(role.getRoleName()); Preconditions.checkState(current == null, "Role %s already exists!", role.getRoleName()); Role createdRole = roleRepository.save(role); if (!CollectionUtils.isEmpty(permissionIds)) { Iterable rolePermissions = permissionIds.stream().map(permissionId -> { RolePermission rolePermission = new RolePermission(); rolePermission.setRoleId(createdRole.getId()); rolePermission.setPermissionId(permissionId); rolePermission.setDataChangeCreatedBy(createdRole.getDataChangeCreatedBy()); rolePermission.setDataChangeLastModifiedBy(createdRole.getDataChangeLastModifiedBy()); return rolePermission; }).collect(Collectors.toList()); rolePermissionRepository.saveAll(rolePermissions); } return createdRole; } /** * Assign role to users * * @return the users assigned roles */ @Transactional @ApolloAuditLog(type = OpType.CREATE, name = "Auth.assignRoleToUsers") @Override public Set assignRoleToUsers(String roleName, Set userIds, String operatorUserId) { Role role = findRoleByRoleName(roleName); Preconditions.checkState(role != null, "Role %s doesn't exist!", roleName); List existedUserRoles = userRoleRepository.findByUserIdInAndRoleId(userIds, role.getId()); Set existedUserIds = existedUserRoles.stream().map(UserRole::getUserId).collect(Collectors.toSet()); Set toAssignUserIds = Sets.difference(userIds, existedUserIds); Iterable toCreate = toAssignUserIds.stream().map(userId -> { UserRole userRole = new UserRole(); userRole.setRoleId(role.getId()); userRole.setUserId(userId); userRole.setDataChangeCreatedBy(operatorUserId); userRole.setDataChangeLastModifiedBy(operatorUserId); return userRole; }).collect(Collectors.toList()); userRoleRepository.saveAll(toCreate); return toAssignUserIds; } /** * Remove role from users */ @Transactional @ApolloAuditLog(type = OpType.DELETE, name = "Auth.removeRoleFromUsers") @Override public void removeRoleFromUsers( @ApolloAuditLogDataInfluence @ApolloAuditLogDataInfluenceTable(tableName = "UserRole") @ApolloAuditLogDataInfluenceTableField(fieldName = "RoleName") String roleName, @ApolloAuditLogDataInfluence @ApolloAuditLogDataInfluenceTable(tableName = "UserRole") @ApolloAuditLogDataInfluenceTableField(fieldName = "UserId") Set userIds, String operatorUserId) { Role role = findRoleByRoleName(roleName); Preconditions.checkState(role != null, "Role %s doesn't exist!", roleName); List existedUserRoles = userRoleRepository.findByUserIdInAndRoleId(userIds, role.getId()); for (UserRole userRole : existedUserRoles) { userRole.setDeleted(true); userRole.setDataChangeLastModifiedTime(new Date()); userRole.setDataChangeLastModifiedBy(operatorUserId); } userRoleRepository.saveAll(existedUserRoles); } /** * Query users with role */ @Override public Set queryUsersWithRole(String roleName) { Role role = findRoleByRoleName(roleName); if (role == null) { return Collections.emptySet(); } List userRoles = userRoleRepository.findByRoleId(role.getId()); List userInfos = userService .findByUserIds(userRoles.stream().map(UserRole::getUserId).collect(Collectors.toList())); if (CollectionUtils.isEmpty(userInfos)) { return Collections.emptySet(); } return userInfos.stream().sorted(Comparator.comparing(UserInfo::getUserId)) .collect(Collectors.toCollection(LinkedHashSet::new)); } /** * Find role by role name, note that roleName should be unique */ @Override public Role findRoleByRoleName(String roleName) { return roleRepository.findTopByRoleName(roleName); } /** * Check whether user has the permission */ @Override public boolean userHasPermission(String userId, String permissionType, String targetId) { Permission permission = permissionRepository.findTopByPermissionTypeAndTargetId(permissionType, targetId); if (permission == null) { return false; } if (isSuperAdmin(userId)) { return true; } List userRoles = userRoleRepository.findByUserId(userId); if (CollectionUtils.isEmpty(userRoles)) { return false; } Set roleIds = userRoles.stream().map(UserRole::getRoleId).collect(Collectors.toSet()); List rolePermissions = rolePermissionRepository.findByRoleIdIn(roleIds); if (CollectionUtils.isEmpty(rolePermissions)) { return false; } for (RolePermission rolePermission : rolePermissions) { if (rolePermission.getPermissionId() == permission.getId()) { return true; } } return false; } @Override public List findUserRoles(String userId) { List userRoles = userRoleRepository.findByUserId(userId); if (CollectionUtils.isEmpty(userRoles)) { return Collections.emptyList(); } Set roleIds = userRoles.stream().map(UserRole::getRoleId).collect(Collectors.toSet()); return Lists.newLinkedList(roleRepository.findAllById(roleIds)); } @Override public boolean isSuperAdmin(String userId) { return portalConfig.superAdmins().contains(userId); } /** * Create permission, note that permissionType + targetId should be unique */ @Transactional @Override public Permission createPermission(Permission permission) { String permissionType = permission.getPermissionType(); String targetId = permission.getTargetId(); Permission current = permissionRepository.findTopByPermissionTypeAndTargetId(permissionType, targetId); Preconditions.checkState(current == null, "Permission with permissionType %s targetId %s already exists!", permissionType, targetId); return permissionRepository.save(permission); } /** * Create permissions, note that permissionType + targetId should be unique */ @Transactional @Override public Set createPermissions(Set permissions) { Multimap targetIdPermissionTypes = HashMultimap.create(); for (Permission permission : permissions) { targetIdPermissionTypes.put(permission.getTargetId(), permission.getPermissionType()); } for (String targetId : targetIdPermissionTypes.keySet()) { Collection permissionTypes = targetIdPermissionTypes.get(targetId); List current = permissionRepository.findByPermissionTypeInAndTargetId(permissionTypes, targetId); Preconditions.checkState(CollectionUtils.isEmpty(current), "Permission with permissionType %s targetId %s already exists!", permissionTypes, targetId); } Iterable results = permissionRepository.saveAll(permissions); return StreamSupport.stream(results.spliterator(), false).collect(Collectors.toSet()); } @Transactional @Override public void deleteRolePermissionsByAppId(String appId, String operator) { appId = EscapeCharacter.DEFAULT.escape(appId); List permissionIds = permissionRepository.findPermissionIdsByAppId(appId); if (!permissionIds.isEmpty()) { // 1. delete Permission permissionRepository.batchDelete(permissionIds, operator); // 2. delete Role Permission rolePermissionRepository.batchDeleteByPermissionIds(permissionIds, operator); } List roleIds = roleRepository.findRoleIdsByAppId(appId); if (!roleIds.isEmpty()) { // 3. delete Role roleRepository.batchDelete(roleIds, operator); // 4. delete User Role userRoleRepository.batchDeleteByRoleIds(roleIds, operator); // 5. delete Consumer Role consumerRoleRepository.batchDeleteByRoleIds(roleIds, operator); } } @Transactional @Override public void deleteRolePermissionsByAppIdAndNamespace(String appId, String namespaceName, String operator) { appId = EscapeCharacter.DEFAULT.escape(appId); List permissionIds = permissionRepository.findPermissionIdsByAppIdAndNamespace(appId, namespaceName); if (!permissionIds.isEmpty()) { // 1. delete Permission permissionRepository.batchDelete(permissionIds, operator); // 2. delete Role Permission rolePermissionRepository.batchDeleteByPermissionIds(permissionIds, operator); } List roleIds = roleRepository.findRoleIdsByAppIdAndNamespace(appId, namespaceName); if (!roleIds.isEmpty()) { // 3. delete Role roleRepository.batchDelete(roleIds, operator); // 4. delete User Role userRoleRepository.batchDeleteByRoleIds(roleIds, operator); // 5. delete Consumer Role consumerRoleRepository.batchDeleteByRoleIds(roleIds, operator); } } @Transactional @Override public void deleteRolePermissionsByCluster(String appId, String env, String clusterName, String operator) { appId = EscapeCharacter.DEFAULT.escape(appId); List permissionIds = permissionRepository.findPermissionIdsByAppIdAndEnvAndCluster(appId, env, clusterName); if (!permissionIds.isEmpty()) { // 1. delete Permission permissionRepository.batchDelete(permissionIds, operator); // 2. delete Role Permission rolePermissionRepository.batchDeleteByPermissionIds(permissionIds, operator); } List roleIds = roleRepository.findRoleIdsByCluster(appId, env, clusterName); if (!roleIds.isEmpty()) { // 3. delete Role roleRepository.batchDelete(roleIds, operator); // 4. delete User Role userRoleRepository.batchDeleteByRoleIds(roleIds, operator); // 5. delete Consumer Role consumerRoleRepository.batchDeleteByRoleIds(roleIds, operator); } } @Override public boolean hasAnyPermission(String userId, List permissions) { if (CollectionUtils.isEmpty(permissions)) { return false; } if (isSuperAdmin(userId)) { return true; } List userPermissions = permissionRepository.findUserPermissions(userId); if (CollectionUtils.isEmpty(userPermissions)) { return false; } Set userPermissionSet = Sets.newHashSet(userPermissions); return permissions.stream().anyMatch(userPermissionSet::contains); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/defaultimpl/DefaultSsoHeartbeatHandler.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.spi.defaultimpl; import com.ctrip.framework.apollo.portal.spi.SsoHeartbeatHandler; import java.io.IOException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; /** * @author Jason Song(song_s@ctrip.com) */ public class DefaultSsoHeartbeatHandler implements SsoHeartbeatHandler { @Override public void doHeartbeat(HttpServletRequest request, HttpServletResponse response) { try { response.sendRedirect("default_sso_heartbeat.html"); } catch (IOException ignore) { } } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/defaultimpl/DefaultUserInfoHolder.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.spi.defaultimpl; import com.ctrip.framework.apollo.portal.spi.UserInfoHolder; import com.ctrip.framework.apollo.portal.entity.bo.UserInfo; /** * 不是ctrip的公司默认提供一个假用户 */ public class DefaultUserInfoHolder implements UserInfoHolder { public DefaultUserInfoHolder() { } @Override public UserInfo getUser() { UserInfo userInfo = new UserInfo(); userInfo.setUserId("apollo"); userInfo.setName("apollo"); return userInfo; } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/defaultimpl/DefaultUserService.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.spi.defaultimpl; import com.google.common.collect.Lists; import com.ctrip.framework.apollo.portal.entity.bo.UserInfo; import com.ctrip.framework.apollo.portal.spi.UserService; import java.util.Collections; import java.util.List; import java.util.Objects; import org.springframework.util.CollectionUtils; /** * @author Jason Song(song_s@ctrip.com) */ public class DefaultUserService implements UserService { private static final String DEFAULT_USER_ID = "apollo"; @Override public List searchUsers(String keyword, int offset, int limit, boolean includeInactiveUsers) { return Collections.singletonList(assembleDefaultUser()); } @Override public UserInfo findByUserId(String userId) { if (Objects.equals(userId, DEFAULT_USER_ID)) { return assembleDefaultUser(); } return null; } @Override public List findByUserIds(List userIds) { if (CollectionUtils.isEmpty(userIds)) { return Collections.emptyList(); } if (userIds.contains(DEFAULT_USER_ID)) { return Lists.newArrayList(assembleDefaultUser()); } return Collections.emptyList(); } private UserInfo assembleDefaultUser() { UserInfo defaultUser = new UserInfo(); defaultUser.setUserId(DEFAULT_USER_ID); defaultUser.setName(DEFAULT_USER_ID); defaultUser.setEmail("apollo@acme.com"); return defaultUser; } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/ldap/ApolloLdapAuthenticationProvider.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.spi.ldap; import com.ctrip.framework.apollo.portal.spi.configuration.LdapExtendProperties; import org.springframework.ldap.core.DirContextOperations; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.ldap.authentication.LdapAuthenticationProvider; import org.springframework.security.ldap.authentication.LdapAuthenticator; import org.springframework.security.ldap.userdetails.LdapAuthoritiesPopulator; import org.springframework.util.Assert; import org.springframework.util.StringUtils; /** * Inherited from LdapAuthenticationProvider and rewritten the authenticate method, * modified the userId used by the previous user input, * changed to use the userId in the LDAP system. * * @author wuzishu */ public class ApolloLdapAuthenticationProvider extends LdapAuthenticationProvider { private LdapExtendProperties properties; public ApolloLdapAuthenticationProvider(LdapAuthenticator authenticator, LdapAuthoritiesPopulator authoritiesPopulator) { super(authenticator, authoritiesPopulator); } public ApolloLdapAuthenticationProvider(LdapAuthenticator authenticator) { super(authenticator); } public ApolloLdapAuthenticationProvider(LdapAuthenticator authenticator, LdapAuthoritiesPopulator authoritiesPopulator, LdapExtendProperties properties) { super(authenticator, authoritiesPopulator); this.properties = properties; } public ApolloLdapAuthenticationProvider(LdapAuthenticator authenticator, LdapExtendProperties properties) { super(authenticator); this.properties = properties; } @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, this.messages.getMessage("LdapAuthenticationProvider.onlySupports", "Only UsernamePasswordAuthenticationToken is supported")); UsernamePasswordAuthenticationToken userToken = (UsernamePasswordAuthenticationToken) authentication; String username = userToken.getName(); String password = (String) authentication.getCredentials(); if (this.logger.isDebugEnabled()) { this.logger.debug("Processing authentication request for user: " + username); } if (!StringUtils.hasLength(username)) { throw new BadCredentialsException( this.messages.getMessage("LdapAuthenticationProvider.emptyUsername", "Empty Username")); } if (!StringUtils.hasLength(password)) { throw new BadCredentialsException(this.messages .getMessage("AbstractLdapAuthenticationProvider.emptyPassword", "Empty Password")); } Assert.notNull(password, "Null password was supplied in authentication token"); DirContextOperations userData = this.doAuthentication(userToken); String loginId = userData.getStringAttribute(properties.getMapping().getLoginId()); UserDetails user = this.userDetailsContextMapper.mapUserFromContext(userData, loginId, this.loadUserAuthorities(userData, loginId, (String) authentication.getCredentials())); return this.createSuccessfulAuthentication(userToken, user); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/ldap/FilterLdapByGroupUserSearch.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.spi.ldap; import static org.springframework.ldap.query.LdapQueryBuilder.query; import javax.naming.Name; import javax.naming.directory.SearchControls; import javax.naming.ldap.LdapName; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.ldap.core.DirContextAdapter; import org.springframework.ldap.core.DirContextOperations; import org.springframework.ldap.core.support.BaseLdapPathContextSource; import org.springframework.ldap.support.LdapUtils; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.ldap.SpringSecurityLdapTemplate; import org.springframework.security.ldap.search.FilterBasedLdapUserSearch; /** * the FilterLdapByGroupUserSearch description. * * @author wuzishu */ public class FilterLdapByGroupUserSearch extends FilterBasedLdapUserSearch { private static final Logger logger = LoggerFactory.getLogger(FilterLdapByGroupUserSearch.class); private static final String MEMBER_UID_ATTR_NAME = "memberUid"; private final String searchBase; private final String groupBase; private final String groupSearch; private final String rdnKey; private final String groupMembershipAttrName; private final String loginIdAttrName; private final SearchControls searchControls = new SearchControls(); private final BaseLdapPathContextSource contextSource; public FilterLdapByGroupUserSearch(String searchBase, String searchFilter, String groupBase, BaseLdapPathContextSource contextSource, String groupSearch, String rdnKey, String groupMembershipAttrName, String loginIdAttrName) { super(searchBase, searchFilter, contextSource); this.searchBase = searchBase; this.groupBase = groupBase; this.groupSearch = groupSearch; this.contextSource = contextSource; this.rdnKey = rdnKey; this.groupMembershipAttrName = groupMembershipAttrName; this.loginIdAttrName = loginIdAttrName; } private Name searchUserById(String userId) { SpringSecurityLdapTemplate template = new SpringSecurityLdapTemplate(this.contextSource); template.setSearchControls(searchControls); return template.searchForObject(query().where(this.loginIdAttrName).is(userId), ctx -> ((DirContextAdapter) ctx).getDn()); } @Override public DirContextOperations searchForUser(String username) { if (logger.isDebugEnabled()) { logger.debug("Searching for user '{}', with user search {}", username, this); } SpringSecurityLdapTemplate template = new SpringSecurityLdapTemplate(this.contextSource); template.setSearchControls(searchControls); return template.searchForObject(groupBase, groupSearch, ctx -> { if (!MEMBER_UID_ATTR_NAME.equals(groupMembershipAttrName)) { String[] members = ((DirContextAdapter) ctx).getStringAttributes(groupMembershipAttrName); for (String item : members) { LdapName memberDn = LdapUtils.newLdapName(item); LdapName memberRdn = LdapUtils.removeFirst(memberDn, LdapUtils.newLdapName(searchBase)); String rdnValue = LdapUtils.getValue(memberRdn, rdnKey).toString(); if (rdnValue.equalsIgnoreCase(username)) { return new DirContextAdapter(memberRdn.toString()); } } throw new UsernameNotFoundException("User " + username + " not found in directory."); } String[] memberUids = ((DirContextAdapter) ctx).getStringAttributes(groupMembershipAttrName); for (String memberUid : memberUids) { if (memberUid.equalsIgnoreCase(username)) { Name name = searchUserById(memberUid); LdapName ldapName = LdapUtils.newLdapName(name); LdapName ldapRdn = LdapUtils.removeFirst(ldapName, LdapUtils.newLdapName(searchBase)); return new DirContextAdapter(ldapRdn); } } throw new UsernameNotFoundException("User " + username + " not found in directory."); }); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/ldap/LdapUserService.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.spi.ldap; import static java.util.stream.Collectors.collectingAndThen; import static java.util.stream.Collectors.toCollection; import static org.springframework.ldap.query.LdapQueryBuilder.query; import com.ctrip.framework.apollo.portal.entity.bo.UserInfo; import com.ctrip.framework.apollo.portal.spi.UserService; import com.google.common.base.Strings; import com.google.common.collect.Sets; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Set; import java.util.TreeSet; import javax.naming.directory.Attribute; import javax.naming.ldap.LdapName; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.ldap.core.AttributesMapper; import org.springframework.ldap.core.ContextMapper; import org.springframework.ldap.core.DirContextAdapter; import org.springframework.ldap.core.LdapTemplate; import org.springframework.ldap.query.ContainerCriteria; import org.springframework.ldap.query.SearchScope; import org.springframework.ldap.support.LdapUtils; import org.springframework.util.CollectionUtils; /** * Ldap user spi service * * Support OpenLdap,ApacheDS,ActiveDirectory use {@link LdapTemplate} as underlying implementation * * @author xm.lin xm.lin@anxincloud.com * @author idefav * @Description ldap user service * @date 18-8-9 下午4:42 */ public class LdapUserService implements UserService { private final LdapTemplate ldapTemplate; /** * ldap search base */ @Value("${spring.ldap.base}") private String base; /** * user objectClass */ @Value("${ldap.mapping.objectClass}") private String objectClassAttrName; /** * user LoginId */ @Value("${ldap.mapping.loginId}") private String loginIdAttrName; /** * user displayName */ @Value("${ldap.mapping.userDisplayName}") private String userDisplayNameAttrName; /** * email */ @Value("${ldap.mapping.email}") private String emailAttrName; /** * rdn */ @Value("${ldap.mapping.rdnKey:}") private String rdnKey; /** * memberOf */ @Value("#{'${ldap.filter.memberOf:}'.split('\\|')}") private String[] memberOf; /** * group search base */ @Value("${ldap.group.groupBase:}") private String groupBase; /** * group filter eg. (&(cn=apollo-admins)(&(member=*))) */ @Value("${ldap.group.groupSearch:}") private String groupSearch; /** * group memberShip eg. member */ @Value("${ldap.group.groupMembership:}") private String groupMembershipAttrName; private static final String MEMBER_OF_ATTR_NAME = "memberOf"; private static final String MEMBER_UID_ATTR_NAME = "memberUid"; public LdapUserService(final LdapTemplate ldapTemplate) { this.ldapTemplate = ldapTemplate; } /** * 用户信息Mapper */ private final ContextMapper ldapUserInfoMapper = (ctx) -> { DirContextAdapter contextAdapter = (DirContextAdapter) ctx; UserInfo userInfo = new UserInfo(); userInfo.setUserId(contextAdapter.getStringAttribute(loginIdAttrName)); userInfo.setName(contextAdapter.getStringAttribute(userDisplayNameAttrName)); userInfo.setEmail(contextAdapter.getStringAttribute(emailAttrName)); return userInfo; }; /** * 查询条件 */ private ContainerCriteria ldapQueryCriteria() { ContainerCriteria criteria = query().searchScope(SearchScope.SUBTREE).where("objectClass").is(objectClassAttrName); if (memberOf.length > 0 && !StringUtils.isEmpty(memberOf[0])) { ContainerCriteria memberOfFilters = query().where(MEMBER_OF_ATTR_NAME).is(memberOf[0]); Arrays.stream(memberOf).skip(1) .forEach(filter -> memberOfFilters.or(MEMBER_OF_ATTR_NAME).is(filter)); criteria.and(memberOfFilters); } return criteria; } /** * 根据entryDN查找用户信息 * * @param member ldap EntryDN * @param userIds 用户ID列表 */ private UserInfo lookupUser(String member, List userIds) { return ldapTemplate.lookup(member, (AttributesMapper) attributes -> { UserInfo tmp = new UserInfo(); Attribute emailAttribute = attributes.get(emailAttrName); if (emailAttribute != null && emailAttribute.get() != null) { tmp.setEmail(emailAttribute.get().toString()); } Attribute loginIdAttribute = attributes.get(loginIdAttrName); if (loginIdAttribute != null && loginIdAttribute.get() != null) { tmp.setUserId(loginIdAttribute.get().toString()); } Attribute userDisplayNameAttribute = attributes.get(userDisplayNameAttrName); if (userDisplayNameAttribute != null && userDisplayNameAttribute.get() != null) { tmp.setName(userDisplayNameAttribute.get().toString()); } if (userIds != null) { if (userIds.stream().anyMatch(c -> c.equals(tmp.getUserId()))) { return tmp; } return null; } return tmp; }); } private UserInfo searchUserById(String userId) { try { return ldapTemplate.searchForObject(query().where(loginIdAttrName).is(userId), ctx -> { UserInfo userInfo = new UserInfo(); DirContextAdapter contextAdapter = (DirContextAdapter) ctx; userInfo.setEmail(contextAdapter.getStringAttribute(emailAttrName)); userInfo.setName(contextAdapter.getStringAttribute(userDisplayNameAttrName)); userInfo.setUserId(contextAdapter.getStringAttribute(loginIdAttrName)); return userInfo; }); } catch (EmptyResultDataAccessException ex) { // EmptyResultDataAccessException means no record found return null; } } /** * 按照group搜索用户 * * @param groupBase group search base * @param groupSearch group filter * @param keyword user search keywords * @param userIds user id list */ private List searchUserInfoByGroup(String groupBase, String groupSearch, String keyword, List userIds) { return ldapTemplate.searchForObject(groupBase, groupSearch, ctx -> { List userInfos = new ArrayList<>(); if (!MEMBER_UID_ATTR_NAME.equals(groupMembershipAttrName)) { String[] members = ((DirContextAdapter) ctx).getStringAttributes(groupMembershipAttrName); for (String item : members) { LdapName ldapName = LdapUtils.newLdapName(item); LdapName memberRdn = LdapUtils.removeFirst(ldapName, LdapUtils.newLdapName(base)); if (keyword != null) { String rdnValue = LdapUtils.getValue(memberRdn, rdnKey).toString(); if (rdnValue.toLowerCase().contains(keyword.toLowerCase())) { UserInfo userInfo = lookupUser(memberRdn.toString(), userIds); userInfos.add(userInfo); } } else { UserInfo userInfo = lookupUser(memberRdn.toString(), userIds); if (userInfo != null) { userInfos.add(userInfo); } } } return userInfos; } Set memberUids = Sets.newHashSet(((DirContextAdapter) ctx).getStringAttributes(groupMembershipAttrName)); if (!CollectionUtils.isEmpty(userIds)) { memberUids = Sets.intersection(memberUids, Sets.newHashSet(userIds)); } for (String memberUid : memberUids) { UserInfo userInfo = searchUserById(memberUid); if (userInfo != null) { if (keyword != null) { if (userInfo.getUserId().toLowerCase().contains(keyword.toLowerCase())) { userInfos.add(userInfo); } } else { userInfos.add(userInfo); } } } return userInfos; }); } @Override public List searchUsers(String keyword, int offset, int limit, boolean includeInactiveUsers) { List users = new ArrayList<>(); if (StringUtils.isNotBlank(groupSearch)) { List userListByGroup = searchUserInfoByGroup(groupBase, groupSearch, keyword, null); users.addAll(userListByGroup); return users.stream().collect(collectingAndThen(toCollection(() -> new TreeSet<>((o1, o2) -> { if (o1.getUserId().equals(o2.getUserId())) { return 0; } return -1; })), ArrayList::new)); } ContainerCriteria criteria = ldapQueryCriteria(); if (!Strings.isNullOrEmpty(keyword)) { criteria.and(query().where(loginIdAttrName).like(keyword + "*").or(userDisplayNameAttrName) .like(keyword + "*")); } users = ldapTemplate.search(criteria, ldapUserInfoMapper); return users; } @Override public UserInfo findByUserId(String userId) { if (StringUtils.isNotBlank(groupSearch)) { List lists = searchUserInfoByGroup(groupBase, groupSearch, null, Collections.singletonList(userId)); if (lists != null && !lists.isEmpty() && lists.get(0) != null) { return lists.get(0); } return null; } try { return ldapTemplate.searchForObject(ldapQueryCriteria().and(loginIdAttrName).is(userId), ldapUserInfoMapper); } catch (EmptyResultDataAccessException ex) { // EmptyResultDataAccessException means no record found return null; } } @Override public List findByUserIds(List userIds) { if (CollectionUtils.isEmpty(userIds)) { return Collections.emptyList(); } if (StringUtils.isNotBlank(groupSearch)) { return searchUserInfoByGroup(groupBase, groupSearch, null, userIds); } ContainerCriteria criteria = query().where(loginIdAttrName).is(userIds.get(0)); userIds.stream().skip(1).forEach(userId -> criteria.or(loginIdAttrName).is(userId)); return ldapTemplate.search(ldapQueryCriteria().and(criteria), ldapUserInfoMapper); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/oidc/ExcludeClientCredentialsClientRegistrationRepository.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.spi.oidc; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Objects; import java.util.Spliterator; import java.util.Spliterators; import java.util.stream.Collectors; import java.util.stream.StreamSupport; import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository; import org.springframework.security.oauth2.core.AuthorizationGrantType; /** * @author vdisk */ public class ExcludeClientCredentialsClientRegistrationRepository implements ClientRegistrationRepository, Iterable { /** * origin clientRegistrationRepository */ private final InMemoryClientRegistrationRepository delegate; /** * exclude client_credentials */ private final List clientRegistrationList; public ExcludeClientCredentialsClientRegistrationRepository( InMemoryClientRegistrationRepository delegate) { Objects.requireNonNull(delegate, "clientRegistrationRepository cannot be null"); this.delegate = delegate; this.clientRegistrationList = Collections.unmodifiableList(StreamSupport .stream(Spliterators.spliteratorUnknownSize(delegate.iterator(), Spliterator.ORDERED), false) .filter(clientRegistration -> !AuthorizationGrantType.CLIENT_CREDENTIALS .equals(clientRegistration.getAuthorizationGrantType())) .collect(Collectors.toList())); } @Override public ClientRegistration findByRegistrationId(String registrationId) { ClientRegistration clientRegistration = this.delegate.findByRegistrationId(registrationId); if (clientRegistration == null) { return null; } if (AuthorizationGrantType.CLIENT_CREDENTIALS .equals(clientRegistration.getAuthorizationGrantType())) { return null; } return clientRegistration; } @Override public Iterator iterator() { return this.clientRegistrationList.iterator(); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/oidc/OidcAuthenticationSuccessEventListener.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.spi.oidc; import com.ctrip.framework.apollo.portal.entity.bo.UserInfo; import com.ctrip.framework.apollo.portal.spi.configuration.OidcExtendProperties; import java.util.Map; import java.util.Map.Entry; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.ApplicationListener; import org.springframework.security.authentication.event.AuthenticationSuccessEvent; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.security.oauth2.jwt.Jwt; /** * @author vdisk */ public class OidcAuthenticationSuccessEventListener implements ApplicationListener { private static final Logger log = LoggerFactory.getLogger(OidcAuthenticationSuccessEventListener.class); private static final Logger oidcLog = LoggerFactory.getLogger(OidcAuthenticationSuccessEventListener.class.getName() + ".oidc"); private static final Logger jwtLog = LoggerFactory.getLogger(OidcAuthenticationSuccessEventListener.class.getName() + ".jwt"); private final OidcLocalUserService oidcLocalUserService; private final OidcExtendProperties oidcExtendProperties; private final ConcurrentMap userIdCache = new ConcurrentHashMap<>(); public OidcAuthenticationSuccessEventListener(OidcLocalUserService oidcLocalUserService, OidcExtendProperties oidcExtendProperties) { this.oidcLocalUserService = oidcLocalUserService; this.oidcExtendProperties = oidcExtendProperties; } @Override public void onApplicationEvent(AuthenticationSuccessEvent event) { Object principal = event.getAuthentication().getPrincipal(); if (principal instanceof OidcUser) { this.oidcUserLogin((OidcUser) principal); return; } if (principal instanceof Jwt) { this.jwtLogin((Jwt) principal); return; } log.warn("principal is neither oidcUser nor jwt, principal=[{}]", principal); } private void oidcUserLogin(OidcUser oidcUser) { String subject = oidcUser.getSubject(); String userDisplayName = OidcUserInfoUtil.getOidcUserDisplayName(oidcUser, this.oidcExtendProperties); String email = oidcUser.getEmail(); this.logOidc(oidcUser, subject, userDisplayName, email); UserInfo newUserInfo = new UserInfo(); newUserInfo.setUserId(subject); newUserInfo.setName(userDisplayName); newUserInfo.setEmail(email); if (this.contains(subject)) { this.oidcLocalUserService.updateUserInfo(newUserInfo); return; } this.oidcLocalUserService.createLocalUser(newUserInfo); } private void logOidc(OidcUser oidcUser, String subject, String userDisplayName, String email) { oidcLog.debug("oidc authentication success, sub=[{}] userDisplayName=[{}] email=[{}]", subject, userDisplayName, email); if (oidcLog.isTraceEnabled()) { Map claims = oidcUser.getClaims(); for (Entry entry : claims.entrySet()) { oidcLog.trace("oidc authentication claims [{}={}]", entry.getKey(), entry.getValue()); } } } private boolean contains(String userId) { if (this.userIdCache.containsKey(userId)) { return true; } UserInfo userInfo = this.oidcLocalUserService.findByUserId(userId); if (userInfo != null) { this.userIdCache.put(userId, userId); return true; } return false; } private void jwtLogin(Jwt jwt) { String subject = jwt.getSubject(); String userDisplayName = OidcUserInfoUtil.getJwtUserDisplayName(jwt, this.oidcExtendProperties); this.logJwt(jwt, subject, userDisplayName); if (this.contains(subject)) { return; } UserInfo newUserInfo = new UserInfo(); newUserInfo.setUserId(subject); newUserInfo.setName(userDisplayName); this.oidcLocalUserService.createLocalUser(newUserInfo); } private void logJwt(Jwt jwt, String subject, String userDisplayName) { jwtLog.debug("jwt authentication success, sub=[{}] userDisplayName=[{}]", subject, userDisplayName); if (jwtLog.isTraceEnabled()) { Map claims = jwt.getClaims(); for (Entry entry : claims.entrySet()) { jwtLog.trace("jwt authentication claims [{}={}]", entry.getKey(), entry.getValue()); } } } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/oidc/OidcLocalUserService.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.spi.oidc; import com.ctrip.framework.apollo.portal.entity.bo.UserInfo; import com.ctrip.framework.apollo.portal.spi.UserService; /** * @author vdisk */ public interface OidcLocalUserService extends UserService { /** * create local user info related to the oidc user * * @param newUserInfo the oidc user's info */ void createLocalUser(UserInfo newUserInfo); /** * update user's info * * @param newUserInfo the new user's info */ void updateUserInfo(UserInfo newUserInfo); } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/oidc/OidcLocalUserServiceImpl.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.spi.oidc; import com.ctrip.framework.apollo.core.utils.StringUtils; import com.ctrip.framework.apollo.portal.entity.bo.UserInfo; import com.ctrip.framework.apollo.portal.entity.po.UserPO; import com.ctrip.framework.apollo.portal.repository.UserRepository; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.crypto.password.DelegatingPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.provisioning.JdbcUserDetailsManager; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.CollectionUtils; /** * @author vdisk */ public class OidcLocalUserServiceImpl implements OidcLocalUserService { private final Collection authorities = Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER")); private final PasswordEncoder placeholderDelegatingPasswordEncoder = new DelegatingPasswordEncoder(PlaceholderPasswordEncoder.ENCODING_ID, Collections .singletonMap(PlaceholderPasswordEncoder.ENCODING_ID, new PlaceholderPasswordEncoder())); private final JdbcUserDetailsManager userDetailsManager; private final UserRepository userRepository; public OidcLocalUserServiceImpl(JdbcUserDetailsManager userDetailsManager, UserRepository userRepository) { this.userDetailsManager = userDetailsManager; this.userRepository = userRepository; } @Transactional(rollbackFor = Exception.class) @Override public void createLocalUser(UserInfo newUserInfo) { UserDetails user = new User(newUserInfo.getUserId(), this.placeholderDelegatingPasswordEncoder.encode(""), authorities); userDetailsManager.createUser(user); this.updateUserInfoInternal(newUserInfo); } private void updateUserInfoInternal(UserInfo newUserInfo) { UserPO managedUser = userRepository.findByUsername(newUserInfo.getUserId()); if (!StringUtils.isBlank(newUserInfo.getEmail())) { managedUser.setEmail(newUserInfo.getEmail()); } if (!StringUtils.isBlank(newUserInfo.getName())) { managedUser.setUserDisplayName(newUserInfo.getName()); } userRepository.save(managedUser); } @Transactional(rollbackFor = Exception.class) @Override public void updateUserInfo(UserInfo newUserInfo) { this.updateUserInfoInternal(newUserInfo); } @Override public List searchUsers(String keyword, int offset, int limit, boolean includeInactiveUsers) { List users = this.findUsers(keyword, includeInactiveUsers); if (CollectionUtils.isEmpty(users)) { return Collections.emptyList(); } return users.stream().map(UserPO::toUserInfo).collect(Collectors.toList()); } private List findUsers(String keyword, boolean includeInactiveUsers) { Map users = new HashMap<>(); List byUsername; List byUserDisplayName; if (includeInactiveUsers) { if (StringUtils.isEmpty(keyword)) { return (List) userRepository.findAll(); } byUsername = userRepository.findByUsernameLike("%" + keyword + "%"); byUserDisplayName = userRepository.findByUserDisplayNameLike("%" + keyword + "%"); } else { if (StringUtils.isEmpty(keyword)) { return userRepository.findFirst20ByEnabled(1); } byUsername = userRepository.findByUsernameLikeAndEnabled("%" + keyword + "%", 1); byUserDisplayName = userRepository.findByUserDisplayNameLikeAndEnabled("%" + keyword + "%", 1); } if (!CollectionUtils.isEmpty(byUsername)) { for (UserPO user : byUsername) { users.put(user.getId(), user); } } if (!CollectionUtils.isEmpty(byUserDisplayName)) { for (UserPO user : byUserDisplayName) { users.put(user.getId(), user); } } return new ArrayList<>(users.values()); } @Override public UserInfo findByUserId(String userId) { UserPO userPO = userRepository.findByUsername(userId); return userPO == null ? null : userPO.toUserInfo(); } @Override public List findByUserIds(List userIds) { List users = userRepository.findByUsernameIn(userIds); if (CollectionUtils.isEmpty(users)) { return Collections.emptyList(); } return users.stream().map(UserPO::toUserInfo).collect(Collectors.toList()); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/oidc/OidcLogoutHandler.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.spi.oidc; import com.ctrip.framework.apollo.portal.spi.LogoutHandler; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; /** * @author vdisk */ public class OidcLogoutHandler implements LogoutHandler { @Override public void logout(HttpServletRequest request, HttpServletResponse response) { // do nothing here } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/oidc/OidcUserInfoHolder.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.spi.oidc; import com.ctrip.framework.apollo.portal.entity.bo.UserInfo; import com.ctrip.framework.apollo.portal.spi.UserInfoHolder; import com.ctrip.framework.apollo.portal.spi.UserService; import com.ctrip.framework.apollo.portal.spi.configuration.OidcExtendProperties; import java.security.Principal; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.oauth2.core.oidc.StandardClaimNames; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.util.StringUtils; /** * @author vdisk */ public class OidcUserInfoHolder implements UserInfoHolder { private static final Logger log = LoggerFactory.getLogger(OidcUserInfoHolder.class); private final UserService userService; private final OidcExtendProperties oidcExtendProperties; public OidcUserInfoHolder(UserService userService, OidcExtendProperties oidcExtendProperties) { this.userService = userService; this.oidcExtendProperties = oidcExtendProperties; } @Override public UserInfo getUser() { UserInfo userInfo = this.getUserInternal(); if (StringUtils.hasText(userInfo.getName())) { return userInfo; } UserInfo userInfoFound = this.userService.findByUserId(userInfo.getUserId()); if (userInfoFound != null) { return userInfoFound; } return userInfo; } private UserInfo getUserInternal() { Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); if (principal instanceof OidcUser) { UserInfo userInfo = new UserInfo(); OidcUser oidcUser = (OidcUser) principal; userInfo.setUserId(oidcUser.getSubject()); userInfo .setName(OidcUserInfoUtil.getOidcUserDisplayName(oidcUser, this.oidcExtendProperties)); userInfo.setEmail(oidcUser.getEmail()); return userInfo; } if (principal instanceof Jwt) { Jwt jwt = (Jwt) principal; UserInfo userInfo = new UserInfo(); userInfo.setUserId(jwt.getSubject()); userInfo.setName(OidcUserInfoUtil.getJwtUserDisplayName(jwt, this.oidcExtendProperties)); return userInfo; } log.debug("principal is neither oidcUser nor jwt, principal=[{}]", principal); if (principal instanceof OAuth2User) { UserInfo userInfo = new UserInfo(); OAuth2User oAuth2User = (OAuth2User) principal; userInfo.setUserId(oAuth2User.getName()); userInfo.setName(oAuth2User.getAttribute(StandardClaimNames.PREFERRED_USERNAME)); userInfo.setEmail(oAuth2User.getAttribute(StandardClaimNames.EMAIL)); return userInfo; } if (principal instanceof Principal) { UserInfo userInfo = new UserInfo(); Principal userPrincipal = (Principal) principal; userInfo.setUserId(userPrincipal.getName()); return userInfo; } UserInfo userInfo = new UserInfo(); userInfo.setUserId(String.valueOf(principal)); return userInfo; } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/oidc/OidcUserInfoUtil.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.spi.oidc; import com.ctrip.framework.apollo.core.utils.StringUtils; import com.ctrip.framework.apollo.portal.spi.configuration.OidcExtendProperties; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.security.oauth2.jwt.Jwt; public class OidcUserInfoUtil { private OidcUserInfoUtil() { throw new UnsupportedOperationException("util class"); } /** * get userDisplayName from oidcUser * * @param oidcUser the user * @param oidcExtendProperties claimName properties * @return userDisplayName */ public static String getOidcUserDisplayName(OidcUser oidcUser, OidcExtendProperties oidcExtendProperties) { String userDisplayNameClaimName = oidcExtendProperties.getUserDisplayNameClaimName(); if (!StringUtils.isBlank(userDisplayNameClaimName)) { return oidcUser.getClaimAsString(userDisplayNameClaimName); } String preferredUsername = oidcUser.getPreferredUsername(); if (!StringUtils.isBlank(preferredUsername)) { return preferredUsername; } return oidcUser.getFullName(); } /** * get userDisplayName from jwt * * @param jwt the user * @param oidcExtendProperties claimName properties * @return userDisplayName */ public static String getJwtUserDisplayName(Jwt jwt, OidcExtendProperties oidcExtendProperties) { String jwtUserDisplayNameClaimName = oidcExtendProperties.getJwtUserDisplayNameClaimName(); if (!StringUtils.isBlank(jwtUserDisplayNameClaimName)) { return jwt.getClaimAsString(jwtUserDisplayNameClaimName); } return null; } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/oidc/PlaceholderPasswordEncoder.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.spi.oidc; import java.util.Base64; import java.util.concurrent.ThreadLocalRandom; import org.springframework.security.crypto.password.PasswordEncoder; /** * @author vdisk */ public class PlaceholderPasswordEncoder implements PasswordEncoder { public static final String ENCODING_ID = "placeholder"; /** * generate a random string as a password placeholder. */ @Override public String encode(CharSequence rawPassword) { byte[] bytes = new byte[32]; ThreadLocalRandom.current().nextBytes(bytes); return Base64.getEncoder().encodeToString(bytes); } /** * placeholder will never matches a password */ @Override public boolean matches(CharSequence rawPassword, String encodedPassword) { return false; } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/package-info.java ================================================ /* * Copyright 2024 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. * */ /** * This package defines common interfaces so that each company could provide their own implementations.
* Currently we provide Default implementations: Default.
* You may refer com.ctrip.framework.apollo.portal.spi.configuration.AuthConfiguration when providing your own implementation. * * @see com.ctrip.framework.apollo.portal.spi.configuration.AuthConfiguration */ package com.ctrip.framework.apollo.portal.spi; ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/springsecurity/ApolloPasswordEncoderFactory.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.spi.springsecurity; import com.ctrip.framework.apollo.portal.spi.oidc.PlaceholderPasswordEncoder; import java.util.HashMap; import java.util.Map; import org.springframework.security.crypto.argon2.Argon2PasswordEncoder; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.factory.PasswordEncoderFactories; import org.springframework.security.crypto.password.DelegatingPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.Pbkdf2PasswordEncoder; import org.springframework.security.crypto.scrypt.SCryptPasswordEncoder; /** * @author vdisk */ public final class ApolloPasswordEncoderFactory { private ApolloPasswordEncoderFactory() {} /** * Creates a {@link DelegatingPasswordEncoder} with default mappings {@link * PasswordEncoderFactories#createDelegatingPasswordEncoder()}, and add a placeholder encoder for * oidc {@link PlaceholderPasswordEncoder} * * @return the {@link PasswordEncoder} to use */ @SuppressWarnings("deprecation") public static PasswordEncoder createDelegatingPasswordEncoder() { // copy from PasswordEncoderFactories, and it's should follow the upgrade of the // PasswordEncoderFactories String encodingId = "bcrypt"; Map encoders = new HashMap<>(); encoders.put(encodingId, new BCryptPasswordEncoder()); encoders.put("ldap", new org.springframework.security.crypto.password.LdapShaPasswordEncoder()); encoders.put("MD4", new org.springframework.security.crypto.password.Md4PasswordEncoder()); encoders.put("MD5", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("MD5")); encoders.put("noop", org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance()); encoders.put("pbkdf2", Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8()); encoders.put("scrypt", SCryptPasswordEncoder.defaultsForSpringSecurity_v5_8()); encoders.put("SHA-1", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-1")); encoders.put("SHA-256", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-256")); encoders.put("sha256", new org.springframework.security.crypto.password.StandardPasswordEncoder()); encoders.put("argon2", Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8()); // placeholder encoder for oidc encoders.put(PlaceholderPasswordEncoder.ENCODING_ID, new PlaceholderPasswordEncoder()); DelegatingPasswordEncoder delegatingPasswordEncoder = new DelegatingPasswordEncoder(encodingId, encoders); // todo: adapt the old password, and it should be removed in the next feature version of the // 1.9.x delegatingPasswordEncoder .setDefaultPasswordEncoderForMatches(new PasswordEncoderAdapter(encoders.get(encodingId))); return delegatingPasswordEncoder; } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/springsecurity/PasswordEncoderAdapter.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.spi.springsecurity; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.util.StringUtils; /** * @author vdisk */ @Deprecated public class PasswordEncoderAdapter implements PasswordEncoder { private static final String PREFIX = "{"; private static final String SUFFIX = "}"; private final PasswordEncoder encoder; public PasswordEncoderAdapter(PasswordEncoder encoder) { this.encoder = encoder; } @Override public String encode(CharSequence rawPassword) { throw new UnsupportedOperationException("encode is not supported"); } @Override public boolean matches(CharSequence rawPassword, String encodedPassword) { boolean matches = this.encoder.matches(rawPassword, encodedPassword); if (matches) { return true; } String id = this.extractId(encodedPassword); if (StringUtils.hasText(id)) { throw new IllegalArgumentException( "There is no PasswordEncoder mapped for the id \"" + id + "\""); } return false; } private String extractId(String prefixEncodedPassword) { if (prefixEncodedPassword == null) { return null; } int start = prefixEncodedPassword.indexOf(PREFIX); if (start != 0) { return null; } int end = prefixEncodedPassword.indexOf(SUFFIX, start); if (end < 0) { return null; } return prefixEncodedPassword.substring(start + 1, end); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/springsecurity/SpringSecurityUserInfoHolder.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.spi.springsecurity; import com.ctrip.framework.apollo.portal.entity.bo.UserInfo; import com.ctrip.framework.apollo.portal.spi.UserInfoHolder; import com.ctrip.framework.apollo.portal.spi.UserService; import java.util.function.Supplier; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.beans.factory.ObjectProvider; import java.security.Principal; public class SpringSecurityUserInfoHolder implements UserInfoHolder { private final Supplier userServiceSupplier; public SpringSecurityUserInfoHolder(UserService userService) { this(() -> userService); } public SpringSecurityUserInfoHolder(ObjectProvider userServiceProvider) { this(userServiceProvider::getObject); } private SpringSecurityUserInfoHolder(Supplier userServiceSupplier) { this.userServiceSupplier = userServiceSupplier; } @Override public UserInfo getUser() { String userId = this.getCurrentUsername(); UserInfo userInfoFound = this.userServiceSupplier.get().findByUserId(userId); if (userInfoFound != null) { return userInfoFound; } UserInfo userInfo = new UserInfo(); userInfo.setUserId(userId); return userInfo; } private String getCurrentUsername() { Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); if (principal instanceof UserDetails) { return ((UserDetails) principal).getUsername(); } if (principal instanceof Principal) { return ((Principal) principal).getName(); } return String.valueOf(principal); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/springsecurity/SpringSecurityUserService.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.spi.springsecurity; import com.ctrip.framework.apollo.common.exception.BadRequestException; import com.ctrip.framework.apollo.core.utils.StringUtils; import com.ctrip.framework.apollo.portal.entity.bo.UserInfo; import com.ctrip.framework.apollo.portal.entity.po.Authority; import com.ctrip.framework.apollo.portal.entity.po.UserPO; import com.ctrip.framework.apollo.portal.repository.AuthorityRepository; import com.ctrip.framework.apollo.portal.repository.UserRepository; import com.ctrip.framework.apollo.portal.spi.UserService; import java.util.HashMap; import java.util.Map; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.CollectionUtils; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.stream.Collectors; /** * @author lepdou 2017-03-10 */ public class SpringSecurityUserService implements UserService { private final PasswordEncoder passwordEncoder; private final UserRepository userRepository; private final AuthorityRepository authorityRepository; public SpringSecurityUserService(PasswordEncoder passwordEncoder, UserRepository userRepository, AuthorityRepository authorityRepository) { this.passwordEncoder = passwordEncoder; this.userRepository = userRepository; this.authorityRepository = authorityRepository; } @Transactional public void create(UserPO user) { String username = user.getUsername(); String newPassword = passwordEncoder.encode(user.getPassword()); UserPO managedUser = userRepository.findByUsername(username); if (managedUser != null) { throw BadRequestException.userAlreadyExists(username); } // create user.setPassword(newPassword); user.setEnabled(user.getEnabled()); userRepository.save(user); // save authorities Authority authority = new Authority(); authority.setUsername(username); authority.setAuthority("ROLE_user"); authorityRepository.save(authority); } @Transactional public void update(UserPO user) { String username = user.getUsername(); String newPassword = passwordEncoder.encode(user.getPassword()); UserPO managedUser = userRepository.findByUsername(username); if (managedUser == null) { throw BadRequestException.userNotExists(username); } managedUser.setPassword(newPassword); managedUser.setEmail(user.getEmail()); managedUser.setUserDisplayName(user.getUserDisplayName()); managedUser.setEnabled(user.getEnabled()); userRepository.save(managedUser); } @Transactional public void changeEnabled(UserPO user) { String username = user.getUsername(); UserPO managedUser = userRepository.findByUsername(username); managedUser.setEnabled(user.getEnabled()); userRepository.save(managedUser); } @Override public List searchUsers(String keyword, int offset, int limit, boolean includeInactiveUsers) { List users = this.findUsers(keyword, includeInactiveUsers); if (CollectionUtils.isEmpty(users)) { return Collections.emptyList(); } return users.stream().map(UserPO::toUserInfo).collect(Collectors.toList()); } private List findUsers(String keyword, boolean includeInactiveUsers) { Map users = new HashMap<>(); List byUsername; List byUserDisplayName; if (includeInactiveUsers) { if (StringUtils.isEmpty(keyword)) { return (List) userRepository.findAll(); } byUsername = userRepository.findByUsernameLike("%" + keyword + "%"); byUserDisplayName = userRepository.findByUserDisplayNameLike("%" + keyword + "%"); } else { if (StringUtils.isEmpty(keyword)) { return userRepository.findFirst20ByEnabled(1); } byUsername = userRepository.findByUsernameLikeAndEnabled("%" + keyword + "%", 1); byUserDisplayName = userRepository.findByUserDisplayNameLikeAndEnabled("%" + keyword + "%", 1); } if (!CollectionUtils.isEmpty(byUsername)) { for (UserPO user : byUsername) { users.put(user.getId(), user); } } if (!CollectionUtils.isEmpty(byUserDisplayName)) { for (UserPO user : byUserDisplayName) { users.put(user.getId(), user); } } return new ArrayList<>(users.values()); } @Override public UserInfo findByUserId(String userId) { UserPO userPO = userRepository.findByUsername(userId); return userPO == null ? null : userPO.toUserInfo(); } @Override public List findByUserIds(List userIds) { List users = userRepository.findByUsernameIn(userIds); if (CollectionUtils.isEmpty(users)) { return Collections.emptyList(); } return users.stream().map(UserPO::toUserInfo).collect(Collectors.toList()); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/util/ConfigFileUtils.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.util; import com.ctrip.framework.apollo.common.dto.ClusterDTO; import com.ctrip.framework.apollo.common.entity.App; import com.ctrip.framework.apollo.common.entity.AppNamespace; import com.ctrip.framework.apollo.common.exception.BadRequestException; import com.ctrip.framework.apollo.core.enums.ConfigFileFormat; import com.ctrip.framework.apollo.core.utils.StringUtils; import com.ctrip.framework.apollo.portal.controller.ConfigsImportController; import com.ctrip.framework.apollo.portal.environment.Env; import com.google.common.base.Splitter; import java.io.File; import java.util.List; import org.springframework.web.multipart.MultipartFile; /** * First version: move from {@link ConfigsImportController#importConfigFile(java.lang.String, java.lang.String, java.lang.String, java.lang.String, org.springframework.web.multipart.MultipartFile)} * @author wxq */ public class ConfigFileUtils { public static final String APP_METADATA_FILENAME = "app.metadata"; public static final String CLUSTER_METADATA_FILE_SUFFIX = ".cluster.metadata"; public static final String APP_NAMESPACE_METADATA_FILE_SUFFIX = ".appnamespace.metadata"; public static void check(MultipartFile file) { checkEmpty(file); final String originalFilename = file.getOriginalFilename(); checkFormat(originalFilename); } /** * @throws BadRequestException if file is empty */ static void checkEmpty(MultipartFile file) { if (file.isEmpty()) { throw new BadRequestException("The file is empty. " + file.getOriginalFilename()); } } /** * @throws BadRequestException if file's format is invalid */ static void checkFormat(final String originalFilename) { final List fileNameSplit = Splitter.on(".").splitToList(originalFilename); if (fileNameSplit.size() <= 1) { throw new BadRequestException("The file format is invalid."); } for (String s : fileNameSplit) { if (StringUtils.isEmpty(s)) { throw new BadRequestException("The file format is invalid."); } } } static String[] getThreePart(final String originalFilename) { return originalFilename.split("[+]"); } /** * @throws BadRequestException if file's name cannot divide to 3 parts by "+" symbol */ static void checkThreePart(final String originalFilename) { String[] parts = getThreePart(originalFilename); if (3 != parts.length) { throw new BadRequestException("file name [" + originalFilename + "] not valid"); } } /** *

   *  "application+default+application.properties" -> "properties"
   *  "application+default+application.yml" -> "yml"
   * 
* @throws BadRequestException if file's format is invalid */ public static String getFormat(final String originalFilename) { final List fileNameSplit = Splitter.on(".").splitToList(originalFilename); if (fileNameSplit.size() <= 1) { throw new BadRequestException("The file format is invalid."); } return fileNameSplit.get(fileNameSplit.size() - 1); } /** *
   *  "123+default+application.properties" -> "123"
   *  "abc+default+application.yml" -> "abc"
   *  "666+default+application.json" -> "666"
   * 
* @throws BadRequestException if file's name is invalid */ public static String getAppId(final String originalFilename) { checkThreePart(originalFilename); return getThreePart(originalFilename)[0]; } public static String getClusterName(final String originalFilename) { checkThreePart(originalFilename); return getThreePart(originalFilename)[1]; } /** *
   *  "application+default+application.properties" -> "application"
   *  "application+default+application.yml" -> "application.yml"
   *  "application+default+application.json" -> "application.json"
   *  "application+default+application.333.yml" -> "application.333.yml"
   * 
* @throws BadRequestException if file's name is invalid */ public static String getNamespace(final String originalFilename) { checkThreePart(originalFilename); final String[] threeParts = getThreePart(originalFilename); final String suffix = threeParts[2]; if (!suffix.contains(".")) { throw new BadRequestException(originalFilename + " namespace and format is invalid!"); } final int lastDotIndex = suffix.lastIndexOf("."); final String namespace = suffix.substring(0, lastDotIndex); // format after last character '.' final String format = suffix.substring(lastDotIndex + 1); if (!ConfigFileFormat.isValidFormat(format)) { throw new BadRequestException(originalFilename + " format is invalid!"); } ConfigFileFormat configFileFormat = ConfigFileFormat.fromString(format); if (configFileFormat.equals(ConfigFileFormat.Properties)) { return namespace; } else { // compatibility of other format return namespace + "." + format; } } /** *
   *   appId    cluster   namespace       return
   *   666      default   application     666+default+application.properties
   *   123      none      action.yml      123+none+action.yml
   * 
*/ public static String toFilename(final String appId, final String clusterName, final String namespace, final ConfigFileFormat configFileFormat) { final String suffix; if (ConfigFileFormat.Properties.equals(configFileFormat)) { suffix = "." + ConfigFileFormat.Properties.getValue(); } else { suffix = ""; } return appId + "+" + clusterName + "+" + namespace + suffix; } /** * file path = ownerName/appId/env/configFilename * @return file path in compressed file */ public static String genNamespacePath(final String ownerName, final String appId, final Env env, final String configFilename) { return String.join(File.separator, ownerName, appId, env.getName(), configFilename); } /** * file path = appId/env/configFilename * @return file path in compressed file */ public static String genNamespacePathIgnoreUser(final String appId, final Env env, final String configFilename) { return String.join(File.separator, appId, env.getName(), configFilename); } /** * path = ownerName/appId/app.metadata */ public static String genAppInfoPath(App app) { return String.join(File.separator, app.getOwnerName(), app.getAppId(), APP_METADATA_FILENAME); } /** * path = {appNamespace}.appnamespace.metadata */ public static String genAppNamespaceInfoPath(AppNamespace appNamespace) { return String.join(File.separator, appNamespace.getAppId() + "+" + appNamespace.getName() + APP_NAMESPACE_METADATA_FILE_SUFFIX); } /** * path = ownerName/appId/env/${clusterName}.metadata */ public static String genClusterInfoPath(App app, Env env, ClusterDTO cluster) { return String.join(File.separator, app.getOwnerName(), app.getAppId(), env.getName(), cluster.getName() + CLUSTER_METADATA_FILE_SUFFIX); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/util/ConfigToFileUtils.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.util; import java.io.BufferedReader; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.PrintWriter; import java.util.List; import java.util.stream.Collectors; /** * jian.tan */ public class ConfigToFileUtils { @Deprecated public static void itemsToFile(OutputStream os, List items) { try { PrintWriter printWriter = new PrintWriter(os); items.forEach(printWriter::println); printWriter.close(); } catch (Exception e) { throw e; } } public static String fileToString(InputStream inputStream) { BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream)); return bufferedReader.lines().collect(Collectors.joining(System.lineSeparator())); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/util/KeyValueUtils.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.util; import java.util.HashMap; import java.util.Map; import java.util.Properties; /** * some tools for manipulate key in map and properties * @author wxq */ public class KeyValueUtils { /** * make a filter on properties. * and convert properties to a map * the suffix match is case insensitive * @param properties * @param suffix suffix in a key * @return a map which key is ends with suffix */ public static Map filterWithKeyIgnoreCaseEndsWith(Properties properties, String suffix) { // use O(n log(n)) algorithm Map keyValues = new HashMap<>(); for (String propertyName : properties.stringPropertyNames()) { keyValues.put(propertyName, properties.getProperty(propertyName)); } return filterWithKeyIgnoreCaseEndsWith(keyValues, suffix); } /** * make a filter on map's key, * keep the k-v which key ends with suffix given * the suffix match is case insensitive * @param keyValues * @param suffix suffix in a key * @return a map which key is ends with suffix */ public static Map filterWithKeyIgnoreCaseEndsWith(Map keyValues, String suffix) { // use O(n) algorithm Map map = new HashMap<>(); for (Map.Entry entry : keyValues.entrySet()) { String key = entry.getKey(); String value = entry.getValue(); // let key and suffix both to upper, // so the suffix match doesn't care about the character is upper or lower if (key.toUpperCase().endsWith(suffix.toUpperCase())) { map.put(key, value); } } return map; } /** * remove key's suffix in a map * suppose that all keys's length not smaller than suffixLength, * if not satisfied, a terrible runtime exception will occur * @param keyValues * @param suffixLength suffix string's length * @return */ public static Map removeKeySuffix(Map keyValues, int suffixLength) { // use O(n) algorithm Map map = new HashMap<>(); for (Map.Entry entry : keyValues.entrySet()) { String key = entry.getKey(); String value = entry.getValue(); String newKey = key.substring(0, key.length() - suffixLength); map.put(newKey, value); } return map; } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/util/NamespaceBOUtils.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.util; import com.google.common.collect.Lists; import com.google.gson.Gson; import com.ctrip.framework.apollo.common.dto.ItemDTO; import com.ctrip.framework.apollo.core.ConfigConsts; import com.ctrip.framework.apollo.core.enums.ConfigFileFormat; import com.ctrip.framework.apollo.portal.controller.ConfigsExportController; import com.ctrip.framework.apollo.portal.entity.bo.ItemBO; import com.ctrip.framework.apollo.portal.entity.bo.NamespaceBO; import org.springframework.util.CollectionUtils; import java.util.Collections; import java.util.List; import java.util.stream.Collectors; /** * @author wxq */ public class NamespaceBOUtils { private static final Gson GSON = new Gson(); /** * namespace must not be {@link ConfigFileFormat#Properties}. the content of namespace in item's value which item's * key is {@link ConfigConsts#CONFIG_FILE_CONTENT_KEY}. * * @param namespaceBO namespace * @return content of non-properties's namespace */ static String convertNonProperties2configFileContent(NamespaceBO namespaceBO) { List itemBOS = namespaceBO.getItems(); for (ItemBO itemBO : itemBOS) { String key = itemBO.getItem().getKey(); // special namespace format(not properties) if (ConfigConsts.CONFIG_FILE_CONTENT_KEY.equals(key)) { ItemDTO dto = itemBO.getItem(); dto.setId(0); dto.setNamespaceId(0); return GSON.toJson(Lists.newArrayList(dto)); } } return ""; } /** * copy from old {@link ConfigsExportController}. convert {@link NamespaceBO} to a file content. * * @return content of config file * @throws IllegalStateException if convert properties to string fail */ public static String convert2configFileContent(NamespaceBO namespaceBO) { // early return if it is not a properties format namespace if (!ConfigFileFormat.Properties.equals(ConfigFileFormat.fromString(namespaceBO.getFormat()))) { // it is not a properties namespace return convertNonProperties2configFileContent(namespaceBO); } // it must be a properties format namespace List itemBOS = namespaceBO.getItems(); if (CollectionUtils.isEmpty(itemBOS)) { return GSON.toJson(Collections.emptyList()); } List itemDTOS = itemBOS.stream().map(itemBO -> { ItemDTO dto = itemBO.getItem(); dto.setId(0); dto.setNamespaceId(0); return dto; }).collect(Collectors.toList()); return GSON.toJson(itemDTOS); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/util/RelativeDateFormat.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.util; import org.apache.commons.lang3.time.FastDateFormat; import java.time.Duration; import java.time.Instant; import java.time.LocalDateTime; import java.time.ZoneId; import java.util.Date; public class RelativeDateFormat { private static final FastDateFormat TIMESTAMP_FORMAT = FastDateFormat.getInstance("yyyy-MM-dd"); private static final String ONE_SECOND_AGO = " seconds ago"; private static final String ONE_MINUTE_AGO = " minutes ago"; private static final String ONE_HOUR_AGO = " hours ago"; private static final String ONE_DAY_AGO = " days ago"; private static final String ONE_MONTH_AGO = " months ago"; public static String format(Date date) { Instant instant = date.toInstant(); ZoneId zoneId = ZoneId.systemDefault(); LocalDateTime localDateTime = instant.atZone(zoneId).toLocalDateTime(); Duration duration = Duration.between(localDateTime, LocalDateTime.now()); if (duration.toMillis() <= 0L) { return "now"; } if (duration.getSeconds() <= 60L) { return (duration.getSeconds() <= 0 ? 1 : duration.getSeconds()) + ONE_SECOND_AGO; } if (duration.toMinutes() < 45L) { return (duration.toMinutes() <= 0 ? 1 : duration.toMinutes()) + ONE_MINUTE_AGO; } if (duration.toHours() < 24L) { return (duration.toHours() <= 0 ? 1 : duration.toHours()) + ONE_HOUR_AGO; } if (localDateTime.isAfter(LocalDateTime.now().minusDays(1))) { return "yesterday"; } if (localDateTime.isAfter(LocalDateTime.now().minusDays(2))) { return "the day before yesterday"; } if (duration.toDays() < 30L) { return (duration.toDays() <= 0 ? 1 : duration.toDays()) + ONE_DAY_AGO; } if (duration.toDays() / 30 <= 3L) { return duration.toDays() / 30 + ONE_MONTH_AGO; } return TIMESTAMP_FORMAT.format(date); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/util/RoleUtils.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.util; import com.ctrip.framework.apollo.core.ConfigConsts; import com.ctrip.framework.apollo.portal.constant.RoleType; import com.google.common.base.Joiner; import com.google.common.base.Splitter; import java.util.Iterator; public class RoleUtils { private static final Joiner STRING_JOINER = Joiner.on(ConfigConsts.CLUSTER_NAMESPACE_SEPARATOR).skipNulls(); private static final Splitter STRING_SPLITTER = Splitter.on(ConfigConsts.CLUSTER_NAMESPACE_SEPARATOR).omitEmptyStrings().trimResults(); public static String buildAppMasterRoleName(String appId) { return STRING_JOINER.join(RoleType.MASTER, appId); } public static String extractAppIdFromMasterRoleName(String masterRoleName) { Iterator parts = STRING_SPLITTER.split(masterRoleName).iterator(); // skip role type if (parts.hasNext() && parts.next().equals(RoleType.MASTER) && parts.hasNext()) { return parts.next(); } return null; } public static String extractAppIdFromRoleName(String roleName) { Iterator parts = STRING_SPLITTER.split(roleName).iterator(); if (parts.hasNext()) { String roleType = parts.next(); if (RoleType.isValidRoleType(roleType) && parts.hasNext()) { return parts.next(); } } return null; } public static String buildAppRoleName(String appId, String roleType) { return STRING_JOINER.join(roleType, appId); } public static String buildModifyNamespaceRoleName(String appId, String namespaceName) { return buildModifyNamespaceRoleName(appId, namespaceName, null); } public static String buildModifyNamespaceRoleName(String appId, String namespaceName, String env) { return STRING_JOINER.join(RoleType.MODIFY_NAMESPACE, appId, namespaceName, env); } public static String buildModifyNamespacesInClusterRoleName(String appId, String env, String clusterName) { return STRING_JOINER.join(RoleType.MODIFY_NAMESPACES_IN_CLUSTER, appId, env, clusterName); } public static String buildModifyDefaultNamespaceRoleName(String appId) { return STRING_JOINER.join(RoleType.MODIFY_NAMESPACE, appId, ConfigConsts.NAMESPACE_APPLICATION); } public static String buildReleaseNamespaceRoleName(String appId, String namespaceName) { return buildReleaseNamespaceRoleName(appId, namespaceName, null); } public static String buildReleaseNamespaceRoleName(String appId, String namespaceName, String env) { return STRING_JOINER.join(RoleType.RELEASE_NAMESPACE, appId, namespaceName, env); } public static String buildReleaseNamespacesInClusterRoleName(String appId, String env, String clusterName) { return STRING_JOINER.join(RoleType.RELEASE_NAMESPACES_IN_CLUSTER, appId, env, clusterName); } public static String buildNamespaceRoleName(String appId, String namespaceName, String roleType) { return buildNamespaceRoleName(appId, namespaceName, roleType, null); } public static String buildNamespaceRoleName(String appId, String namespaceName, String roleType, String env) { return STRING_JOINER.join(roleType, appId, namespaceName, env); } public static String buildClusterRoleName(String appId, String env, String clusterName, String roleType) { return STRING_JOINER.join(roleType, appId, env, clusterName); } public static String buildReleaseDefaultNamespaceRoleName(String appId) { return STRING_JOINER.join(RoleType.RELEASE_NAMESPACE, appId, ConfigConsts.NAMESPACE_APPLICATION); } public static String buildNamespaceTargetId(String appId, String namespaceName) { return buildNamespaceTargetId(appId, namespaceName, null); } public static String buildNamespaceTargetId(String appId, String namespaceName, String env) { return STRING_JOINER.join(appId, namespaceName, env); } public static String buildClusterTargetId(String appId, String env, String clusterName) { return STRING_JOINER.join(appId, env, clusterName); } public static String buildDefaultNamespaceTargetId(String appId) { return STRING_JOINER.join(appId, ConfigConsts.NAMESPACE_APPLICATION); } public static String buildCreateApplicationRoleName(String permissionType, String permissionTargetId) { return STRING_JOINER.join(permissionType, permissionTargetId); } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/util/checker/AuthUserPasswordChecker.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.util.checker; import com.ctrip.framework.apollo.portal.component.config.PortalConfig; import com.google.common.base.Strings; import java.util.List; import java.util.regex.Pattern; import org.springframework.stereotype.Component; @Component public class AuthUserPasswordChecker implements UserPasswordChecker { private static final Pattern PWD_PATTERN = Pattern.compile("^(?=.*[0-9].*)(?=.*[a-zA-Z].*).{8,20}$"); private final PortalConfig portalConfig; public AuthUserPasswordChecker(final PortalConfig portalConfig) { this.portalConfig = portalConfig; } @Override public CheckResult checkWeakPassword(String password) { if (Strings.isNullOrEmpty(password) || !PWD_PATTERN.matcher(password).matches()) { return new CheckResult(Boolean.FALSE, "Password needs a number and letter and between 8~20 characters"); } if (isCommonlyUsed(password)) { return new CheckResult(Boolean.FALSE, "Passwords cannot be consecutive, regular letters or numbers. And cannot be commonly used. " + "e.g: abcd1234, 1234qwer, 1q2w3e4r, 1234asdfghjk, ..."); } return new CheckResult(Boolean.TRUE, null); } /** * @return The password contains code fragment. */ private boolean isCommonlyUsed(String password) { List list = portalConfig.getUserPasswordNotAllowList(); if (list == null || list.isEmpty()) { return false; } for (String s : list) { if (password.toLowerCase().contains(s)) { return true; } } return false; } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/util/checker/CheckResult.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.util.checker; public class CheckResult { private final boolean success; private final String message; public CheckResult(boolean success, String message) { this.success = success; this.message = message; } public boolean isSuccess() { return success; } public String getMessage() { return message; } } ================================================ FILE: apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/util/checker/UserPasswordChecker.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.util.checker; public interface UserPasswordChecker { CheckResult checkWeakPassword(String password); } ================================================ FILE: apollo-portal/src/main/resources/apollo-env.properties ================================================ # # Copyright 2024 Apollo Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # local.meta=http://localhost:8080 dev.meta=${dev_meta} fat.meta=${fat_meta} uat.meta=${uat_meta} lpt.meta=${lpt_meta} pro.meta=${pro_meta} ================================================ FILE: apollo-portal/src/main/resources/apollo-portal.conf ================================================ MODE=service PID_FOLDER=. # console appender log file folder LOG_FOLDER=/opt/logs/ # console appender log file name LOG_FILENAME=apollo-portal.console.log # write application logs only to file appender export LOG_APPENDERS=FILE ================================================ FILE: apollo-portal/src/main/resources/application-github.properties ================================================ # # Copyright 2024 Apollo Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # DataSource spring.datasource.url = ${spring_datasource_url} spring.datasource.username = ${spring_datasource_username} spring.datasource.password = ${spring_datasource_password} ================================================ FILE: apollo-portal/src/main/resources/application-ldap-activedirectory-sample.yml ================================================ # # Copyright 2024 Apollo Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # ldap sample for active directory, need to rename this file to application-ldap.yml to make it effective spring: ldap: base: "dc=example,dc=com" username: "admin" # 配置管理员账号,用于搜索、匹配用户 password: "password" search-filter: "(sAMAccountName={0})" # 用户过滤器,登录的时候用这个过滤器来搜索用户 urls: - "ldap://1.1.1.1:389" ldap: mapping: # 配置 ldap 属性 object-class: "user" # ldap 用户 objectClass 配置 login-id: "sAMAccountName" # ldap 用户惟一 id,用来作为登录的 id user-display-name: "cn" # ldap 用户名,用来作为显示名 email: "userPrincipalName" # ldap 邮箱属性 # filter: # 可选项,配置过滤,目前只支持 memberOf # memberOf: "CN=ServiceDEV,OU=test,DC=example,DC=com|CN=WebDEV,OU=test,DC=example,DC=com" # 只允许 memberOf 属性为 ServiceDEV 和 WebDEV 的用户访问 ================================================ FILE: apollo-portal/src/main/resources/application-ldap-apacheds-sample.yml ================================================ # # Copyright 2024 Apollo Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # ldap sample for apache ds, need to rename this file to application-ldap.yml to make it effective spring: ldap: base: "dc=example,dc=com" username: "uid=admin,ou=system" # 配置管理员账号,用于搜索、匹配用户 password: "password" search-filter: "(uid={0})" # 用户过滤器,登录的时候用这个过滤器来搜索用户 urls: - "ldap://localhost:10389" ldap: mapping: # 配置 ldap 属性 object-class: "inetOrgPerson" # ldap 用户 objectClass 配置 login-id: "uid" # ldap 用户惟一 id,用来作为登录的 id rdn-key: "cn" # ldap rdn key,可选项,如需启用group search需要配置 user-display-name: "displayName" # ldap 用户名,用来作为显示名 email: "mail" # ldap 邮箱属性 # group: # 配置ldap group,可选配置,启用后只有特定group的用户可以登录apollo # object-class: "groupOfNames" # 配置groupClassName # group-base: "ou=group" # group search base # group-search: "(&(cn=dev))" # group filter # group-membership: "member" # group memberShip eg. member or memberUid ================================================ FILE: apollo-portal/src/main/resources/application-ldap-openldap-sample.yml ================================================ # # Copyright 2024 Apollo Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # ldap sample for open ldap, need to rename this file to application-ldap.yml to make it effective spring: ldap: base: "dc=example,dc=org" username: "cn=admin,dc=example,dc=org" # 配置管理员账号,用于搜索、匹配用户 password: "password" search-filter: "(uid={0})" # 用户过滤器,登录的时候用这个过滤器来搜索用户 urls: - "ldap://localhost:389" ldap: mapping: # 配置 ldap 属性 object-class: "inetOrgPerson" # ldap 用户 objectClass 配置 login-id: "uid" # ldap 用户惟一 id,用来作为登录的 id rdn-key: "uid" # ldap rdn key,可选项,如需启用group search需要配置 user-display-name: "cn" # ldap 用户名,用来作为显示名 email: "mail" # ldap 邮箱属性 # group: # 启用group search,可选配置,启用后只有特定group的用户可以登录apollo # object-class: "posixGroup" # 配置groupClassName # group-base: "ou=group" # group search base # group-search: "(&(cn=dev))" # group filter # group-membership: "memberUid" # group memberShip eg. member or memberUid ================================================ FILE: apollo-portal/src/main/resources/application-oidc-sample.yml ================================================ # # Copyright 2024 Apollo Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # server: # 解析反向代理请求头 forward-headers-strategy: framework spring: security: oauth2: client: provider: # provider-name 是 oidc 提供者的名称, 任意字符均可, registration 的配置需要用到这个名称 : # 必须是 https, oidc 的 issuer-uri, 和 jwt 的 issuer-uri 一致的话直接引用即可, 也可以单独设置 issuer-uri: ${spring.security.oauth2.resourceserver.jwt.issuer-uri} registration: # registration-name 是 oidc 客户端的名称, 任意字符均可, oidc 登录必须配置一个 authorization_code 类型的 registration : # oidc 登录必须配置一个 authorization_code 类型的 registration authorization-grant-type: authorization_code client-authentication-method: client_secret_basic # client-id 是在 oidc 提供者处配置的客户端ID, 用于登录 provider client-id: apollo-portal # provider 的名称, 需要和上面配置的 provider 名称保持一致 provider: # openid 为 oidc 登录的必须 scope, 此处可以添加其它自定义的 scope scope: - openid # client-secret 是在 oidc 提供者处配置的客户端密码, 用于登录 provider # 从安全角度考虑更推荐使用环境变量来配置, 环境变量的命名规则为: 将配置项的 key 当中的 点(.)、横杠(-)替换为下划线(_), 然后将所有字母改为大写, spring boot 会自动处理符合此规则的环境变量 # 例如 spring.security.oauth2.client.registration..client-secret -> SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION__CLIENT_SECRET ( 可以替换为自定义的 oidc 客户端的名称) client-secret: d43c91c0-xxxx-xxxx-xxxx-xxxxxxxxxxxx # registration-name-client 是 oidc 客户端的名称, 任意字符均可, client_credentials 类型的 registration 为选填项, 可以不配置 registration-name-client: # client_credentials 类型的 registration 为选填项, 用于 apollo-portal 作为客户端请求其它被 oidc 保护的资源, 可以不配置 authorization-grant-type: client_credentials client-authentication-method: client_secret_basic # client-id 是在 oidc 提供者处配置的客户端ID, 用于登录 provider client-id: apollo-portal # provider 的名称, 需要和上面配置的 provider 名称保持一致 provider: # openid 为 oidc 登录的必须 scope, 此处可以添加其它自定义的 scope scope: - openid # client-secret 是在 oidc 提供者处配置的客户端密码, 用于登录 provider, 多个 registration 的密码如果一致可以直接引用 client-secret: ${spring.security.oauth2.client.registration.registration-name.client-secret} resourceserver: jwt: # 必须是 https, jwt 的 issuer-uri # 例如 你的 issuer-uri 是 https://host:port/auth/realms/apollo/.well-known/openid-configuration, 那么此处只需要配置 https://host:port/auth/realms/apollo 即可, spring boot 处理的时候会自动加上 /.well-known/openid-configuration 的后缀 issuer-uri: https://host:port/auth/realms/apollo ================================================ FILE: apollo-portal/src/main/resources/application.properties ================================================ # # Copyright 2024 Apollo Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # You may uncomment the following config to activate different spring profiles #spring.profiles.active=github,consul-discovery #spring.profiles.active=github,zookeeper-discovery #spring.profiles.active=github,custom-defined-discovery #spring.profiles.active=github,database-discovery # You may change the following config to activate different database profiles like h2/postgres spring.profiles.group.github = mysql # true: enabled the new feature of audit log # false/missing: disable it apollo.audit.log.enabled = true ================================================ FILE: apollo-portal/src/main/resources/application.yml ================================================ # # Copyright 2024 Apollo Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # spring: profiles: active: ${apollo_profile} jpa: properties: hibernate: metadata_builder_contributor: com.ctrip.framework.apollo.common.jpa.SqlFunctionsMetadataBuilderContributor query: plan_cache_max_size: 192 # limit query plan cache max size session: store-type: jdbc jdbc: initialize-schema: never servlet: multipart: max-file-size: 200MB # import data configs max-request-size: 200MB server: compression: enabled: true tomcat: use-relative-redirects: true servlet: session: cookie: name: SESSION # prevent csrf same-site: Lax management: health: status: order: DOWN, OUT_OF_SERVICE, UNKNOWN, UP ldap: enabled: false # disable ldap health check by default ================================================ FILE: apollo-portal/src/main/resources/jpa/portaldb.init.h2.sql ================================================ -- -- Copyright 2024 Apollo Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. -- You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. -- CREATE TABLE SPRING_SESSION ( PRIMARY_ID VARCHAR(255) NOT NULL, SESSION_ID VARCHAR(255) NOT NULL, CREATION_TIME BIGINT NOT NULL, LAST_ACCESS_TIME BIGINT NOT NULL, MAX_INACTIVE_INTERVAL INT NOT NULL, EXPIRY_TIME BIGINT NOT NULL, PRINCIPAL_NAME VARCHAR(100), CONSTRAINT SPRING_SESSION_PK PRIMARY KEY (PRIMARY_ID) ); CREATE UNIQUE INDEX SPRING_SESSION_IX1 ON SPRING_SESSION (SESSION_ID); CREATE INDEX SPRING_SESSION_IX2 ON SPRING_SESSION (EXPIRY_TIME); CREATE INDEX SPRING_SESSION_IX3 ON SPRING_SESSION (PRINCIPAL_NAME); CREATE TABLE SPRING_SESSION_ATTRIBUTES ( SESSION_PRIMARY_ID VARCHAR(255) NOT NULL, ATTRIBUTE_NAME VARCHAR(200) NOT NULL, ATTRIBUTE_BYTES BLOB NOT NULL, CONSTRAINT SPRING_SESSION_ATTRIBUTES_PK PRIMARY KEY (SESSION_PRIMARY_ID, ATTRIBUTE_NAME), CONSTRAINT SPRING_SESSION_ATTRIBUTES_FK FOREIGN KEY (SESSION_PRIMARY_ID) REFERENCES SPRING_SESSION (PRIMARY_ID) ON DELETE CASCADE ); INSERT INTO "ServerConfig" ("Key", "Value", "Comment", "DataChange_CreatedBy", "DataChange_CreatedTime") VALUES ('apollo.portal.envs', 'dev', '可支持的环境列表', 'default', '1970-01-01 00:00:00'), ('organizations', '[{"orgId":"TEST1","orgName":"样例部门1"},{"orgId":"TEST2","orgName":"样例部门2"}]', '部门列表', 'default', '1970-01-01 00:00:00'), ('superAdmin', 'apollo', 'Portal超级管理员', 'default', '1970-01-01 00:00:00'), ('api.readTimeout', '10000', 'http接口read timeout', 'default', '1970-01-01 00:00:00'), ('consumer.token.salt', 'someSalt', 'consumer token salt', 'default', '1970-01-01 00:00:00'), ('admin.createPrivateNamespace.switch', 'true', '是否允许项目管理员创建私有namespace', 'default', '1970-01-01 00:00:00'), ('configView.memberOnly.envs', 'pro', '只对项目成员显示配置信息的环境列表,多个env以英文逗号分隔', 'default', '1970-01-01 00:00:00'), ('apollo.portal.meta.servers', '{}', '各环境Meta Service列表', 'default', '1970-01-01 00:00:00'); INSERT INTO "Users" ("Username", "Password", "UserDisplayName", "Email", "Enabled") VALUES ('apollo', '$2a$10$7r20uS.BQ9uBpf3Baj3uQOZvMVvB1RN3PYoKE94gtz2.WAOuiiwXS', 'apollo', 'apollo@acme.com', 1); INSERT INTO "Authorities" ("Username", "Authority") VALUES ('apollo', 'ROLE_user'); CREATE ALIAS IF NOT EXISTS UNIX_TIMESTAMP FOR "com.ctrip.framework.apollo.common.jpa.H2Function.unixTimestamp"; ================================================ FILE: apollo-portal/src/main/resources/logback.xml ================================================ propertyContains("LOG_APPENDERS", "FILE") && !propertyContains("LOG_APPENDERS", "CONSOLE") propertyContains("LOG_APPENDERS", "CONSOLE") && !propertyContains("LOG_APPENDERS", "FILE") ================================================ FILE: apollo-portal/src/main/resources/portal.properties ================================================ # # Copyright 2024 Apollo Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # #Used for apollo-assembly spring.application.name= apollo-portal server.port= 8070 logging.file.name= /opt/logs/apollo-portal.log spring.jmx.default-domain = apollo-portal ================================================ FILE: apollo-portal/src/main/resources/static/app/access_key.html ================================================ {{'Config.AccessKeyManage' | translate }}
================================================ FILE: apollo-portal/src/main/resources/static/app/manage_cluster.html ================================================ {{'Config.ManageCluster' | translate }}

{{'Common.Environment' | translate }}: {{env.name}}

{{'Common.Cluster' | translate }}: {{cluster.name}}
================================================ FILE: apollo-portal/src/main/resources/static/app/setting.html ================================================ {{'App.Setting.Title' | translate }}
================================================ FILE: apollo-portal/src/main/resources/static/app.html ================================================ {{'App.CreateProject' | translate }}
{{'App.CreateProject' | translate }}
{{'App.AppIdTips' | translate }}
{{'App.AppNameTips' | translate }}
{{'App.AppOwnerTips' | translate }}

{{'App.AppAdminTips1' | translate }}
{{'App.AppAdminTips2' | translate }}
================================================ FILE: apollo-portal/src/main/resources/static/audit_log_menu.html ================================================ {{'ApolloAuditLog.Title' | translate}}

{{'ApolloAuditLog.Disabled' | translate}}


{{'ApolloAuditLog.DisabledTips' | translate}}

{{'ApolloAuditLog.MoreDetails' | translate}}

{{'ApolloAuditLog.OpName' | translate }} {{'ApolloAuditLog.OpType' | translate }} {{'ApolloAuditLog.Operator' | translate }} {{'ApolloAuditLog.HappenedTime' | translate }} {{'ApolloAuditLog.Description' | translate }}
{{ alog.opName }} {{ alog.opType }} {{ alog.operator }} {{ alog.happenedTime | date: 'yyyy-MM-dd HH:mm:ss' }} {{ alog.description }}
{{'ApolloAuditLog.LoadMore' | translate }}
================================================ FILE: apollo-portal/src/main/resources/static/audit_log_trace_detail.html ================================================ {{ 'ApolloAuditLog.TraceDetailTips' | translate }}

{{ 'ApolloAuditLog.TraceDetailTips' | translate }}

{{'ApolloAuditLog.TraceIdTips' | translate }}:{{traceId}}
{{'ApolloAuditLog.TraceAuditLogTips' | translate }}
{{'ApolloAuditLog.RelatedDataInfluenceTips' | translate }}

{{'ApolloAuditLog.OpType' | translate}}:{{showingDetail.logDTO.opType}}

{{'ApolloAuditLog.Operator' | translate}}:{{showingDetail.logDTO.operator}}
{{'ApolloAuditLog.OpName' | translate}}:{{showingDetail.logDTO.opName}}
{{'ApolloAuditLog.Description' | translate}}:{{showingDetail.logDTO.description}}

{{'ApolloAuditLog.InfluenceEntity' | translate}}:

{{'ApolloAuditLog.DataInfluence.EntityName' | translate}}: {{dataInfluenceEntity[1].name}}
{{'ApolloAuditLog.DataInfluence.EntityId' | translate}}: {{dataInfluenceEntity[1].id}}
{{'ApolloAuditLog.DataInfluence.AnyMatchedEntityId' | translate}}

{{'ApolloAuditLog.DataInfluence.Fields' | translate}}:
{{'ApolloAuditLog.DataInfluence.MatchedFields' | translate}}:
{{dataInfluence.fieldName}} ==> {{dataInfluence.fieldNewValue}}
{{dataInfluence.fieldName}} : {{dataInfluence.fieldOldValue}} ==> (deleted)
{{dataInfluence.fieldName}} <== {{showingDetail.logDTO.opType == 'DELETE' ? dataInfluence.fieldOldValue : dataInfluence.fieldNewValue}}
{{'ApolloAuditLog.NoDataInfluence' | translate }}
{{'ApolloAuditLog.FieldChangeHistory' | translate }}

{{entityNameOfFindRelated + ':' + entityIdOfFindRelated + ':' + fieldNameOfFindRelated}}

{{'ApolloAuditLog.DataInfluence.FieldNewValue' | translate }} {{'ApolloAuditLog.DataInfluence.HappenedTime' | translate }}
{{ di.fieldNewValue ? di.fieldNewValue : '(deleted)' }} {{ di.happenedTime | date: 'yyyy-MM-dd HH:mm:ss' }}
{{'ApolloAuditLog.DataInfluence.LoadMore' | translate }}
{{'ApolloAuditLog.NoDataInfluence' | translate }}

{{'ApolloAuditLog.NoTraceDetail' | translate }}

================================================ FILE: apollo-portal/src/main/resources/static/cluster/ns_role.html ================================================ {{'Cluster.Role.Title' | translate }}

{{'Cluster.Role.NoPermission' | translate }}

================================================ FILE: apollo-portal/src/main/resources/static/cluster.html ================================================ {{'Cluster.CreateCluster' | translate }}
================================================ FILE: apollo-portal/src/main/resources/static/config/diff.html ================================================ {{'Config.Diff.Title' | translate }}
{{'Config.Diff.TipsTitle' | translate }}:
  • {{'Config.Diff.Tips' | translate }}

Key Value ( {{'Common.Environment' | translate }} : , {{'Common.Cluster' | translate }} : ) Comment ( {{'Common.Environment' | translate }} : , {{'Common.Cluster' | translate }} : )
================================================ FILE: apollo-portal/src/main/resources/static/config/history.html ================================================ {{'Config.History.Title' | translate }}
================================================ FILE: apollo-portal/src/main/resources/static/config/sync.html ================================================ {{'Config.Sync.Title' | translate }}
================================================ FILE: apollo-portal/src/main/resources/static/config.html ================================================ {{'Config.Title' | translate }}

{{'Config.Note' | translate }}: {{'Config.HasNotPublishNamespace' | translate }}

================================================ FILE: apollo-portal/src/main/resources/static/config_export.html ================================================ {{'ConfigExport.Title' | translate }}
{{'ConfigExport.Title' | translate }} {{'ConfigExport.Env.TitleTips' | translate }}
{{'ConfigExport.Export' | translate }}

({{'ConfigExport.ExportTips' | translate }})


{{'ConfigExport.IgnoreExistedNamespace' | translate }}
{{'ConfigExport.OverwriteExistedNamespace' | translate }}
{{'ConfigExport.Import' | translate }}

({{'ConfigExport.ImportTips' | translate }})

{{'ConfigExport.Title' | translate }} {{'ConfigExport.App.TitleTips' | translate }}
{{'ConfigExport.ClusterNameTips' | translate }}

{{'ConfigExport.IgnoreExistedNamespace' | translate }}
{{'ConfigExport.OverwriteExistedNamespace' | translate }}
{{'ConfigExport.Import' | translate }}

({{'ConfigExport.ImportTips' | translate }})

================================================ FILE: apollo-portal/src/main/resources/static/default_sso_heartbeat.html ================================================ SSO Heartbeat ================================================ FILE: apollo-portal/src/main/resources/static/delete_app_cluster_namespace.html ================================================ {{'Delete.Title' | translate }}
{{'Delete.DeleteApp' | translate }} {{'Delete.DeleteAppTips' | translate }}

{{'Delete.AppIdTips' | translate }}
{{'Delete.DeleteCluster' | translate }} {{'Delete.DeleteClusterTips' | translate }}

{{'Delete.ClusterNameTips' | translate }}
{{'Delete.DeleteNamespace' | translate }} {{'Delete.DeleteNamespaceTips' | translate }}
{{'Delete.DeleteNamespaceTips2' | translate }}
{{'Delete.AppNamespaceNameTips' | translate }}

{{'Common.IsRootUser' | translate }}

================================================ FILE: apollo-portal/src/main/resources/static/global_search_value.html ================================================ {{'Global.Title' | translate }}
================================================ FILE: apollo-portal/src/main/resources/static/i18n/en.json ================================================ { "Common.Title": "Apollo Configuration Center", "Common.Nav.ShowNavBar": "Display navigation bar", "Common.Nav.HideNavBar": "Hide navigation bar", "Common.Nav.Help": "Help", "Common.Nav.AdminTools": "Admin Tools", "Common.Nav.NonAdminTools": "Tools", "Common.Nav.UserManage": "User Management", "Common.Nav.SystemRoleManage": "System Permission Management", "Common.Nav.OpenMange": "Open Platform Authorization Management", "Common.Nav.SystemConfig": "System Configuration", "Common.Nav.DeleteApp-Cluster-Namespace": "Delete Apps, Clusters, AppNamespace", "Common.Nav.SystemInfo": "System Information", "Common.Nav.ConfigExport": "Config Export / Import", "Common.Nav.Logout": "Logout", "Common.Department": "Department", "Common.Cluster": "Cluster", "Common.Environment": "Environment", "Common.GrayscaleInstance": "GrayscaleInstance", "Common.Instance": "Instance", "Common.Email": "Email", "Common.AppId": "App Id", "Common.Namespace": "Namespace", "Common.LinkedNamespace": "LinkedNamespace", "Common.AppName": "App Name", "Common.AppOwner": "App Owner", "Common.AppOwnerLong": "App Owner", "Common.AppAdmin": "App Administrators", "Common.ClusterName": "Cluster Name", "Common.ClusterRemarks": "Remarks", "Common.Submit": "Submit", "Common.Save": "Save", "Common.Created": "Created Successfully", "Common.CreateFailed": "Fail to Create", "Common.Deleted": "Delete Successfully", "Common.DeleteFailed": "Fail to Delete", "Common.ReturnToIndex": "Return to project page", "Common.ReturnToManageClusterPage": "Return to manage cluster page", "Common.Cancel": "Cancel", "Common.Ok": "OK", "Common.Search": "Query", "Common.IsRootUser": "Current page is only accessible to Apollo administrator.", "Common.PleaseChooseDepartment": "Please select department", "Common.PleaseChooseOwner": "Please select app owner", "Common.LoginExpiredTips": "Your login is expired. Please refresh the page and try again.", "Common.Operation": "Operation", "Common.Delete": "Delete", "Common.ForceDelete": "Force Delete", "Component.DeleteNamespace.Title": "Delete Namespace", "Component.DeleteNamespace.PublicContent": "Caution, the public namespace for all environments will be deleted! This will cause the instances unable to get the configuration of this namespace. Are you sure you want to delete it?", "Component.DeleteNamespace.PrivateContent": "Caution, the private namespace for all environments will be deleted! This will cause the instances unable to get the configuration of this namespace. Are you sure you want to delete it?", "Component.DeleteNamespace.LinkedContent": "Caution, all the namespaces associated with the current environment will be deleted! This will cause the instances unable to get the configuration of this namespace. Are you sure you want to delete it?", "Component.DeleteNamespace.ForceDeleteContent": "There are instances in use for the current namespace within 24 hours, are you sure to force delete the namespace?", "Component.GrayscalePublishRule.Title": "Edit Grayscale Rule", "Component.GrayscalePublishRule.AppId": "Grayscale AppId", "Component.GrayscalePublishRule.AcceptRule": "Grayscale Application Rule", "Component.GrayscalePublishRule.AcceptPartInstance": "Apply to some instances", "Component.GrayscalePublishRule.AcceptAllInstance": "Apply to all instances", "Component.GrayscalePublishRule.IP": "Grayscale IP", "Component.GrayscalePublishRule.Label": "Grayscale Label", "Component.GrayscalePublishRule.AppIdFilterTips": "(The list of instances are filtered by the typed AppId automatically)", "Component.GrayscalePublishRule.IpTips": "Can't find the IP you want? You may ", "Component.GrayscalePublishRule.EnterIp": "enter IP manually", "Component.GrayscalePublishRule.EnterIpTips": "Enter the list of IP, using ',' as the separator, and then click the Add button.", "Component.GrayscalePublishRule.EnterLabelTips": "Enter the list of Label, using ',' as the separator, and then click the Add button.", "Component.GrayscalePublishRule.Add": "Add", "Component.ConfigItem.Title": "Add Configuration", "Component.ConfigItem.TitleTips": "(Reminder: Configuration can be added in batch via text mode)", "Component.ConfigItem.AddGrayscaleItem": "Add Grayscale Configuration", "Component.ConfigItem.ModifyItem": "Modify Configuration", "Component.ConfigItem.ItemKey": "Key", "Component.ConfigItem.ItemValue": "Value", "Component.ConfigItem.ItemValueTips": "Note: Special characters (Spaces, Newline, Tab, Chinese comma) easily cause configuration errors. If you want to check special characters in Value, please click", "Component.ConfigItem.ItemValueShowDetection": "Check Special Characters", "Component.ConfigItem.ItemValueNotHiddenChars": "No Special Characters", "Component.ConfigItem.FormatItemValue": "Format Content", "Component.ConfigItem.ItemComment": "Comment", "Component.ConfigItem.ChooseCluster": "Select Cluster", "Component.ConfigItem.ItemTypeName": "Type", "Component.ConfigItem.ItemTypeString": "String", "Component.ConfigItem.ItemTypeNumber": "Number", "Component.ConfigItem.ItemTypeBoolean": "Boolean", "Component.ConfigItem.ItemTypeJson": "JSON", "Component.ConfigItem.ItemNumberError": "Illegal Number", "Component.ConfigItem.ItemJsonError": "JSON Incorrect format", "Component.ConfigItem.ItemTypeTrue": "True", "Component.ConfigItem.ItemTypeFalse": "False", "Component.MergePublish.Title": "Full Release", "Component.MergePublish.Tips": "Full release will merge the configurations of grayscale version into the main version and release them.", "Component.MergePublish.NextStep": "After full release, choose which behavior you want", "Component.MergePublish.DeleteGrayscale": "Delete grayscale version", "Component.MergePublish.ReservedGrayscale": "Keep grayscale version", "Component.Namespace.Branch.IsChanged": "Modified", "Component.Namespace.Branch.ChangeUser": "Current Modifier", "Component.Namespace.Branch.ContinueGrayscalePublish": "Continue to Grayscale Release", "Component.Namespace.Branch.GrayscalePublish": "Grayscale Release", "Component.Namespace.Branch.MergeToMasterAndPublish": "Merge to the main version and release the main version's configurations ", "Component.Namespace.Branch.AllPublish": "Full Release", "Component.Namespace.Branch.DiscardGrayscaleVersion": "Abandon Grayscale Version", "Component.Namespace.Branch.DiscardGrayscale": "Abandon Grayscale", "Component.Namespace.Branch.NoPermissionTips": "You are not this project's administrator, nor you have edit or release permission for the namespace. Thus you cannot view the configuration.", "Component.Namespace.Branch.Tab.Configuration": "Configuration", "Component.Namespace.Branch.Tab.GrayscaleRule": "Grayscale Rule", "Component.Namespace.Branch.Tab.GrayscaleInstance": "Grayscale Instance List", "Component.Namespace.Branch.Tab.ChangeHistory": "Change History", "Component.Namespace.Branch.Body.Item": "Grayscale Configuration", "Component.Namespace.Branch.Body.AddedItem": "Add Grayscale Configuration", "Component.Namespace.Branch.Body.PublishState": "Release Status", "Component.Namespace.Branch.Body.ItemSort": "Sort", "Component.Namespace.Branch.Body.ItemKey": "Key", "Component.Namespace.Branch.Body.ItemMasterValue": "Value of Main Version", "Component.Namespace.Branch.Body.ItemGrayscaleValue": "Grayscale value", "Component.Namespace.Branch.Body.ItemComment": "Comment", "Component.Namespace.Branch.Body.ItemLastModify": "Last Modifier", "Component.Namespace.Branch.Body.ItemLastModifyTime": "Last Modified Time", "Component.Namespace.Branch.Body.ItemOperator": "Operation", "Component.Namespace.Branch.Body.ClickToSeeItemValue": "Click to view released values", "Component.Namespace.Branch.Body.ItemNoPublish": "Unreleased", "Component.Namespace.Branch.Body.ItemPublished": "Released", "Component.Namespace.Branch.Body.ItemEffective": "Effective configuration", "Component.Namespace.Branch.Body.ClickToSee": "Click to view", "Component.Namespace.Branch.Body.DeletedItem": "Deleted configuration", "Component.Namespace.Branch.Body.Delete": "Deleted", "Component.Namespace.Branch.Body.ChangedFromMaster": "Configuration modified from the main version", "Component.Namespace.Branch.Body.ModifiedItem": "Modified configuration", "Component.Namespace.Branch.Body.Modify": "Modified", "Component.Namespace.Branch.Body.AddedByGrayscale": "Specific configuration for grayscale version", "Component.Namespace.Branch.Body.Added": "New", "Component.Namespace.Branch.Body.Op.Modify": "Modify", "Component.Namespace.Branch.Body.Op.Delete": "Delete", "Component.Namespace.MasterBranch.Body.Title": "Configuration of the main version", "Component.Namespace.MasterBranch.Body.PublishState": "Release Status", "Component.Namespace.MasterBranch.Body.ItemKey": "Key", "Component.Namespace.MasterBranch.Body.ItemValue": "Value", "Component.Namespace.MasterBranch.Body.ItemComment": "Comment", "Component.Namespace.MasterBranch.Body.ItemLastModify": "Last Modifier", "Component.Namespace.MasterBranch.Body.ItemLastModifyTime": "Last Modified Time", "Component.Namespace.MasterBranch.Body.ItemOperator": "Operation", "Component.Namespace.MasterBranch.Body.ClickToSeeItemValue": "Click to check released values", "Component.Namespace.MasterBranch.Body.ItemNoPublish": "Unreleased", "Component.Namespace.MasterBranch.Body.ItemEffective": "Effective configuration", "Component.Namespace.MasterBranch.Body.ItemPublished": "Released", "Component.Namespace.MasterBranch.Body.AddedItem": "New configuration", "Component.Namespace.MasterBranch.Body.ModifyItem": "Modify the grayscale configuration", "Component.Namespace.Branch.GrayScaleRule.NoPermissionTips": "You do not have the permission to edit grayscale rule. Only those who have the permission to edit or release the namespace can edit grayscale rule. If you need to edit grayscale rule, please contact the project administrator to apply for the permission.", "Component.Namespace.Branch.GrayScaleRule.AppId": "Grayscale AppId", "Component.Namespace.Branch.GrayScaleRule.RuleList": "Grayscale Rule List", "Component.Namespace.Branch.GrayScaleRule.Operator": "Operation", "Component.Namespace.Branch.GrayScaleRule.ApplyToAllInstances": "ALL", "Component.Namespace.Branch.GrayScaleRule.Modify": "Modify", "Component.Namespace.Branch.GrayScaleRule.Delete": "Delete", "Component.Namespace.Branch.GrayScaleRule.AddNewRule": "Create Rule", "Component.Namespace.Branch.Instance.RefreshList": "Refresh List", "Component.Namespace.Branch.Instance.ItemToSee": "View configuration", "Component.Namespace.Branch.Instance.InstanceAppId": "App ID", "Component.Namespace.Branch.Instance.InstanceClusterName": "Cluster Name", "Component.Namespace.Branch.Instance.InstanceDataCenter": "Data Center", "Component.Namespace.Branch.Instance.InstanceIp": "IP", "Component.Namespace.Branch.Instance.InstanceGetItemTime": "Configuration Fetched Time", "Component.Namespace.Branch.Instance.LoadMore": "Refresh list", "Component.Namespace.Branch.Instance.NoInstance": "No instance information", "Component.Namespace.Branch.History.ItemType": "Type", "Component.Namespace.Branch.History.ItemKey": "Key", "Component.Namespace.Branch.History.ItemOldValue": "Old Value", "Component.Namespace.Branch.History.ItemNewValue": "New Value", "Component.Namespace.Branch.History.ItemComment": "Comment", "Component.Namespace.Branch.History.NewAdded": "Add", "Component.Namespace.Branch.History.Modified": "Update", "Component.Namespace.Branch.History.Deleted": "Delete", "Component.Namespace.Branch.History.LoadMore": "Load more", "Component.Namespace.Branch.History.NoHistory": "No Change History", "Component.Namespace.Header.Title.Private": "Private", "Component.Namespace.Header.Title.PrivateTips": "The configuration of private namespace ({{namespace.baseInfo.namespaceName}}) can be only fetched by clients whose AppId is {{appId}}", "Component.Namespace.Header.Title.Public": "Public", "Component.Namespace.Header.Title.PublicTips": "The configuration of namespace ({{namespace.baseInfo.namespaceName}}) can be fetched by any client.", "Component.Namespace.Header.Title.Extend": "Association", "Component.Namespace.Header.Title.ExtendTips": "The configuration of namespace ({{namespace.baseInfo.namespaceName}}) will override the configuration of the public namespace, and the combined configuration can only be fetched by clients whose AppId is {{appId}}.", "Component.Namespace.Header.Title.ExpandAndCollapse": "[Expand/Collapse]", "Component.Namespace.Header.Title.Master": "Main Version", "Component.Namespace.Header.Title.Grayscale": "Grayscale Version", "Component.Namespace.Master.LoadNamespace": "Load Namespace", "Component.Namespace.Master.LoadNamespaceTips": "Load Namespace", "Component.Namespace.Master.Items.Changed": "Modified", "Component.Namespace.Master.Items.ChangedUser": "Current modifier", "Component.Namespace.Master.Items.Publish": "Release", "Component.Namespace.Master.Items.PublishTips": "Release configuration", "Component.Namespace.Master.Items.Rollback": "Rollback", "Component.Namespace.Master.Items.RollbackTips": "Rollback released configuration", "Component.Namespace.Master.Items.PublishHistory": "Release History", "Component.Namespace.Master.Items.PublishHistoryTips": "View the release history", "Component.Namespace.Master.Items.Grant": "Authorize", "Component.Namespace.Master.Items.GrantTips": "Manage the configuration edit and release permission", "Component.Namespace.Master.Items.Grayscale": "Grayscale", "Component.Namespace.Master.Items.GrayscaleTips": "Create a test version", "Component.Namespace.Master.Items.RequestPermission": "Apply for configuration permission", "Component.Namespace.Master.Items.RequestPermissionTips": "You do not have any configuration permission. Please apply.", "Component.Namespace.Master.Items.DeleteNamespace": "Delete Namespace", "Component.Namespace.Master.Items.ExportNamespace": "Export Namespace", "Component.Namespace.Master.Items.ImportNamespace": "Import Namespace", "Component.Namespace.Master.Items.NoPermissionTips": "You are not this project's administrator, nor you have edit or release permission for the namespace. Thus you cannot view the configuration.", "Component.Namespace.Master.Items.ItemList": "Table", "Component.Namespace.Master.Items.ItemListByText": "Text", "Component.Namespace.Master.Items.ItemHistory": "Change History", "Component.Namespace.Master.Items.ItemInstance": "Instance List", "Component.Namespace.Master.Items.CopyText": "Copy", "Component.Namespace.Master.Items.Fullscreen": "Fullscreen", "Component.Namespace.Master.Items.ExitFullscreen": "Exit Fullscreen", "Component.Namespace.Master.Items.GrammarCheck": "Syntax Check", "Component.Namespace.Master.Items.CancelChanged": "Cancel", "Component.Namespace.Master.Items.Change": "Modify", "Component.Namespace.Master.Items.SummitChanged": "Submit", "Component.Namespace.Master.Items.SortByKey": "Filter the configurations by key", "Component.Namespace.Master.Items.FilterItem": "Filter", "Component.Namespace.Master.Items.RevokeItemTips": "Revoke configuration changes", "Component.Namespace.Master.Items.RevokeItem" :"Revoke", "Component.Namespace.Master.Items.SyncItemTips": "Synchronize configurations among environments", "Component.Namespace.Master.Items.SyncItem": "Synchronize", "Component.Namespace.Master.Items.DiffItemTips": "Compare the configurations among environments", "Component.Namespace.Master.Items.DiffItem": "Compare", "Component.Namespace.Master.Items.AddItem": "Add Configuration", "Component.Namespace.Master.Items.Body.ItemsNoPublishedTips": "Tips: This namespace has never been released. Apollo client will not be able to fetch the configuration and will record 404 log information. Please release it in time.", "Component.Namespace.Master.Items.Body.FilterByKey": "Input key to filter", "Component.Namespace.Master.Items.Body.PublishState": "Release Status", "Component.Namespace.Master.Items.Body.Sort": "Sort", "Component.Namespace.Master.Items.Body.ItemKey": "Key", "Component.Namespace.Master.Items.Body.ItemValue": "Value", "Component.Namespace.Master.Items.Body.ItemComment": "Comment", "Component.Namespace.Master.Items.Body.ItemLastModify": "Last Modifier", "Component.Namespace.Master.Items.Body.ItemLastModifyTime": "Last Modified Time", "Component.Namespace.Master.Items.Body.ItemOperator": "Operation", "Component.Namespace.Master.Items.Body.NoPublish": "Unreleased", "Component.Namespace.Master.Items.Body.NoPublishTitle": "Click to view released values", "Component.Namespace.Master.Items.Body.NoPublishTips": "New configuration, no released value", "Component.Namespace.Master.Items.Body.Published": "Released", "Component.Namespace.Master.Items.Body.PublishedTitle": "Effective configuration", "Component.Namespace.Master.Items.Body.ClickToSee": "Click to view", "Component.Namespace.Master.Items.Body.Grayscale": "Gray", "Component.Namespace.Master.Items.Body.HaveGrayscale": "This configuration has grayscale configuration. Click to view the value of grayscale.", "Component.Namespace.Master.Items.Body.NewAdded": "New", "Component.Namespace.Master.Items.Body.NewAddedTips": "New Configuration", "Component.Namespace.Master.Items.Body.Modified": "Modified", "Component.Namespace.Master.Items.Body.ModifiedTips": "Modified Configuration", "Component.Namespace.Master.Items.Body.Deleted": "Deleted", "Component.Namespace.Master.Items.Body.DeletedTips": "Deleted Configuration", "Component.Namespace.Master.Items.Body.ModifyTips": "Modify", "Component.Namespace.Master.Items.Body.DeleteTips": "Delete", "Component.Namespace.Master.Items.Body.Link.Title": "Overridden Configuration", "Component.Namespace.Master.Items.Body.Link.NoCoverLinkItem": "No Overridden Configuration", "Component.Namespace.Master.Items.Body.Public.Title": "Public Configuration", "Component.Namespace.Master.Items.Body.Public.Published": "Released Configuration", "Component.Namespace.Master.Items.Body.Public.NoPublish": "Unreleased Configuration", "Component.Namespace.Master.Items.Body.Public.NoPublicNamespaceTips1": "Owner of the current public namespace", "Component.Namespace.Master.Items.Body.Public.NoPublicNamespaceTips2": "hasn't associated this namespace, please contact the owner of {{namespace.parentAppId}} to associate this namespace in the {{namespace.parentAppId}} project.", "Component.Namespace.Master.Items.Body.Public.NoPublished": "No Released Configuration", "Component.Namespace.Master.Items.Body.Public.PublishedAndCover": "Override this configuration", "Component.Namespace.Master.Items.Body.NoPublished.Title": "No public configuration", "Component.Namespace.Master.Items.Body.NoPublished.PublishedValue": "Released Value", "Component.Namespace.Master.Items.Body.NoPublished.NoPublishedValue": "Unreleased Value", "Component.Namespace.Master.Items.Body.HistoryView.ItemType": "Type", "Component.Namespace.Master.Items.Body.HistoryView.ItemKey": "Key", "Component.Namespace.Master.Items.Body.HistoryView.ItemOldValue": "Old Value", "Component.Namespace.Master.Items.Body.HistoryView.ItemNewValue": " New Value", "Component.Namespace.Master.Items.Body.HistoryView.ItemComment": "Comment", "Component.Namespace.Master.Items.Body.HistoryView.NewAdded": "Add", "Component.Namespace.Master.Items.Body.HistoryView.Updated": "Update", "Component.Namespace.Master.Items.Body.HistoryView.Deleted": "Delete", "Component.Namespace.Master.Items.Body.HistoryView.LoadMore": "Load more", "Component.Namespace.Master.Items.Body.HistoryView.NoHistory": "No Change History", "Component.Namespace.Master.Items.Body.HistoryView.FilterHistory": "Filter History", "Component.Namespace.Master.Items.Body.HistoryView.FilterHistory.SortByKey": "Filter the history by key", "Component.Namespace.Master.Items.Body.Instance.Tips": "Tips: Only show instances who have fetched configurations in the last 24 hrs ", "Component.Namespace.Master.Items.Body.Instance.UsedNewItem": "Instances using the latest configuration", "Component.Namespace.Master.Items.Body.Instance.NoUsedNewItem": "Instances using outdated configuration", "Component.Namespace.Master.Items.Body.Instance.AllInstance": "All Instances", "Component.Namespace.Master.Items.Body.Instance.RefreshList": "Refresh List", "Component.Namespace.Master.Items.Body.Instance.ToSeeItem": "View Configuration", "Component.Namespace.Master.Items.Body.Instance.LoadMore": "Load more", "Component.Namespace.Master.Items.Body.Instance.ItemAppId": "App ID", "Component.Namespace.Master.Items.Body.Instance.ItemCluster": "Cluster Name", "Component.Namespace.Master.Items.Body.Instance.ItemDataCenter": "Data Center", "Component.Namespace.Master.Items.Body.Instance.ItemIp": "IP", "Component.Namespace.Master.Items.Body.Instance.ItemGetTime": "Configuration fetched time", "Component.Namespace.Master.Items.Body.Instance.NoInstanceTips": "No Instance Information", "Component.PublishDeny.Title": "Release Restriction", "Component.PublishDeny.Tips1": "You can't release! The operators to edit and release the configurations in {{env}} environment must be different, please find someone else who has the release permission of this namespace to do the release operation.", "Component.PublishDeny.Tips2": "(If it is non working time or a special situation, you may release by clicking the 'Emergency Release' button.)", "Component.PublishDeny.EmergencyPublish": "Emergency Release", "Component.PublishDeny.Close": "Close", "Component.Publish.Title": "Release", "Component.Publish.Tips": "(Only the released configurations can be fetched by clients, and this release will only be applied to the current environment: {{env}})", "Component.Publish.Grayscale": "Grayscale Release", "Component.Publish.GrayscaleTips": "(The grayscale configurations are only applied to the instances specified in grayscale rules)", "Component.Publish.AllPublish": "Full Release", "Component.Publish.AllPublishTips": "(Full release configurations are applied to all instances)", "Component.Publish.ToSeeChange": "View changes", "Component.Publish.CompareWithMasterValue": "Compare with master", "Component.Publish.CompareWithPublishedValue": "Compare with published", "Component.Publish.PublishedValue": "Released values", "Component.Publish.Changes": "Changes", "Component.Publish.Key": "Key", "Component.Publish.NoPublishedValue": "Unreleased values", "Component.Publish.ModifyUser": "Modifier", "Component.Publish.ModifyTime": "Modified Time", "Component.Publish.ModifyRecord": "Record", "Component.Publish.NewAdded": "New", "Component.Publish.NewAddedTips": "New Configuration", "Component.Publish.Modified": "Modified", "Component.Publish.ModifiedTips": "Modified Configuration", "Component.Publish.Deleted": "Deleted", "Component.Publish.DeletedTips": "Deleted Configuration", "Component.Publish.MasterValue": "Main version value", "Component.Publish.GrayValue": "Grayscale version value", "Component.Publish.GrayPublishedValue": "Released grayscale version value", "Component.Publish.GrayNoPublishedValue": "Unreleased grayscale version value", "Component.Publish.ItemNoChange": "No configuration changes", "Component.Publish.GrayItemNoChange": "No configuration changes", "Component.Publish.NoGrayItems": "No grayscale changes", "Component.Publish.Release": "Release Name", "Component.Publish.ReleaseComment": "Comment", "Component.Publish.OpPublish": "Release", "Component.Rollback.To": "roll back to", "Component.Rollback.Tips": "This operation will roll back to the last released version, and the current version is abandoned, but there is no impact to the currently editing configurations. You may view the currently effective version in the release history page", "Component.RollbackTo.Tips": "This operation will roll back to this released version, and the current version is abandoned, but there is no impact to the currently editing configurations", "Component.Rollback.ClickToView": "Click to view", "Component.Rollback.ItemType": "Type", "Component.Rollback.ItemKey": "Key", "Component.Rollback.RollbackBeforeValue": "Before Rollback", "Component.Rollback.RollbackAfterValue": "After Rollback", "Component.Rollback.Added": "Add", "Component.Rollback.Modified": "Update", "Component.Rollback.Deleted": "Delete", "Component.Rollback.NoChange": "No configuration changes", "Component.Rollback.OpRollback": "Rollback", "Component.ShowText.Title": "View", "Login.Login": "Login", "Login.UserNameOrPasswordIncorrect": "Incorrect username or password", "Login.LogoutSuccessfully": "Logout Successfully", "Index.MyProject": "My projects", "Index.CreateProject": "Create project", "Index.LoadMore": "Load more", "Index.FavoriteItems": "Favorite projects", "Index.Topping": "Top", "Index.FavoriteCancel": "Remove favorite", "Index.FavoriteTip": "You haven't favorited any items yet. You can favorite items on the project homepage.", "Index.RecentlyViewedItems": "Recent projects", "Index.GetCreateAppRoleFailed": "Failed to get the information of create project permission", "Index.Topped": "Top Successfully", "Index.CancelledFavorite": "Remove favorite successfully", "Index.PublicNamespace": "Public namespaces", "Index.SearchNamespace": "Search Public Namespace(AppId or Namespace)", "Index.PublicNamespaceTip": "You haven't created any public namespaces yet. You can create them in your projects.", "Index.appTable.operation": "Operation", "Index.appTable.Format": "Format", "Index.appTable.Comment": "Comment", "Cluster.CreateCluster": "Create Cluster", "Cluster.Tips.1": "By adding clusters, the same program can use different configuration in different clusters (such as different data centers)", "Cluster.Tips.2": "If the different clusters use the same configuration, there is no need to create clusters", "Cluster.Tips.3": "By default, Apollo reads IDC attributes in /opt/settings/server.properties(Linux) or C:\\opt\\settings\\server.properties(Windows) files on the machine as cluster names, such as SHAJQ (Jinqiao Data Center), SHAOY (Ouyang Data Center)", "Cluster.Tips.4": "The cluster name created here should be consistent with the IDC attribute in server.properties on the machine", "Cluster.CreateNameTips": "(Cluster names such as SHAJQ, SHAOY or customized clusters such as SHAJQ-xx, SHAJQ-yy)", "Cluster.CreateRemarksTips": "(Adding remarks to clusters can help users better understand the purpose of each cluster.)", "Cluster.ChooseEnvironment": "Environment", "Cluster.LoadingEnvironmentError": "Error in loading environment information", "Cluster.ClusterCreated": "Created cluster successfully", "Cluster.ClusterCreateFailed": "Failed to create cluster", "Cluster.PleaseChooseEnvironment": "Please select the environment", "Cluster.Grant": "Authorize", "Cluster.GrantTips": "Manage the configuration edit and release permission", "Cluster.Role.Title": "Cluster Permission Management", "Cluster.Role.GrantModifyTo": "Permission to edit", "Cluster.Role.GrantModifyTo2": "(Can edit the configuration)", "Cluster.Role.GrantPublishTo": "Permission to release", "Cluster.Role.GrantPublishTo2": "(Can release the configuration)", "Cluster.Role.Add": "Add", "Cluster.Role.NoPermission": "You do not have permission!", "Config.Title": "Apollo Configuration Center", "Config.AppIdNotFound": "doesn't exist, ", "Config.ClickByCreate": "click to create", "Config.EnvList": "Environments", "Config.EnvListTips": "Manage the configuration of different environments and clusters by switching environments and clusters", "Config.ProjectInfo": "Project Info", "Config.ModifyBasicProjectInfo": "Modify project's basic information", "Config.Favorite": "Favorite", "Config.CancelFavorite": "Cancel Favorite", "Config.MissEnv": "Missing environment", "Config.MissNamespace": "Missing Namespace", "Config.ProjectManage": "Manage Project", "Config.AccessKeyManage": "Manage AccessKey", "Config.CreateAppMissEnv": "Recover Environments", "Config.CreateAppMissNamespace": "Recover Namespaces", "Config.AddCluster": "Add Cluster", "Config.AddNamespace": "Add Namespace", "Config.CurrentlyOperatorEnv": "Current environment", "Config.DoNotRemindAgain": "No longer prompt", "Config.Note": "Note", "Config.ClusterIsDefaultTipContent": "All instances that do not belong to the '{{name}}' cluster will fetch the default cluster (current page) configuration, and those that belong to the '{{name}}' cluster will use the corresponding cluster configuration!", "Config.ClusterIsCustomTipContent": "Instances belonging to the '{{name}}' cluster will only fetch the configuration of the '{{name}}' cluster (the current page), and the default cluster configuration will only be fetched when the corresponding namespace has not been released in the current cluster.", "Config.HasNotPublishNamespace": "The following environment/cluster has unreleased configurations, the client will not fetch the unreleased configurations, please release them in time.", "Config.RevokeItem.DialogTitle": "Revoke configuration changes", "Config.RevokeItem.DialogContent": "Modified but unpublished configurations in the current namespace will be revoked. Are you sure to revoke the configuration changes?", "Config.DeleteItem.DialogTitle": "Delete configuration", "Config.DeleteItem.DialogContent": "You are deleting the configuration whose Key is '{{config.key}}' Value is '{{config.value}}'.
Are you sure to delete the configuration?", "Config.PublishNoPermission.DialogTitle": "Release", "Config.PublishNoPermission.DialogContent": "You do not have release permission. Please ask the project administrators '{{masterUsers}}' to authorize release permission.", "Config.ModifyNoPermission.DialogTitle": "Apply for Configuration Permission", "Config.ModifyNoPermission.DialogContent": "Please ask the project administrators '{{masterUsers}}' to authorize release or edit permission.", "Config.MasterNoPermission.DialogTitle": "Apply for Configuration Permission", "Config.MasterNoPermission.DialogContent": "You are not this project's administrator. Only project administrators have the permission to add clusters and namespaces. Please ask the project administrators '{{masterUsers}}' to assign administrator permission", "Config.NamespaceLocked.DialogTitle": "Edit not allowed", "Config.NamespaceLocked.DialogContent": "Current namespace is being edited by '{{lockOwner}}', and a release phase can only be edited by one person.", "Config.RollbackAlert.DialogTitle": "Rollback", "Config.RollbackAlert.DialogContent": "Are you sure to roll back?", "Config.EmergencyPublishAlert.DialogTitle": "Emergency release", "Config.EmergencyPublishAlert.DialogContent": "Are you sure to perform the emergency release?", "Config.DeleteBranch.DialogTitle": "Delete grayscale", "Config.DeleteBranch.DialogContent": "Deleting grayscale will lose the grayscale configurations. Are you sure to delete it?", "Config.UpdateRuleTips.DialogTitle": "Update gray rule prompt", "Config.UpdateRuleTips.DialogContent": "Grayscale rules are in effect. However there are unreleased configurations in grayscale version, they won't be effective until a manual grayscale release is performed.", "Config.MergeAndReleaseDeny.DialogTitle": "Full release", "Config.MergeAndReleaseDeny.DialogContent": "The main version has unreleased configuration. Please release the main version first.", "Config.GrayReleaseWithoutRulesTips.DialogTitle": "Missing gray rule prompt", "Config.GrayReleaseWithoutRulesTips.DialogContent": "The grayscale version has not configured any grayscale rule. Please configure the grayscale rules.", "Config.DeleteNamespaceDenyForMasterInstance.DialogTitle": "Delete namespace warning", "Config.DeleteNamespaceDenyForMasterInstance.DialogContent": "There are '{{deleteNamespaceContext.namespace.instancesCount}}' instances using namespace ('{{deleteNamespaceContext.namespace.baseInfo.namespaceName}}'), and deleting namespace would cause those instances failed to fetch configuration.
Please go to \"Instance List\" to confirm the instance information. If you confirm that the relevant instances are no longer using the namespace configuration, you can contact the Apollo administrators to delete the instance information (Instance Config) or wait for the instance to expire automatically 24 hours before deletion.", "Config.DeleteNamespaceDenyForBranchInstance.DialogTitle": "Delete namespace warning information", "Config.DeleteNamespaceDenyForBranchInstance.DialogContent": "There are '{{deleteNamespaceContext.namespace.branch.latestReleaseInstances.total}}' instances using the grayscale version of namespace ('{{deleteNamespaceContext.namespace.baseInfo.namespaceName}}') configuration, and deleting Namespace would cause those instances failed to fetch configuration.
Please go to \"Grayscale Version\"=> \"Instance List\" to confirm the instance information. If you confirm the relevant instances are no longer using the namespace configuration, you can contact the Apollo administrators to delete the instance information (Instance Config) or wait for the instance to expire automatically 24 hours before deletion.", "Config.DeleteNamespaceDenyForPublicNamespace.DialogTitle": "Delete Namespace Failure Tip", "Config.DeleteNamespaceDenyForPublicNamespace.DialogContent": "Delete Namespace Failure Tip", "Config.DeleteNamespaceDenyForPublicNamespace.PleaseEnterAppId": "Please enter appId", "Config.SyntaxCheckFailed.DialogTitle": "Syntax Check Error", "Config.SyntaxCheckFailed.DialogContent": "Delete Namespace Failure Tip", "Config.CreateBranchTips.DialogTitle": "Create Grayscale Notice", "Config.CreateBranchTips.DialogContent": "By creating grayscale version, you can do grayscale test for some configurations.
Grayscale process is as follows:
    1. Create grayscale version
    2. Configure grayscale configuration items
    3. Configure grayscale rules. If it is a private namespace, it can be grayed according to the IP and Label of client. If it is a public namespace, it can be grayed according to appId, IP and Label.
    4. Grayscale release
Grayscale version has two final results: Full release and Abandon grayscale
Full release: grayscale configurations are merged with the main version and released, all clients will use the merged configurations
Abandon grayscale: Delete grayscale version. All clients will use the configurations of the main version
Notice:
    1. If the grayscale version has been released, then the grayscale rules will be effective immediately without the need to release grayscale configuration again.", "Config.ProjectMissEnvInfos": "There are missing environments in the current project, please click \"Recover Environments\" on the left side of the page to do the recovery.", "Config.ProjectMissNamespaceInfos": "There are missing namespaces in the current environment. Please click \"Recover Namespaces\" on the left side of the page to do the recovery.", "Config.SystemError": "System error, please try again or contact the system administrator", "Config.FavoriteSuccessfully": "Favorite Successfully", "Config.FavoriteFailed": "Failed to favorite", "Config.CancelledFavorite": "Cancel favorite successfully", "Config.CancelFavoriteFailed": "Failed to cancel the favorite", "Config.GetUserInfoFailed": "Failed to obtain user login information", "Config.LoadingAllNamespaceError": "Failed to load configuration", "Config.CancelFavoriteError": "Failure to Cancel Collection", "Config.Deleted": "Delete Successfully", "Config.DeleteFailed": "Failed to delete", "Config.GrayscaleCreated": "Create Grayscale Successfully", "Config.GrayscaleCreateFailed": "Failed to create grayscale", "Config.BranchDeleted": "Delete branch successfully", "Config.BranchDeleteFailed": "Failed to delete branch", "Config.DeleteNamespaceFailedTips": "The following projects are associated with this public namespace and they must all be deleted deleting the public Namespace", "Config.DeleteNamespaceNoPermissionFailedTitle": "Failed to delete", "Config.DeleteNamespaceNoPermissionFailedTips": "You do not have Project Administrator permission. Only Administrators can delete namespace. Please ask Project Administrators [{{users}}] to delete namespace.", "Config.Key": "Key", "Config.Value": "Value", "Config.Comment": "Comment", "Config.Operation": "Operation", "Config.Add": "Add Config", "Config.SortByKey": "Filter Config by Key", "Config.FilterConfig": "Filter", "Config.Reset": "Reset", "Config.ManageCluster": "Manage Cluster", "Cluster.Role.InitClusterPermissionError": "Error initializing authorization", "Cluster.Role.GetGrantUserError": "Failed to load authorized users", "Cluster.Role.PleaseChooseUser": "Please select the user", "Cluster.Role.Added": "Add Successfully", "Cluster.Role.AddFailed": "Failed to add", "Cluster.Role.Deleted": "Delete Successfully", "Cluster.Role.DeleteFailed": "Failed to Delete", "Delete.Title": "Delete applications, clusters, AppNamespace", "Delete.DeleteApp": "Delete application", "Delete.DeleteAppTips": "(Because deleting applications has very large impacts, only system administrators are allowed to delete them for the time being. Make sure that no client fetches the configuration of the application before deleting it.)", "Delete.AppIdTips": "(Please query application information before deleting)", "Delete.AppInfo": "Application information", "Delete.DeleteCluster": "Delete clusters", "Delete.DeleteClusterTips": "(Because deleting clusters has very large impacts, only system administrators are allowed to delete them for the time being. Make sure that no client fetches the configuration of the cluster before deleting it.)", "Delete.EnvName": "Environment Name", "Delete.ClusterNameTips": "(Please query cluster information before deletion)", "Delete.ClusterInfo": "Cluster information", "Delete.DeleteNamespace": "Delete AppNamespace", "Delete.DeleteNamespaceTips": "(Note that Namespace and AppNamespace in all environments will be deleted!", "Delete.DeleteNamespaceTips2": "For public Namespace, it is necessary to ensure that no application associates the AppNamespace", "Delete.AppNamespaceName": "AppNamespace name", "Delete.AppNamespaceNameTips": "(For non-properties namespaces, please add the suffix, such as apollo.xml)", "Delete.AppNamespaceInfo": "AppNamespace Information", "Delete.IsRootUserTips": "The current page is only accessible to Apollo administrators", "Delete.PleaseEnterAppId": "Please enter appId", "Delete.AppIdNotFound": "AppId: '{{appId}}' does not exist!", "Delete.AppInfoContent": "Application name: '{{appName}}' department: '{{departmentName}}({{departmentId}})' owner: '{{ownerName}}'", "Delete.ConfirmDeleteAppId": "Are you sure to delete AppId: '{{appId}}'?", "Delete.Deleted": "Delete Successfully", "Delete.PleaseEnterAppIdAndEnvAndCluster": "Please enter appId, environment, and cluster name", "Delete.ClusterInfoContent": "AppId: '{{appId}}' environment: '{{env}}' cluster name: '{{clusterName}}'", "Delete.ConfirmDeleteCluster": "Are you sure to delete the cluster? AppId: '{{appId}}' environment: '{{env}}' cluster name:'{{clusterName}}'", "Delete.PleaseEnterAppIdAndNamespace": "Please enter appId and AppNamespace names", "Delete.AppNamespaceInfoContent": "AppId: '{{appId}}' AppNamespace name: '{{namespace}}' isPublic: '{{isPublic}}'", "Delete.ConfirmDeleteNamespace": "Are you sure to delete AppNamespace and Namespace for all environments? AppId: '{{appId}}' environment: 'All environments' AppNamespace name: '{{namespace}}'", "Namespace.Title": "New Namespace", "Namespace.UnderstandMore": "(Click to learn more about Namespace)", "Namespace.Link.Tips1": "Applications can override the configuration of a public namespace by associating a public namespace", "Namespace.Link.Tips2": "If the application does not need to override the configuration of the public namespace, then there is no need to associate the public namespace", "Namespace.CreatePublic.Tips1": "The configuration of the public Namespace can be fetched by any application", "Namespace.CreatePublic.Tips2": "Configuration of public components or the need for multiple applications to share the same configuration can be achieved by creating a public namespace.", "Namespace.CreatePublic.Tips3": "If other applications need to override the configuration of the public namespace, you can associate the public namespace in other applications, and then configure the configuration that needs to be overridden in the associated namespace.", "Namespace.CreatePublic.Tips4": "If other applications do not need to override the configuration of public namespace, then there is no need to associate public namespace in other applications.", "Namespace.CreatePrivate.Tips1": "The configuration of a private Namespace can only be fetched by the application to which it belongs.", "Namespace.CreatePrivate.Tips2": "Group management configuration can be achieved by creating a private namespace", "Namespace.CreatePrivate.Tips3": "The format of private namespaces can be xml, yml, yaml, json, txt. You can get the content of namespace in non-property format through the ConfigFile interface in apollo-client.", "Namespace.CreatePrivate.Tips4": "The 1.3.0 and above versions of apollo-client provide better support for yaml/yml. Config objects can be obtained directly through ConfigService.getConfig(\"someNamespace.yml\"), or through @EnableApolloConfig(\"someNamespace.yml\") or apollo.bootstrap.namespaces=someNamespace.yml to inject YML configuration into Spring/Spring Boot", "Namespace.CreateNamespace": "Create Namespace", "Namespace.AssociationPublicNamespace": "Associate Public Namespace", "Namespace.ChooseCluster": "Select Cluster", "Namespace.NamespaceName": "Name", "Namespace.AutoAddDepartmentPrefix": "Add department prefix", "Namespace.AutoAddDepartmentPrefixTips": "(The name of a public namespace needs to be globally unique, and adding a department prefix helps ensure global uniqueness)", "Namespace.NamespaceType": "Type", "Namespace.NamespaceType.Public": "Public", "Namespace.NamespaceType.Private": "Private", "Namespace.Remark": "Remarks", "Namespace.Namespace": "Namespace", "Namespace.PleaseChooseNamespace": "Please select namespace", "Namespace.LoadingPublicNamespaceError": "Failed to load public namespace", "Namespace.LoadingAppInfoError": "Failed to load App information", "Namespace.PleaseChooseCluster": "Select Cluster", "Namespace.CheckNamespaceNameLengthTip": "The namespace name should not be longer than 32 characters. Department prefix:'{{departmentLength}}' characters, name {{namespaceLength}} characters", "ServiceConfig.Title": "System Configuration", "ServiceConfig.PortalDB.Tips": "(Maintain Apollo PortalDB.ServerConfig table data, will override configuration items if they already exist in the edit operation, or create configuration items. Configuration updates take effect automatically in a minute)", "ServiceConfig.ConfigDB.Tips": "(Maintain Apollo ConfigDB.ServerConfig table data, will override configuration items if they already exist in the edit operation, or create configuration items. Configuration updates take effect automatically in a minute)", "ServiceConfig.PortalDB.Tab": "PortalDB configuration management", "ServiceConfig.ConfigDB.Tab": "ConfigDB configuration management", "ServiceConfig.Switch.Env": "Switch Environment", "ServiceConfig.Key": "Key", "ServiceConfig.KeyTips": "(Please query the configuration information before modifying the configuration)", "ServiceConfig.Value": "Value", "ServiceConfig.Comment": "Comment", "ServiceConfig.Saved": "Save Successfully", "ServiceConfig.SaveFailed": "Failed to Save", "ServiceConfig.PleaseEnterKey": "Please enter key", "ServiceConfig.KeyNotExistsAndCreateTip": "Key: '{{key}}' does not exist. Click Save to create the configuration item.", "ServiceConfig.KeyExistsAndSaveTip": "Key: '{{key}}' already exists. Click Save will override the configuration item.", "AccessKey.Tips.1": "Add up to 5 access keys per environment.", "AccessKey.Tips.2": "Once the environment has any enabled access key, the client will be required to configure access key, or the configurations cannot be obtained.", "AccessKey.Tips.3": "Observed access keys are used for pre-check and logging only. Note: Once the environment has any enabled access key, the observed status will no longer take effect.", "AccessKey.Tips.4": "Configure the access key to prevent unauthorized clients from obtaining the application configuration. The configuration method is as follows(only apollo-client version 1.6.0+):", "AccessKey.Tips.4.1": "Via jvm parameter: apollo-client version >=1.9.0 is recommended to use -Dapollo.access-key.secret; other versions use -Dapollo.accesskey.secret", "AccessKey.Tips.4.2": "Through the os environment variable: apollo-client version >=1.9.0 is recommended to use APOLLO_ACCESS_KEY_SECRET; other versions use APOLLO_ACCESSKEY_SECRET", "AccessKey.Tips.4.3": "Via META-INF/app.properties or application.properties: apollo-client version >=1.9.0 is recommended to use apollo.access-key.secret; other versions use apollo.accesskey.secret(note that the multi-environment secret is different)", "AccessKey.NoAccessKeyServiceTips": "There are no access keys in this environment.", "AccessKey.ConfigAccessKeys.Secret": "Access Key Secret", "AccessKey.ConfigAccessKeys.Status": "Status", "AccessKey.ConfigAccessKeys.LastModify": "Last Modifier", "AccessKey.ConfigAccessKeys.LastModifyTime": "Last Modified Time", "AccessKey.ConfigAccessKeys.Operator": "Operation", "AccessKey.Operator.Disable": "Disable", "AccessKey.Operator.Enable": "Enable", "AccessKey.Operator.Observe": "Observe", "AccessKey.Operator.Disabled": "Disabled", "AccessKey.Operator.Enabled": "Enabled", "AccessKey.Operator.Observed": "Observed", "AccessKey.Operator.Remove": "Remove", "AccessKey.Operator.CreateSuccess": "Access key created successfully", "AccessKey.Operator.DisabledSuccess": "Access key disabled successfully", "AccessKey.Operator.EnabledSuccess": "Access key enabled successfully", "AccessKey.Operator.ObservedSuccess": "Access key observed successfully", "AccessKey.Operator.RemoveSuccess": "Access key removed successfully", "AccessKey.Operator.CreateError": "Access key created failed", "AccessKey.Operator.DisabledError": "Access key disabled failed", "AccessKey.Operator.EnabledError": "Access key enabled failed", "AccessKey.Operator.ObservedError": "Access key observed failed", "AccessKey.Operator.RemoveError": "Access key removed failed", "AccessKey.Operator.DisabledTips": "Are you sure you want to disable the access key?", "AccessKey.Operator.EnabledTips": "Are you sure you want to enable the access key?", "AccessKey.Operator.ObservedTips": "Are you sure you want to observe the access key?", "AccessKey.Operator.RemoveTips": "Are you sure you want to remove the access key?", "AccessKey.LoadError": "Error Loading access keys", "SystemInfo.Title": "System Information", "SystemInfo.SystemVersion": "System version", "SystemInfo.Tips1": "The environment list comes from the apollo.portal.envs configuration in Apollo PortalDB.ServerConfig, and can be configured in System Configuration page. For more information, please refer the apollo.portal.envs - supportable environment list section in Distributed Deployment Guide.", "SystemInfo.Tips2": "The meta server address shows the meta server information for this environment configuration. For more information, please refer the Configuring meta service information for apollo-portal section in Distributed Deployment Guide.", "SystemInfo.Active": "Active", "SystemInfo.ActiveTips": "(Current environment status is abnormal, please diagnose with the system information below and Check Health results of AdminService)", "SystemInfo.MetaServerAddress": "Meta server address", "SystemInfo.ConfigServices": "Config Services", "SystemInfo.ConfigServices.Name": "Name", "SystemInfo.ConfigServices.InstanceId": "Instance Id", "SystemInfo.ConfigServices.HomePageUrl": "Home Page Url", "SystemInfo.ConfigServices.CheckHealth": "Check Health", "SystemInfo.NoConfigServiceTips": "No config service found!", "SystemInfo.Check": "Check", "SystemInfo.AdminServices": "Admin Services", "SystemInfo.AdminServices.Name": "Name", "SystemInfo.AdminServices.InstanceId": "Instance Id", "SystemInfo.AdminServices.HomePageUrl": "Home Page Url", "SystemInfo.AdminServices.CheckHealth": "Check Health", "SystemInfo.NoAdminServiceTips": "No admin service found!", "SystemInfo.IsRootUser": "The current page is only accessible to Apollo administrators", "SystemRole.Title": "System Permission Management", "SystemRole.AddCreateAppRoleToUser": "Create application permission for users", "SystemRole.AddCreateAppRoleToUserTips": "(When role.create-application.enabled=true is set in system configurations, only super admin and those accounts with Create application permission can create application)", "SystemRole.ChooseUser": "Select User", "SystemRole.Add": "Add", "SystemRole.AuthorizedUser": "Users with permission", "SystemRole.ModifyAppAdminUser": "Modify Application Administrator Allocation Permissions", "SystemRole.ModifyAppAdminUserTips": "(When role.manage-app-master.enabled=true is set in system configurations, only super admin and those accounts with application administrator allocation permissions can modify the application's administrators)", "SystemRole.AppIdTips": "(Please query the application information first)", "SystemRole.AppInfo": "Application information", "SystemRole.AllowAppMasterAssignRole": "Allow this user to add Master as an administrator", "SystemRole.DeleteAppMasterAssignRole": "Disallow this user to add Master as an administrator", "SystemRole.IsRootUser": "The current page is only accessible to Apollo administrators", "SystemRole.PleaseChooseUser": "Please select a user", "SystemRole.Added": "Add Successfully", "SystemRole.AddFailed": "Failed to add", "SystemRole.Deleted": "Delete Successfully", "SystemRole.DeleteFailed": "Failed to Delete", "SystemRole.GetCanCreateProjectUsersError": "Error getting user list with create project permission", "SystemRole.PleaseEnterAppId": "Please enter appId", "SystemRole.AppIdNotFound": "AppId: '{{appId}}' does not exist!", "SystemRole.AppInfoContent": "Application name: '{{appName}}' department: '{{departmentName}}({{departmentId}})' owner: '{{ownerName}}'", "SystemRole.DeleteMasterAssignRoleTips": "Are you sure to disallow the user '{{userId}}' to add Master as an administrator for AppId:'{{appId}}'?", "SystemRole.DeletedMasterAssignRoleTips": "Disallow the user '{{userId}}' to add Master as an administrator for AppId:'{{appId}}' Successfully", "SystemRole.AllowAppMasterAssignRoleTips": "Are you sure to allow the user '{{userId}}' to add Master as an administrator for AppId:'{{appId}}'?", "SystemRole.AllowedAppMasterAssignRoleTips": "Allow the user '{{userId}}' to add Master as an administrator for AppId:'{{appId}}' Successfully", "UserMange.Title": "User Management", "UserMange.TitleTips": "(Only valid for the default Spring Security simple authentication method: - Dapollo_profile = github,auth)", "UserMange.UserName": "User Login Name", "UserMange.UserDisplayName": "User Display Name", "UserMange.Pwd": "Password", "UserMange.ConfirmPwd": "Confirm Password", "UserMange.PwdNotMatch": "Passwords do not match", "UserMange.Email": "Email", "UserMange.Created": "Create user successfully", "UserMange.CreateFailed": "Failed to create user", "UserMange.Edited": "Edit user successfully", "UserMange.EditFailed": "Failed to edit user", "UserMange.Enabled.succeed": "Change user enabled successfully", "UserMange.Enabled.failure": "Failed to change user enabled", "UserMange.Enabled": "Enabled", "UserMange.Enable": "Enable", "UserMange.Disable": "Disable", "UserMange.Operation": "Operation", "UserMange.Edit": "Edit", "UserMange.Add": "Add new user", "UserMange.Back": "Back", "UserMange.SortByUserLoginName": "Filter user by login name", "UserMange.FilterUser": "Filter", "UserMange.Reset": "Reset", "UserMange.Save": "Save", "UserMange.Cancel": "Cancel", "Open.Manage.Title": "Open Platform", "Open.Manage.CreateThirdApp": "Create third-party applications", "Open.Manage.CreateThirdAppTips": "(Note: Third-party applications can manage configuration through Apollo Open Platform)", "Open.Manage.ThirdAppId": "Third party appId", "Open.Manage.ThirdAppIdTips": "(Please check if the third-party application has already exists first)", "Open.Manage.ThirdAppName": "Third party application name", "Open.Manage.ThirdAppNameTips": "(Suggested format xx-yy-zz e.g. apollo-server)", "Open.Manage.ProjectOwner": "Owner", "Open.Manage.Create": "Create", "Open.Manage.GrantPermission": "Authorization", "Open.Manage.GrantPermissionTips": "(Namespace level permissions include edit and release namespace. Application level permissions include creating namespace, edit or release any namespace in the application.)", "Open.Manage.Token": "Token", "Open.Manage.ManagedAppId": "Managed AppId", "Open.Manage.ManagedNamespace": "Managed Namespace", "Open.Manage.ManagedNamespaceTips": "(For non-properties namespaces, please add the suffix, such as apollo.xml)", "Open.Manage.GrantType": "Authorization type", "Open.Manage.GrantType.Namespace": "Namespace", "Open.Manage.GrantType.App": "App", "Open.Manage.GrantEnv": "Environments", "Open.Manage.GrantEnvTips": "(If you don't select any environment, then will have permissions to all environments.)", "Open.Manage.PleaseEnterAppId": "Please enter appId", "Open.Manage.AppNotCreated": "App('{{appId}}') does not exist, please create it first", "Open.Manage.GrantSuccessfully": "Authorize Successfully", "Open.Manage.GrantFailed": "Failed to authorize", "Open.Manage.ViewAndGrantPermission": "Grant Permission", "Open.Manage.DeleteConsumer.Confirm": "You are deleting a third-party app with AppId={{toOperationConsumer.appId}},AppName={{toOperationConsumer.name}},
Are you sure you want to delete?", "Open.Manage.DeleteConsumer.Success": "Third-party app deleted successfully", "Open.Manage.DeleteConsumer.Error": "Third-party app deletion failed", "Open.Manage.CreateConsumer.Button": "Create Third-Party App", "Open.Manage.Consumer.AllowCreateApplication": "Allow app creation?", "Open.Manage.Consumer.AllowCreateApplicationTips": "(Allow third-party applications to create apps and grant them app administrator privileges.", "Open.Manage.Consumer.AllowCreateApplication.No": "no", "Open.Manage.Consumer.AllowCreateApplication.Yes": "yes", "Open.Manage.Consumer.RateLimit.Enabled": "Whether to enable rate limit", "Open.Manage.Consumer.RateLimit.Enabled.Tips": "(After enabling this feature, when third-party applications publish configurations on Apollo, their traffic will be controlled according to the configured QPS limit)", "Open.Manage.Consumer.RateLimitValue": "Rate limiting QPS", "Open.Manage.Consumer.RateLimitValueTips": "(Unit: times/second, for example: 100 means that the configuration is published at most 100 times per second)", "Open.Manage.Consumer.RateLimitValue.Error": "The minimum rate limiting QPS is 1", "Open.Manage.Consumer.RateLimitValue.Display": "Unlimited", "Namespace.Role.Title": "Permission Management", "Namespace.Role.GrantModifyTo": "Permission to edit", "Namespace.Role.GrantModifyTo2": "(Can edit the configuration)", "Namespace.Role.AllEnv": "All environments", "Namespace.Role.GrantPublishTo": "Permission to release", "Namespace.Role.GrantPublishTo2": "(Can release the configuration)", "Namespace.Role.Add": "Add", "Namespace.Role.NoPermission": "You do not have permission!", "Namespace.Role.InitNamespacePermissionError": "Error initializing authorization", "Namespace.Role.GetEnvGrantUserError": "Failed to load authorized users for '{{env}}'", "Namespace.Role.GetGrantUserError": "Failed to load authorized users", "Namespace.Role.PleaseChooseUser": "Please select the user", "Namespace.Role.Added": "Add Successfully", "Namespace.Role.AddFailed": "Failed to add", "Namespace.Role.Deleted": "Delete Successfully", "Namespace.Role.DeleteFailed": "Failed to Delete", "Config.Sync.Title": "Synchronize Configuration", "Config.Sync.FistStep": "(Step 1: Select Synchronization Information)", "Config.Sync.SecondStep": "(Step 2: Check Diff)", "Config.Sync.PreviousStep": "Previous step", "Config.Sync.NextStep": "Next step", "Config.Sync.Sync": "Synchronize", "Config.Sync.Tips": "Tips", "Config.Sync.Tips1": "Configurations between multiple environments and clusters can be maintained by synchronize configuration", "Config.Sync.Tips2": "It should be noted that the configurations will not take effect until they are released after synchronization.", "Config.Sync.SyncNamespace": "Synchronized Namespace", "Config.Sync.SyncToCluster": "Synchronize to which cluster", "Config.Sync.NeedToSyncItem": "Configuration to synchronize", "Config.Sync.SortByLastModifyTime": "Filter by last update time", "Config.Sync.BeginTime": "Start time", "Config.Sync.EndTime": "End time", "Config.Sync.Filter": "Filter", "Config.Sync.Rest": "Reset", "Config.Sync.ItemKey": "Key", "Config.Sync.ItemValue": "Value", "Config.Sync.ItemCreateTime": "Create Time", "Config.Sync.ItemUpdateTime": "Update Time", "Config.Sync.NoNeedSyncItem": "No updated configuration", "Config.Sync.IgnoreSync": "Ignore synchronization", "Config.Sync.Step2Type": "Type", "Config.Sync.Step2Key": "Key", "Config.Sync.Step2SyncBefore": "Before Sync", "Config.Sync.Step2SyncAfter": "After Sync", "Config.Sync.Step2Comment": "Comment", "Config.Sync.Step2Operator": "Operation", "Config.Sync.NewAdd": "Add", "Config.Sync.NoSyncItem": "Do not synchronize the configuration", "Config.Sync.Delete": "Delete", "Config.Sync.Update": "Update", "Config.Sync.SyncSuccessfully": "Synchronize Successfully!", "Config.Sync.SyncFailed": "Failed to Synchronize!", "Config.Sync.LoadingItemsError": "Error loading configuration", "Config.Sync.PleaseChooseNeedSyncItems": "Please select the configuration that needs synchronization", "Config.Sync.PleaseChooseCluster": "Select Cluster", "Config.History.Title": "Release History", "Config.History.MasterVersionPublish": "Main version release", "Config.History.MasterVersionRollback": "Main version rollback", "Config.History.GrayscaleOperator": "Grayscale operation", "Config.History.PublishHistory": "Release History", "Config.History.OperationType0": "Normal release", "Config.History.OperationType1": "Rollback", "Config.History.OperationType2": "Grayscale Release", "Config.History.OperationType3": "Update Gray Rules", "Config.History.OperationType4": "Full Grayscale Release", "Config.History.OperationType5": "Grayscale Release(Main Version Release)", "Config.History.OperationType6": "Grayscale Release(Main Version Rollback)", "Config.History.OperationType7": "Abandon Grayscale", "Config.History.OperationType8": "Delete Grayscale(Full Release)", "Config.History.UrgentPublish": "Emergency Release", "Config.History.LoadMore": "Load more", "Config.History.Abandoned": "Abandoned", "Config.History.RollbackTo": "Rollback To This Release", "Config.History.RollbackToTips": "Rollback released configuration to this release", "Config.History.ChangedItem": "Changed Configuration", "Config.History.ChangedItemTips": "View changes between this release and the previous release", "Config.History.AllItem": "Full Configuration", "Config.History.AllItemTips": "View all configurations for this release", "Config.History.ChangeType": "Type", "Config.History.ChangeKey": "Key", "Config.History.ChangeValue": "Value", "Config.History.ChangeOldValue": "Old Value", "Config.History.ChangeNewValue": "New Value", "Config.History.ChangeTypeNew": "Add", "Config.History.ChangeTypeModify": "Update", "Config.History.ChangeTypeDelete": "Delete", "Config.History.NoChange": "No configuration changes", "Config.History.NoItem": "No configuration", "Config.History.GrayscaleRule": "Grayscale Rule", "Config.History.GrayscaleAppId": "Grayscale AppId", "Config.History.GrayscaleIp": "Grayscale IP", "Config.History.NoGrayscaleRule": "No Grayscale Rule", "Config.History.NoPermissionTips": "You are not this project's administrator, nor you have edit or release permission for the namespace. Thus you cannot view the release history.", "Config.History.NoPublishHistory": "No release history", "Config.History.LoadingHistoryError": "No release history", "Config.Diff.Title": "Compare Configuration", "Config.Diff.FirstStep": "(Step 1: Select what to compare)", "Config.Diff.SecondStep": "(Step 2: View the differences)", "Config.Diff.PreviousStep": "Previous step", "Config.Diff.NextStep": "Next step", "Config.Diff.TipsTitle": "Tips", "Config.Diff.Tips": "By comparing configuration, you can see configuration differences between multiple environments and clusters", "Config.Diff.DiffCluster": "Clusters to be compared", "Config.Diff.DiffType": "Diff Type", "Config.Diff.HasDiffComment": "Whether to compare comments or not", "Config.Diff.TextDiff": "Text", "Config.Diff.TableDiff": "Table", "Config.Diff.SearchKey": "search configuration", "Config.Diff.OnlyShowDiffKeys": "Only display configuration items with different values", "Config.Diff.PleaseChooseTwoCluster": "Please select at least two clusters", "Config.Diff.TextDiffMostChooseTwoCluster": "Please select at most two clusters", "ConfigExport.Title": "Config Export/Import", "ConfigExport.Env.TitleTips" : "(The data (application, cluster and namespace) of one cluster can be migrated to another cluster by exporting and importing the configuration)", "ConfigExport.App.TitleTips" : "(All configurations of an application in a specified environment and cluster can be migrated to another cluster by exporting and importing the configuration)", "ConfigExport.SelectExportEnv" : "Select the environment to export", "ConfigExport.SelectImportEnv" : "Select the environment to import", "ConfigExport.ExportTips" : "In case of large amount of data, the export speed is slow. Please wait patiently", "ConfigExport.ImportConflictLabel" : "How to deal with existing namespaces when importing", "ConfigExport.IgnoreExistedNamespace" : "Ignore existing namespaces", "ConfigExport.OverwriteExistedNamespace" : "Overwrite existing namespace", "ConfigExport.UploadFile" : "Upload the exported file", "ConfigExport.UploadFileTip" : "Please upload the exported compressed file", "ConfigExport.ImportSuccess" : "Import success", "ConfigExport.ImportingTip" : "Importing, please wait patiently. After importing, please check whether the namespace configuration is correct. If it is correct, publish the namespace to take effect", "ConfigExport.ImportFailed" : "Import failed", "ConfigExport.ExportFailed" : "Export failed", "ConfigExport.NoPermissionTip" : "You are not this project's administrator. Only project administrators have the permission to export/import configurations.", "ConfigExport.ExportSuccess" : "Exporting data. The data volume will cause slow speed. Please wait patiently", "ConfigExport.ImportTips" : "After the import is completed, please check whether the namespace configuration is correct. After the check is correct, it needs to be published to take effect", "ConfigExport.Export" : "Export", "ConfigExport.Import" : "Import", "ConfigExport.Download": "Download", "ConfigExport.Env.Tab": "Operate By Environment", "ConfigExport.App.Tab": "Operate By Application Cluster", "ConfigExport.ClusterNameTips": "(Please query cluster information before export)", "ConfigExport.PleaseEnterAppIdAndEnvAndCluster": "Please enter appId, environment, and cluster name, and then query", "ConfigExport.ClusterInfoContent": "AppId: '{{appId}}' environment: '{{env}}' cluster name: '{{clusterName}}'", "ConfigExport.EnvName": "Environment Name", "ConfigExport.ClusterInfo": "Cluster information", "ConfigImport.Title": "Import Namespace", "ConfigImport.Tip1": "When the configuration item conflicts, the imported value will overwrite the past value", "ConfigImport.Tip2": "When the configuration items do not conflict, a new configuration item will be added", "ConfigImport.Tip3": "After importing the configuration item, it needs to be released to take effect", "App.CreateProject": "Create Project", "App.AppIdTips": "(Application's unique identifiers)", "App.AppNameTips": "(Suggested format xx-yy-zz e.g. apollo-server)", "App.AppOwnerTips": "(After enabling the application administrator allocation restrictions, the application owner and project administrator are default to current account, not subject to change)", "App.AppAdminTips1": "(The application owner has project administrator permission by default.", "App.AppAdminTips2": "Project administrators can create namespace, cluster, and assign user permissions)", "App.AccessKey.NoPermissionTips": "You do not have permission to operate, please ask [{{users}}] to authorize", "App.Setting.Title": "Manage Project", "App.Setting.Admin": "Administrators", "App.Setting.AdminTips": "(Project administrators have the following permissions: 1. Create namespace 2. Create clusters 3. Manage project and namespace permissions)", "App.Setting.Add": "Add", "App.Setting.BasicInfo": "Basic information", "App.Setting.ProjectName": "App Name", "App.Setting.ProjectNameTips": "(Suggested format xx-yy-zz e.g. apollo-server)", "App.Setting.ProjectOwner": "Owner", "App.Setting.Modify": "Modify project information", "App.Setting.Cancel": "Cancel", "App.Setting.NoPermissionTips": "You do not have permission to operate, please ask [{{users}}] to authorize", "App.Setting.DeleteAdmin": "Delete Administrator", "App.Setting.CanNotDeleteAllAdmin": "Cannot delete all administrators", "App.Setting.PleaseChooseUser": "Please select a user", "App.Setting.Added": "Add Successfully", "App.Setting.AddFailed": "Failed to Add", "App.Setting.Deleted": "Delete Successfully", "App.Setting.DeleteFailed": "Failed to Delete", "App.Setting.Modified": "Update Successfully", "Valdr.App.AppId.Size": "AppId cannot be longer than 64 characters", "Valdr.App.AppId.Required": "AppId cannot be empty", "Valdr.App.appName.Size": "The app name cannot be longer than 128 characters", "Valdr.App.appName.Required": "App name cannot be empty", "Valdr.Cluster.ClusterName.Size": "Cluster names cannot be longer than 32 characters", "Valdr.Cluster.ClusterName.Required": "Cluster name cannot be empty", "Valdr.AppNamespace.NamespaceName.Size": "Namespace name cannot be longer than 32 characters", "Valdr.AppNamespace.NamespaceName.Required": "Namespace name cannot be empty", "Valdr.AppNamespace.Comment.Size": "Comment length should not exceed 64 characters", "Valdr.Item.Key.Size": "Key cannot be longer than 128 characters", "Valdr.Item.Key.Required": "Key can't be empty", "Valdr.Item.Comment.Size": "Comment length should not exceed 256 characters", "Valdr.Release.ReleaseName.Size": "Release Name cannot be longer than 64 characters", "Valdr.Release.ReleaseName.Required": "Release Name cannot be empty", "Valdr.Release.Comment.Size": "Comment length should not exceed 256 characters", "ApolloConfirmDialog.DefaultConfirmBtnName": "OK", "ApolloConfirmDialog.SearchPlaceHolder": "Search Apps by appId, appName, configuration key", "RulesModal.ChooseInstances": "Select from the list of instances", "RulesModal.InvalidIp": "Illegal IP Address: '{{ip}}'", "RulesModal.GrayscaleAppIdCanNotBeNull": "Grayscale AppId cannot be empty", "RulesModal.AppIdExistsRule": "Rules already exist for AppId='{{appId}}'", "RulesModal.RuleListCanNotBeNull": "Rule list cannot be empty", "RulesModal.LabelListCanNotBeNull": "Label list cannot be empty", "ItemModal.KeyExists": "Key='{{key}}' already exists", "ItemModal.AddedTips": "Add Successfully. need to release configuration to take effect", "ItemModal.AddFailed": "Failed to Add", "ItemModal.PleaseChooseCluster": "Please Select Cluster", "ItemModal.ModifiedTips": "Update Successfully. need to release configuration to take effect", "ItemModal.ModifyFailed": "Failed to Update", "ItemModal.Tabs": "Tab-character", "ItemModal.NewLine": "Newline-character", "ItemModal.Space": "Blank-space", "ItemModal.ChineseComma": "Chinese comma", "ItemModal.JsonDuplicateKeyWarning": "JSON contains duplicate keys, formatting skipped to avoid silent data loss", "ApolloNsPanel.LoadingHistoryError": "Failed to load change history", "ApolloNsPanel.LoadingGrayscaleError": "Failed to load change history", "ApolloNsPanel.Deleted": "Delete Successfully", "ApolloNsPanel.GrayscaleModified": "Update grayscale rules successfully", "ApolloNsPanel.GrayscaleModifyFailed": "Failed to update grayscale rules", "ApolloNsPanel.ModifiedTips": "Update Successfully. need to release configuration to take effect", "ApolloNsPanel.ModifyFailed": "Failed to Update", "ApolloNsPanel.GrammarIsRight": "Syntax is correct", "ApolloNsPanel.JsonDuplicateKeyWarning": "JSON contains duplicate keys, displayed as plain text to avoid silent data loss", "ReleaseModal.Published": "Release Successfully", "ReleaseModal.PublishFailed": "Failed to Release", "ReleaseModal.GrayscalePublished": "Grayscale Release Successfully", "ReleaseModal.GrayscalePublishFailed": "Failed to Grayscale Release", "ReleaseModal.AllPublished": "Full Release Successfully", "ReleaseModal.AllPublishFailed": "Failed to Full Release", "Rollback.NoRollbackList": "No released history to rollback", "Rollback.SameAsCurrentRelease": "This release is the same as current release", "Rollback.RollbackSuccessfully": "Rollback Successfully", "Rollback.RollbackFailed": "Failed to Rollback", "Revoke.RevokeFailed": "Failed to Revoke", "Revoke.RevokeSuccessfully": "Revoke Successfully", "ApolloAuditLog.Disabled": "Audit Log disabled already", "ApolloAuditLog.DisabledTips": "you can add \"apollo.audit.log.enabled = true\" on properties to enable it", "ApolloAuditLog.MoreDetails": "more detail please see the document", "ApolloAuditLog.TraceAuditLogTips": "AuditLogs of Trace", "ApolloAuditLog.RelatedDataInfluenceTips": "DataInfluences of AuditLog", "ApolloAuditLog.DataInfluenceTips": "DataInfluences:", "ApolloAuditLog.DataInfluence.EntityName": "entity name", "ApolloAuditLog.DataInfluence.EntityId": "entity ID", "ApolloAuditLog.DataInfluence.AnyMatchedEntityId": "any entity matched", "ApolloAuditLog.DataInfluence.FieldName": "field name", "ApolloAuditLog.DataInfluence.FieldNewValue": "recorded value", "ApolloAuditLog.DataInfluence.Fields": "influenced fields", "ApolloAuditLog.DataInfluence.MatchedFields": "matched fields", "ApolloAuditLog.DataInfluence.HappenedTime": "recorded time", "ApolloAuditLog.TraceIdTips": "Trace Unique ID", "ApolloAuditLog.OpName": "operate name", "ApolloAuditLog.HappenedTime": "happened time", "ApolloAuditLog.StartDate": "start date", "ApolloAuditLog.Description": "description", "ApolloAuditLog.Title": "Audit Log", "ApolloAuditLog.DoQuery": "Query", "ApolloAuditLog.OpNameTips": "type in operate name", "ApolloAuditLog.OpType": "operate type", "ApolloAuditLog.Operator": "operator", "ApolloAuditLog.LoadMore": "load more", "ApolloAuditLog.DataInfluence.LoadMore": "load more", "ApolloAuditLog.EndDate": "end date", "ApolloAuditLog.NoTraceDetail": "No Trace Details", "ApolloAuditLog.NoDataInfluence": "No DataInfluences", "ApolloAuditLog.TraceDetailTips": "Trace Details", "ApolloAuditLog.SpanIdTips": "operation ID", "ApolloAuditLog.ParentSpan": "parent operation", "ApolloAuditLog.FollowsFromSpan": "last operation", "ApolloAuditLog.FieldChangeHistory": "Field Change History", "ApolloAuditLog.InfluenceEntity": "Audit entity influenced", "Global.Title": "Global Search for Value", "Global.App": "App ID", "Global.Env": "Env Name", "Global.Cluster": "Cluster Name", "Global.NameSpace": "NameSpace Name", "Global.Key": "Key", "Global.Value": "Value", "Global.ValueSearch.Tips" : "(Fuzzy search, key can be the name or content of the configuration item, value is the value of the configuration item.)", "Global.Operate" : "Operate", "Global.Expand" : "Expand", "Global.Abbreviate" : "Abbreviate", "Global.JumpToEditPage" : "Jump to edit page", "Item.GlobalSearchByKey": "Search by Key", "Item.GlobalSearchByValue": "Search by Value", "Item.GlobalSearch": "Search", "Item.GlobalSearchSystemError": "System error, please try again or contact the system administrator", "Item.GlobalSearch.Tips": "Search hint", "ApolloGlobalSearch.NoData" : "No data yet, please search or add", "Paging.TotalItems.part1" : "Total of", "Paging.TotalItems.part2" : "records", "Paging.DisplayNumber" : "per/Page", "Paging.PageNumberOne" : "First", "Paging.PageNumberLast" : "Last" } ================================================ FILE: apollo-portal/src/main/resources/static/i18n/zh-CN.json ================================================ { "Common.Title": "Apollo 配置中心", "Common.Nav.ShowNavBar": "显示导航栏", "Common.Nav.HideNavBar": "隐藏导航栏", "Common.Nav.Help": "帮助", "Common.Nav.AdminTools": "管理员工具", "Common.Nav.NonAdminTools": "工具", "Common.Nav.UserManage": "用户管理", "Common.Nav.SystemRoleManage": "系统权限管理", "Common.Nav.OpenMange": "开放平台授权管理", "Common.Nav.SystemConfig": "系统参数", "Common.Nav.DeleteApp-Cluster-Namespace": "删除应用、集群、AppNamespace", "Common.Nav.SystemInfo": "系统信息", "Common.Nav.ConfigExport": "配置导出导入", "Common.Nav.Logout": "退出", "Common.Department": "部门", "Common.Cluster": "集群", "Common.Environment": "环境", "Common.GrayscaleInstance": "灰度实例", "Common.Instance": "实例", "Common.Email": "邮箱", "Common.AppId": "AppId", "Common.Namespace": "Namespace", "Common.LinkedNamespace": "关联的 Namespace", "Common.AppName": "应用名称", "Common.AppOwner": "负责人", "Common.AppOwnerLong": "应用负责人", "Common.AppAdmin": "应用管理员", "Common.ClusterName": "集群名称", "Common.ClusterRemarks": "集群备注", "Common.Submit": "提交", "Common.Save": "保存", "Common.Created": "创建成功", "Common.CreateFailed": "创建失败", "Common.Deleted": "删除成功", "Common.DeleteFailed": "删除失败", "Common.ReturnToIndex": "返回到应用首页", "Common.ReturnToManageClusterPage": "返回到管理集群页面", "Common.Cancel": "取消", "Common.Ok": "确定", "Common.Search": "查询", "Common.IsRootUser": "当前页面只对 Apollo 管理员开放", "Common.PleaseChooseDepartment": "请选择部门", "Common.PleaseChooseOwner": "请选择应用负责人", "Common.LoginExpiredTips": "您的登录信息已过期,请刷新页面后重试", "Common.Operation": "操作", "Common.Delete": "删除", "Common.ForceDelete": "强制删除", "Component.DeleteNamespace.Title": "删除 Namespace", "Component.DeleteNamespace.PublicContent": "注意,所有环境的公共 Namespace 都会被删除!这将导致实例以及关联的 Namespace 获取不到此 Namespace 的配置,确定要删除吗?", "Component.DeleteNamespace.PrivateContent": "注意,所有环境的私有 Namespace 都会被删除!这将导致实例获取不到此 Namespace 的配置,确定要删除吗?", "Component.DeleteNamespace.LinkedContent": "注意,当前环境关联的 Namespace 会被删除!这将导致实例获取不到此 Namespace 的配置,确定要删除吗?", "Component.DeleteNamespace.ForceDeleteContent": "当前 Namespace 在 24H 内,存在使用中的实例,二次确认是否强制删除?", "Component.GrayscalePublishRule.Title": "编辑灰度规则", "Component.GrayscalePublishRule.AppId": "灰度的 AppId", "Component.GrayscalePublishRule.AcceptRule": "灰度应用规则", "Component.GrayscalePublishRule.AcceptPartInstance": "应用到部分实例", "Component.GrayscalePublishRule.AcceptAllInstance": "应用到所有的实例", "Component.GrayscalePublishRule.IP": "灰度的 IP", "Component.GrayscalePublishRule.Label": "灰度的标签", "Component.GrayscalePublishRule.AppIdFilterTips": "(实例列表会根据输入的 AppId 自动过滤)", "Component.GrayscalePublishRule.IpTips": "没找到你想要的 IP?可以", "Component.GrayscalePublishRule.EnterIp": "手动输入 IP", "Component.GrayscalePublishRule.EnterIpTips": "输入 IP 列表,英文逗号隔开,输入完后点击添加按钮", "Component.GrayscalePublishRule.EnterLabelTips": "输入标签列表,英文逗号隔开,输入完后点击添加按钮", "Component.GrayscalePublishRule.Add": "添加", "Component.ConfigItem.Title": "添加配置项", "Component.ConfigItem.TitleTips": "(温馨提示: 可以通过文本模式批量添加配置)", "Component.ConfigItem.AddGrayscaleItem": "添加灰度配置项", "Component.ConfigItem.ModifyItem": "修改配置项", "Component.ConfigItem.ItemKey": "Key", "Component.ConfigItem.ItemValue": "Value", "Component.ConfigItem.ItemValueTips": "注意: 特殊字符(空格、换行符、制表符Tab、中文逗号)容易导致配置出错,如果需要检测 Value 中特殊字符,请点击", "Component.ConfigItem.ItemValueShowDetection": "检测特殊字符", "Component.ConfigItem.ItemValueNotHiddenChars": "无特殊字符", "Component.ConfigItem.FormatItemValue": "格式化", "Component.ConfigItem.ItemComment": "Comment", "Component.ConfigItem.ChooseCluster": "选择集群", "Component.ConfigItem.ItemTypeName": "类型", "Component.ConfigItem.ItemTypeString": "String", "Component.ConfigItem.ItemTypeNumber": "Number", "Component.ConfigItem.ItemTypeBoolean": "Boolean", "Component.ConfigItem.ItemTypeJson": "JSON", "Component.ConfigItem.ItemNumberError": "非法 Number", "Component.ConfigItem.ItemJsonError": "JSON 格式不正确", "Component.ConfigItem.ItemTypeTrue": "true", "Component.ConfigItem.ItemTypeFalse": "false", "Component.MergePublish.Title": "全量发布", "Component.MergePublish.Tips": "全量发布将会把灰度版本的配置合并到主分支,并发布。", "Component.MergePublish.NextStep": "全量发布后,您希望", "Component.MergePublish.DeleteGrayscale": "删除灰度版本", "Component.MergePublish.ReservedGrayscale": "保留灰度版本", "Component.Namespace.Branch.IsChanged": "有修改", "Component.Namespace.Branch.ChangeUser": "当前修改者", "Component.Namespace.Branch.ContinueGrayscalePublish": "继续灰度发布", "Component.Namespace.Branch.GrayscalePublish": "灰度发布", "Component.Namespace.Branch.MergeToMasterAndPublish": "合并到主版本并发布主版本配置", "Component.Namespace.Branch.AllPublish": "全量发布", "Component.Namespace.Branch.DiscardGrayscaleVersion": "废弃灰度版本", "Component.Namespace.Branch.DiscardGrayscale": "放弃灰度", "Component.Namespace.Branch.NoPermissionTips": "您不是该应用的管理员,也没有该 Namespace 的编辑或发布权限,无法查看配置信息。", "Component.Namespace.Branch.Tab.Configuration": "配置", "Component.Namespace.Branch.Tab.GrayscaleRule": "灰度规则", "Component.Namespace.Branch.Tab.GrayscaleInstance": "灰度实例列表", "Component.Namespace.Branch.Tab.ChangeHistory": "更改历史", "Component.Namespace.Branch.Body.Item": "灰度的配置", "Component.Namespace.Branch.Body.AddedItem": "新增灰度配置", "Component.Namespace.Branch.Body.PublishState": "发布状态", "Component.Namespace.Branch.Body.ItemSort": "排序", "Component.Namespace.Branch.Body.ItemKey": "Key", "Component.Namespace.Branch.Body.ItemMasterValue": "主版本的值", "Component.Namespace.Branch.Body.ItemGrayscaleValue": "灰度的值", "Component.Namespace.Branch.Body.ItemComment": "备注", "Component.Namespace.Branch.Body.ItemLastModify": "最后修改人", "Component.Namespace.Branch.Body.ItemLastModifyTime": "最后修改时间", "Component.Namespace.Branch.Body.ItemOperator": "操作", "Component.Namespace.Branch.Body.ClickToSeeItemValue": "点击查看已发布的值", "Component.Namespace.Branch.Body.ItemNoPublish": "未发布", "Component.Namespace.Branch.Body.ItemPublished": "已发布", "Component.Namespace.Branch.Body.ItemEffective": "已生效的配置", "Component.Namespace.Branch.Body.ClickToSee": "点击查看", "Component.Namespace.Branch.Body.DeletedItem": "删除的配置", "Component.Namespace.Branch.Body.Delete": "删", "Component.Namespace.Branch.Body.ChangedFromMaster": "修改主版本的配置", "Component.Namespace.Branch.Body.ModifiedItem": "修改的配置", "Component.Namespace.Branch.Body.Modify": "改", "Component.Namespace.Branch.Body.AddedByGrayscale": "灰度版本特有的配置", "Component.Namespace.Branch.Body.Added": "新", "Component.Namespace.Branch.Body.Op.Modify": "修改", "Component.Namespace.Branch.Body.Op.Delete": "删除", "Component.Namespace.MasterBranch.Body.Title": "主版本的配置", "Component.Namespace.MasterBranch.Body.PublishState": "发布状态", "Component.Namespace.MasterBranch.Body.ItemKey": "Key", "Component.Namespace.MasterBranch.Body.ItemValue": "Value", "Component.Namespace.MasterBranch.Body.ItemComment": "备注", "Component.Namespace.MasterBranch.Body.ItemLastModify": "最后修改人", "Component.Namespace.MasterBranch.Body.ItemLastModifyTime": "最后修改时间", "Component.Namespace.MasterBranch.Body.ItemOperator": "操作", "Component.Namespace.MasterBranch.Body.ClickToSeeItemValue": "点击查看已发布的值", "Component.Namespace.MasterBranch.Body.ItemNoPublish": "未发布", "Component.Namespace.MasterBranch.Body.ItemEffective": "已生效的配置", "Component.Namespace.MasterBranch.Body.ItemPublished": "已发布", "Component.Namespace.MasterBranch.Body.AddedItem": "新增的配置", "Component.Namespace.MasterBranch.Body.ModifyItem": "修改此灰度配置", "Component.Namespace.Branch.GrayScaleRule.NoPermissionTips": "您没有权限编辑灰度规则, 具有 Namespace 修改权或者发布权的人员才可以编辑灰度规则. 如需要编辑灰度规则,请找应用管理员申请权限.", "Component.Namespace.Branch.GrayScaleRule.AppId": "灰度的 AppId", "Component.Namespace.Branch.GrayScaleRule.RuleList": "灰度的规则列表", "Component.Namespace.Branch.GrayScaleRule.Operator": "操作", "Component.Namespace.Branch.GrayScaleRule.ApplyToAllInstances": "ALL", "Component.Namespace.Branch.GrayScaleRule.Modify": "修改", "Component.Namespace.Branch.GrayScaleRule.Delete": "删除", "Component.Namespace.Branch.GrayScaleRule.AddNewRule": "新增规则", "Component.Namespace.Branch.Instance.RefreshList": "刷新列表", "Component.Namespace.Branch.Instance.ItemToSee": "查看配置", "Component.Namespace.Branch.Instance.InstanceAppId": "App ID", "Component.Namespace.Branch.Instance.InstanceClusterName": "Cluster Name", "Component.Namespace.Branch.Instance.InstanceDataCenter": "Data Center", "Component.Namespace.Branch.Instance.InstanceIp": "IP", "Component.Namespace.Branch.Instance.InstanceGetItemTime": "配置获取时间", "Component.Namespace.Branch.Instance.LoadMore": "刷新列表", "Component.Namespace.Branch.Instance.NoInstance": "无实例信息", "Component.Namespace.Branch.History.ItemType": "Type", "Component.Namespace.Branch.History.ItemKey": "Key", "Component.Namespace.Branch.History.ItemOldValue": "Old Value", "Component.Namespace.Branch.History.ItemNewValue": "New Value", "Component.Namespace.Branch.History.ItemComment": "Comment", "Component.Namespace.Branch.History.NewAdded": "新增", "Component.Namespace.Branch.History.Modified": "更新", "Component.Namespace.Branch.History.Deleted": "删除", "Component.Namespace.Branch.History.LoadMore": "加载更多", "Component.Namespace.Branch.History.NoHistory": "无更改历史", "Component.Namespace.Header.Title.Private": "私有", "Component.Namespace.Header.Title.PrivateTips": "私有 Namespace({{namespace.baseInfo.namespaceName}}) 的配置只能被 AppId 为 {{appId}} 的客户端读取到", "Component.Namespace.Header.Title.Public": "公共", "Component.Namespace.Header.Title.PublicTips": "Namespace({{namespace.baseInfo.namespaceName}}) 的配置能被任何客户端读取到", "Component.Namespace.Header.Title.Extend": "关联", "Component.Namespace.Header.Title.ExtendTips": "Namespace({{namespace.baseInfo.namespaceName}}) 的配置将会覆盖公共 Namespace 的配置, 且合并之后的配置只能被 AppId 为 {{appId}} 的客户端读取到", "Component.Namespace.Header.Title.ExpandAndCollapse": "[展开/收缩]", "Component.Namespace.Header.Title.Master": "主版本", "Component.Namespace.Header.Title.Grayscale": "灰度版本", "Component.Namespace.Master.LoadNamespace": "加载 Namespace", "Component.Namespace.Master.LoadNamespaceTips": "加载 Namespace", "Component.Namespace.Master.Items.Changed": "有修改", "Component.Namespace.Master.Items.ChangedUser": "当前修改者", "Component.Namespace.Master.Items.Publish": "发布", "Component.Namespace.Master.Items.PublishTips": "发布配置", "Component.Namespace.Master.Items.Rollback": "回滚", "Component.Namespace.Master.Items.RollbackTips": "回滚已发布配置", "Component.Namespace.Master.Items.PublishHistory": "发布历史", "Component.Namespace.Master.Items.PublishHistoryTips": "查看发布历史", "Component.Namespace.Master.Items.Grant": "授权", "Component.Namespace.Master.Items.GrantTips": "配置修改、发布权限", "Component.Namespace.Master.Items.Grayscale": "灰度", "Component.Namespace.Master.Items.GrayscaleTips": "创建测试版本", "Component.Namespace.Master.Items.RequestPermission": "申请配置权限", "Component.Namespace.Master.Items.RequestPermissionTips": "您没有任何配置权限,请申请", "Component.Namespace.Master.Items.DeleteNamespace": "删除 Namespace", "Component.Namespace.Master.Items.ExportNamespace": "导出 Namespace", "Component.Namespace.Master.Items.ImportNamespace": "导入 Namespace", "Component.Namespace.Master.Items.NoPermissionTips": "您不是该应用的管理员,也没有该 Namespace 的编辑或发布权限,无法查看配置信息。", "Component.Namespace.Master.Items.ItemList": "表格", "Component.Namespace.Master.Items.ItemListByText": "文本", "Component.Namespace.Master.Items.ItemHistory": "更改历史", "Component.Namespace.Master.Items.ItemInstance": "实例列表", "Component.Namespace.Master.Items.CopyText": "复制文本", "Component.Namespace.Master.Items.Fullscreen": "全屏", "Component.Namespace.Master.Items.ExitFullscreen": "退出全屏", "Component.Namespace.Master.Items.GrammarCheck": "语法检查", "Component.Namespace.Master.Items.CancelChanged": "取消修改", "Component.Namespace.Master.Items.Change": "修改配置", "Component.Namespace.Master.Items.SummitChanged": "提交修改", "Component.Namespace.Master.Items.SortByKey": "按 Key 过滤配置", "Component.Namespace.Master.Items.FilterItem": "过滤配置", "Component.Namespace.Master.Items.SyncItemTips": "同步各环境间配置", "Component.Namespace.Master.Items.SyncItem": "同步配置", "Component.Namespace.Master.Items.RevokeItemTips": "撤销配置的修改", "Component.Namespace.Master.Items.RevokeItem": "撤销配置", "Component.Namespace.Master.Items.DiffItemTips": "比较各环境间配置", "Component.Namespace.Master.Items.DiffItem": "比较配置", "Component.Namespace.Master.Items.AddItem": "新增配置", "Component.Namespace.Master.Items.Body.ItemsNoPublishedTips": "Tips: 此 Namespace 从来没有发布过,Apollo 客户端将获取不到配置并记录 404 日志信息,请及时发布。", "Component.Namespace.Master.Items.Body.FilterByKey": "输入 key 过滤", "Component.Namespace.Master.Items.Body.PublishState": "发布状态", "Component.Namespace.Master.Items.Body.Sort": "排序", "Component.Namespace.Master.Items.Body.ItemKey": "Key", "Component.Namespace.Master.Items.Body.ItemValue": "Value", "Component.Namespace.Master.Items.Body.ItemComment": "备注", "Component.Namespace.Master.Items.Body.ItemLastModify": "最后修改人", "Component.Namespace.Master.Items.Body.ItemLastModifyTime": "最后修改时间", "Component.Namespace.Master.Items.Body.ItemOperator": "操作", "Component.Namespace.Master.Items.Body.NoPublish": "未发布", "Component.Namespace.Master.Items.Body.NoPublishTitle": "点击查看已发布的值", "Component.Namespace.Master.Items.Body.NoPublishTips": "新增的配置,无发布的值", "Component.Namespace.Master.Items.Body.Published": "已发布", "Component.Namespace.Master.Items.Body.PublishedTitle": "已生效的配置", "Component.Namespace.Master.Items.Body.ClickToSee": "点击查看", "Component.Namespace.Master.Items.Body.Grayscale": "灰", "Component.Namespace.Master.Items.Body.HaveGrayscale": "该配置有灰度配置,点击查看灰度的值", "Component.Namespace.Master.Items.Body.NewAdded": "新", "Component.Namespace.Master.Items.Body.NewAddedTips": "新增的配置", "Component.Namespace.Master.Items.Body.Modified": "改", "Component.Namespace.Master.Items.Body.ModifiedTips": "修改的配置", "Component.Namespace.Master.Items.Body.Deleted": "删", "Component.Namespace.Master.Items.Body.DeletedTips": "删除的配置", "Component.Namespace.Master.Items.Body.ModifyTips": "修改", "Component.Namespace.Master.Items.Body.DeleteTips": "删除", "Component.Namespace.Master.Items.Body.Link.Title": "覆盖的配置", "Component.Namespace.Master.Items.Body.Link.NoCoverLinkItem": "无覆盖的配置", "Component.Namespace.Master.Items.Body.Public.Title": "公共的配置", "Component.Namespace.Master.Items.Body.Public.Published": "已发布的配置", "Component.Namespace.Master.Items.Body.Public.NoPublish": "未发布的配置", "Component.Namespace.Master.Items.Body.Public.NoPublicNamespaceTips1": "当前公共 Namespace 的所有者", "Component.Namespace.Master.Items.Body.Public.NoPublicNamespaceTips2": "没有关联此 Namespace,请联系{{namespace.parentAppId}}的所有者在{{namespace.parentAppId}}应用里关联此 Namespace", "Component.Namespace.Master.Items.Body.Public.NoPublished": "无发布的配置", "Component.Namespace.Master.Items.Body.Public.PublishedAndCover": "覆盖此配置", "Component.Namespace.Master.Items.Body.NoPublished.Title": "无公共的配置", "Component.Namespace.Master.Items.Body.NoPublished.PublishedValue": "已发布的值", "Component.Namespace.Master.Items.Body.NoPublished.NoPublishedValue": "未发布的值", "Component.Namespace.Master.Items.Body.HistoryView.ItemType": "Type", "Component.Namespace.Master.Items.Body.HistoryView.ItemKey": "Key", "Component.Namespace.Master.Items.Body.HistoryView.ItemOldValue": "Old Value", "Component.Namespace.Master.Items.Body.HistoryView.ItemNewValue": " New Value", "Component.Namespace.Master.Items.Body.HistoryView.ItemComment": "Comment", "Component.Namespace.Master.Items.Body.HistoryView.NewAdded": "新增", "Component.Namespace.Master.Items.Body.HistoryView.Updated": "更新", "Component.Namespace.Master.Items.Body.HistoryView.Deleted": "删除", "Component.Namespace.Master.Items.Body.HistoryView.LoadMore": "加载更多", "Component.Namespace.Master.Items.Body.HistoryView.NoHistory": "无更改历史", "Component.Namespace.Master.Items.Body.HistoryView.FilterHistory": "过滤更改历史", "Component.Namespace.Master.Items.Body.HistoryView.FilterHistory.SortByKey": "按 Key 过滤更改历史", "Component.Namespace.Master.Items.Body.Instance.Tips": "实例说明:只展示最近一天访问过 Apollo 的实例", "Component.Namespace.Master.Items.Body.Instance.UsedNewItem": "使用最新配置的实例", "Component.Namespace.Master.Items.Body.Instance.NoUsedNewItem": "使用非最新配置的实例", "Component.Namespace.Master.Items.Body.Instance.AllInstance": "所有实例", "Component.Namespace.Master.Items.Body.Instance.RefreshList": "刷新列表", "Component.Namespace.Master.Items.Body.Instance.ToSeeItem": "查看配置", "Component.Namespace.Master.Items.Body.Instance.LoadMore": "加载更多", "Component.Namespace.Master.Items.Body.Instance.ItemAppId": "App ID", "Component.Namespace.Master.Items.Body.Instance.ItemCluster": "Cluster Name", "Component.Namespace.Master.Items.Body.Instance.ItemDataCenter": "Data Center", "Component.Namespace.Master.Items.Body.Instance.ItemIp": "IP", "Component.Namespace.Master.Items.Body.Instance.ItemGetTime": "配置获取时间", "Component.Namespace.Master.Items.Body.Instance.NoInstanceTips": "无实例信息", "Component.PublishDeny.Title": "发布受限", "Component.PublishDeny.Tips1": "您不能发布哟~{{env}}环境配置的编辑和发布必须为不同的人,请找另一个具有当前 Namespace 发布权的人操作发布~", "Component.PublishDeny.Tips2": "(如果是非工作时间或者特殊情况,您可以通过点击'紧急发布'按钮进行发布)", "Component.PublishDeny.EmergencyPublish": "紧急发布", "Component.PublishDeny.Close": "关闭", "Component.Publish.Title": "发布", "Component.Publish.Tips": "(只有发布过的配置才会被客户端获取到,此次发布只会作用于当前环境:{{env}})", "Component.Publish.Grayscale": "灰度发布", "Component.Publish.GrayscaleTips": "(灰度发布的配置只会作用于在灰度规则中配置的实例)", "Component.Publish.AllPublish": "全量发布", "Component.Publish.AllPublishTips": "(全量发布的配置会作用于全部的实例)", "Component.Publish.ToSeeChange": "查看变更", "Component.Publish.CompareWithMasterValue": "与主版本对比", "Component.Publish.CompareWithPublishedValue": "与已发布对比", "Component.Publish.PublishedValue": "已发布的值", "Component.Publish.Changes": "改动", "Component.Publish.Key": "Key", "Component.Publish.NoPublishedValue": "待发布的值", "Component.Publish.ModifyUser": "修改人", "Component.Publish.ModifyTime": "修改时间", "Component.Publish.ModifyRecord": "修改记录", "Component.Publish.NewAdded": "新", "Component.Publish.NewAddedTips": "新增的配置", "Component.Publish.Modified": "改", "Component.Publish.ModifiedTips": "修改的配置", "Component.Publish.Deleted": "删", "Component.Publish.DeletedTips": "删除的配置", "Component.Publish.MasterValue": "主版本值", "Component.Publish.GrayValue": "灰度版本的值", "Component.Publish.GrayPublishedValue": "灰度版本发布的值", "Component.Publish.GrayNoPublishedValue": "灰度版本未发布的值", "Component.Publish.ItemNoChange": "配置没有变化", "Component.Publish.GrayItemNoChange": "灰度配置没有变化", "Component.Publish.NoGrayItems": "没有灰度的配置项", "Component.Publish.Release": "版本名称", "Component.Publish.ReleaseComment": "说明", "Component.Publish.OpPublish": "发布", "Component.Rollback.To": "回滚到", "Component.Rollback.Tips": "此操作将会回滚到上一个发布版本,且当前版本作废,但不影响正在修改的配置。可在发布历史页面查看当前生效的版本", "Component.RollbackTo.Tips":"此操作将会回滚到此发布版本,且当前版本作废,但不影响正在修改的配置", "Component.Rollback.ClickToView": "点击查看", "Component.Rollback.ItemType": "Type", "Component.Rollback.ItemKey": "Key", "Component.Rollback.RollbackBeforeValue": "回滚前", "Component.Rollback.RollbackAfterValue": "回滚后", "Component.Rollback.Added": "新增", "Component.Rollback.Modified": "更新", "Component.Rollback.Deleted": "删除", "Component.Rollback.NoChange": "配置没有变化", "Component.Rollback.OpRollback": "回滚", "Component.ShowText.Title": "查看", "Login.Login": "登录", "Login.UserNameOrPasswordIncorrect": "用户名或密码错误", "Login.LogoutSuccessfully": "登出成功", "Index.MyProject": "我的应用", "Index.CreateProject": "创建应用", "Index.LoadMore": "加载更多", "Index.FavoriteItems": "收藏的应用", "Index.Topping": "置顶", "Index.FavoriteCancel": "取消收藏", "Index.FavoriteTip": "您还没有收藏过任何应用,在项目主页可以收藏应用哟~", "Index.RecentlyViewedItems": "最近浏览的应用", "Index.GetCreateAppRoleFailed": "获取创建应用权限信息失败", "Index.Topped": "置顶成功", "Index.CancelledFavorite": "取消收藏成功", "Index.PublicNamespace": "公共 Namespace", "Index.SearchNamespace": "搜索公共 Namespace(AppId、Namespace)", "Index.PublicNamespaceTip": "您还没有任何公共 Namespace,在你的项目中可以创建哟~", "Index.appTable.operation": "操作", "Index.appTable.Format": "格式", "Index.appTable.Comment": "备注", "Cluster.CreateCluster": "新建集群", "Cluster.Tips.1": "通过添加集群,可以使同一份程序在不同的集群(如不同的数据中心)使用不同的配置", "Cluster.Tips.2": "如果不同集群使用一样的配置,则没有必要创建集群", "Cluster.Tips.3": "Apollo 默认会读取机器上 /opt/settings/server.properties(linux)或C:\\opt\\settings\\server.properties(windows)文件中的 idc 属性作为集群名字, 如 SHAJQ(金桥数据中心)、SHAOY(欧阳数据中心)", "Cluster.Tips.4": "在这里创建的集群名字需要和机器上 server.properties 中的 idc 属性一致", "Cluster.CreateNameTips": "(部署集群如: SHAJQ,SHAOY 或自定义集群如: SHAJQ-xx,SHAJQ-yy)", "Cluster.CreateRemarksTips": "(为新建集群增加备注说明可以帮助用户更好地理解每个集群的用途)", "Cluster.ChooseEnvironment": "选择环境", "Cluster.LoadingEnvironmentError": "加载环境信息出错", "Cluster.ClusterCreated": "集群创建成功", "Cluster.ClusterCreateFailed": "集群创建失败", "Cluster.PleaseChooseEnvironment": "请选择环境", "Cluster.Grant": "授权", "Cluster.GrantTips": "配置修改、发布权限", "Cluster.Role.Title": "集群权限管理", "Cluster.Role.GrantModifyTo": "修改权", "Cluster.Role.GrantModifyTo2": "(可以修改配置)", "Cluster.Role.GrantPublishTo": "发布权", "Cluster.Role.GrantPublishTo2": "(可以发布配置)", "Cluster.Role.Add": "添加", "Cluster.Role.NoPermission": "您没有权限哟!", "Cluster.Role.InitClusterPermissionError": "初始化授权出错", "Cluster.Role.GetGrantUserError": "加载授权用户出错", "Cluster.Role.PleaseChooseUser": "请选择用户", "Cluster.Role.Added": "添加成功", "Cluster.Role.AddFailed": "添加失败", "Cluster.Role.Deleted": "删除成功", "Cluster.Role.DeleteFailed": "删除失败", "Config.Title": "Apollo 配置中心", "Config.AppIdNotFound": "不存在,", "Config.ClickByCreate": "点击创建", "Config.EnvList": "环境列表", "Config.EnvListTips": "通过切换环境、集群来管理不同环境、集群的配置", "Config.ProjectInfo": "应用信息", "Config.ModifyBasicProjectInfo": "修改应用基本信息", "Config.Favorite": "收藏", "Config.CancelFavorite": "取消收藏", "Config.MissEnv": "缺失的环境", "Config.MissNamespace": "缺失的 Namespace", "Config.ProjectManage": "管理应用", "Config.AccessKeyManage": "管理密钥", "Config.CreateAppMissEnv": "补缺环境", "Config.CreateAppMissNamespace": "补缺 Namespace", "Config.AddCluster": "添加集群", "Config.AddNamespace": "添加 Namespace", "Config.CurrentlyOperatorEnv": "当前操作环境", "Config.DoNotRemindAgain": "不再提示", "Config.Note": "注意", "Config.ClusterIsDefaultTipContent": "所有不属于 '{{name}}' 集群的实例会使用 default 集群(当前页面)的配置,属于 '{{name}}' 的实例会使用对应集群的配置!", "Config.ClusterIsCustomTipContent": "属于 '{{name}}' 集群的实例只会使用 '{{name}}' 集群(当前页面)的配置,只有当对应 Namespace 在当前集群没有发布过配置时,才会使用 default 集群的配置。", "Config.HasNotPublishNamespace": "以下环境/集群有未发布的配置,客户端获取不到未发布的配置,请及时发布。", "Config.RevokeItem.DialogTitle": "撤销配置", "Config.RevokeItem.DialogContent": "当前命名空间下已修改但尚未发布的配置将被撤销,确定要撤销么?", "Config.DeleteItem.DialogTitle": "删除配置", "Config.DeleteItem.DialogContent": "您正在删除 Key 为 '{{config.key}}' Value 为 '{{config.value}}' 的配置,
确定要删除配置吗?", "Config.PublishNoPermission.DialogTitle": "发布", "Config.PublishNoPermission.DialogContent": "您没有发布权限哦~ 请找应用管理员 '{{masterUsers}}' 分配发布权限", "Config.ModifyNoPermission.DialogTitle": "申请配置权限", "Config.ModifyNoPermission.DialogContent": "请找应用管理员 '{{masterUsers}}' 分配编辑或发布权限", "Config.MasterNoPermission.DialogTitle": "申请配置权限", "Config.MasterNoPermission.DialogContent": "您不是应用管理员, 只有应用管理员才有添加集群、Namespace 的权限。如需管理员权限,请找应用管理员 '{{masterUsers}}' 分配管理员权限", "Config.NamespaceLocked.DialogTitle": "编辑受限", "Config.NamespaceLocked.DialogContent": "当前namespace正在被 '{{lockOwner}}' 编辑,一次发布只能被一个人修改.", "Config.RollbackAlert.DialogTitle": "回滚", "Config.RollbackAlert.DialogContent": "确定要回滚吗?", "Config.EmergencyPublishAlert.DialogTitle": "紧急发布", "Config.EmergencyPublishAlert.DialogContent": "确定要紧急发布吗?", "Config.DeleteBranch.DialogTitle": "删除灰度", "Config.DeleteBranch.DialogContent": "删除灰度会丢失灰度的配置,确定要删除吗?", "Config.UpdateRuleTips.DialogTitle": "更新灰度规则提示", "Config.UpdateRuleTips.DialogContent": "灰度规则已生效,但发现灰度版本有未发布的配置,这些配置需要手动灰度发布才会生效", "Config.MergeAndReleaseDeny.DialogTitle": "全量发布", "Config.MergeAndReleaseDeny.DialogContent": "Namespace 主版本有未发布的配置,请先发布主版本配置", "Config.GrayReleaseWithoutRulesTips.DialogTitle": "缺失灰度规则提示", "Config.GrayReleaseWithoutRulesTips.DialogContent": "灰度版本还没有配置任何灰度规则,请配置灰度规则", "Config.DeleteNamespaceDenyForMasterInstance.DialogTitle": "删除 Namespace 警告信息", "Config.DeleteNamespaceDenyForMasterInstance.DialogContent": "发现有 '{{deleteNamespaceContext.namespace.instancesCount}}' 个实例正在使用 Namespace('{{deleteNamespaceContext.namespace.baseInfo.namespaceName}}'),删除 Namespace 将导致实例获取不到配置。
请到 “实例列表” 确认实例信息,如确认相关实例都已经不再使用该 Namespace 配置,可以联系 Apollo 相关负责人删除实例信息(InstanceConfig)或等待实例24小时自动过期后再来删除。", "Config.DeleteNamespaceDenyForBranchInstance.DialogTitle": "删除Namespace警告信息", "Config.DeleteNamespaceDenyForBranchInstance.DialogContent": "发现有 '{{deleteNamespaceContext.namespace.branch.latestReleaseInstances.total}}' 个实例正在使用 Namespace('{{deleteNamespaceContext.namespace.baseInfo.namespaceName}}')灰度版本的配置,删除 Namespace 将导致实例获取不到配置。
请到 “灰度版本” => “实例列表” 确认实例信息,如确认相关实例都已经不再使用该 Namespace 配置,可以联系 Apollo 相关负责人删除实例信息(InstanceConfig)或等待实例24小时自动过期后再来删除。", "Config.DeleteNamespaceDenyForPublicNamespace.DialogTitle": "删除 Namespace 失败提示", "Config.DeleteNamespaceDenyForPublicNamespace.DialogContent": "删除 Namespace 失败提示", "Config.DeleteNamespaceDenyForPublicNamespace.PleaseEnterAppId": "请输入 AppId", "Config.SyntaxCheckFailed.DialogTitle": "语法检查错误", "Config.SyntaxCheckFailed.DialogContent": "删除 Namespace 失败提示", "Config.CreateBranchTips.DialogTitle": "创建灰度须知", "Config.CreateBranchTips.DialogContent": "通过创建灰度版本,您可以对某些配置做灰度测试
灰度流程为:
  1.创建灰度版本
  2.配置灰度配置项
  3.配置灰度规则.如果是私有的 Namespace 可以按照客户端的 IP 和 Label 进行灰度,如果是公共的 Namespace则可以同时按 AppId,客户端的 IP 和客户端的 Label 进行灰度
  4.灰度发布
灰度版本最终有两种结果:全量发布和放弃灰度
全量发布:灰度的配置合到主版本并发布,所有的客户端都会使用合并后的配置
放弃灰度:删除灰度版本,所有的客户端都会使用回主版本的配置
注意事项:
  1.如果灰度版本已经有灰度发布过,那么修改灰度规则后,无需再次灰度发布就立即生效", "Config.ProjectMissEnvInfos": "当前应用有环境缺失,请点击页面左侧『补缺环境』补齐数据", "Config.ProjectMissNamespaceInfos": "当前环境有 Namespace 缺失,请点击页面左侧『补缺 Namespace』补齐数据", "Config.SystemError": "系统出错,请重试或联系系统负责人", "Config.FavoriteSuccessfully": "收藏成功", "Config.FavoriteFailed": "收藏失败", "Config.CancelledFavorite": "取消收藏成功", "Config.CancelFavoriteFailed": "取消收藏失败", "Config.GetUserInfoFailed": "获取用户登录信息失败", "Config.LoadingAllNamespaceError": "加载配置信息出错", "Config.CancelFavoriteError": "取消收藏失败", "Config.Deleted": "删除成功", "Config.DeleteFailed": "删除失败", "Config.GrayscaleCreated": "创建灰度成功", "Config.GrayscaleCreateFailed": "创建灰度失败", "Config.BranchDeleted": "分支删除成功", "Config.BranchDeleteFailed": "分支删除失败", "Config.DeleteNamespaceFailedTips": "以下应用已关联此公共 Namespace,必须先删除全部已关联的 Namespace 才能删除公共 Namespace", "Config.DeleteNamespaceNoPermissionFailedTitle": "删除失败", "Config.DeleteNamespaceNoPermissionFailedTips": "您没有应用管理员权限,只有管理员才能删除 Namespace,请找应用管理员 [{{users}}] 删除 Namespace", "Config.Key": "Key", "Config.Value": "Value", "Config.Comment": "Comment", "Config.Operation": "Operation", "Config.Add": "新增配置", "Config.SortByKey": "按Key值过滤", "Config.FilterConfig": "过滤配置", "Config.Reset": "重置", "Config.ManageCluster": "管理集群", "Delete.Title": "删除应用、集群、AppNamespace", "Delete.DeleteApp": "删除应用", "Delete.DeleteAppTips": "(由于删除应用影响面较大,所以现在暂时只允许系统管理员删除,请确保没有客户端读取该应用的配置后再做删除动作)", "Delete.AppIdTips": "(删除前请先查询应用信息)", "Delete.AppInfo": "应用信息", "Delete.DeleteCluster": "删除集群", "Delete.DeleteClusterTips": "(由于删除集群影响面较大,所以现在暂时只允许系统管理员删除,请确保没有客户端读取该集群的配置后再做删除动作)", "Delete.EnvName": "环境名称", "Delete.ClusterNameTips": "(删除前请先查询应用集群信息)", "Delete.ClusterInfo": "集群信息", "Delete.DeleteNamespace": "删除 AppNamespace", "Delete.DeleteNamespaceTips": "(注意,所有环境的 Namespace 和 AppNamespace 都会被删除!)", "Delete.DeleteNamespaceTips2": "对于公共 Namespace 需要确保没有应用关联了该 AppNamespace。", "Delete.AppNamespaceName": "AppNamespace 名称", "Delete.AppNamespaceNameTips": "(非 properties 类型的 Namespace 请加上类型后缀,例如 apollo.xml)", "Delete.AppNamespaceInfo": "AppNamespace 信息", "Delete.IsRootUserTips": "当前页面只对 Apollo 管理员开放", "Delete.PleaseEnterAppId": "请输入 AppId", "Delete.AppIdNotFound": "AppId: '{{appId}}'不存在!", "Delete.AppInfoContent": "应用名:'{{appName}}' 部门:'{{departmentName}}({{departmentId}})' 负责人:'{{ownerName}}'", "Delete.ConfirmDeleteAppId": "确认删除 AppId: '{{appId}}'?", "Delete.Deleted": "删除成功", "Delete.PleaseEnterAppIdAndEnvAndCluster": "请输入 AppId、环境和集群名称", "Delete.ClusterInfoContent": "AppId:'{{appId}}' 环境:'{{env}}' 集群名称:'{{clusterName}}'", "Delete.ConfirmDeleteCluster": "确认删除集群?AppId:'{{appId}}' 环境:'{{env}}' 集群名称:'{{clusterName}}'", "Delete.PleaseEnterAppIdAndNamespace": "请输入 AppId 和 AppNamespace 名称", "Delete.AppNamespaceInfoContent": "AppId:'{{appId}}' AppNamespace 名称:'{{namespace}}' isPublic:'{{isPublic}}'", "Delete.ConfirmDeleteNamespace": "确认删除所有环境的 AppNamespace 和 Namespace ?appId: '{{appId}}' 环境:'所有环境' AppNamespace 名称:'{{namespace}}'", "Namespace.Title": "新建 Namespace", "Namespace.UnderstandMore": "(点击了解更多 Namespace 相关知识)", "Namespace.Link.Tips1": "应用可以通过关联公共 Namespace 来覆盖公共 Namespace 的配置", "Namespace.Link.Tips2": "如果应用不需要覆盖公共 Namespace 的配置,那么无需关联公共 Namespace", "Namespace.CreatePublic.Tips1": "公共的 Namespace 的配置能被任何应用读取", "Namespace.CreatePublic.Tips2": "通过创建公共 Namespace 可以实现公共组件的配置,或多个应用共享同一份配置的需求", "Namespace.CreatePublic.Tips3": "如果其它应用需要覆盖公共部分的配置,可以在其它应用那里关联公共 Namespace,然后在关联的 Namespace 里面配置需要覆盖的配置即可", "Namespace.CreatePublic.Tips4": "如果其它应用不需要覆盖公共部分的配置,那么就不需要在其它应用那里关联公共 Namespace", "Namespace.CreatePrivate.Tips1": "私有 Namespace 的配置只能被所属的应用获取到", "Namespace.CreatePrivate.Tips2": "通过创建一个私有的 Namespace 可以实现分组管理配置", "Namespace.CreatePrivate.Tips3": "私有 Namespace 的格式可以是 xml、yml、yaml、json、txt. 您可以通过 apollo-client 中 ConfigFile 接口来获取非 properties 格式 Namespace 的内容", "Namespace.CreatePrivate.Tips4": "1.3.0 及以上版本的 apollo-client 针对 yaml/yml 提供了更好的支持,可以通过 ConfigService.getConfig(\"someNamespace.yml\")直接获取 Config对象,也可以通过 @EnableApolloConfig(\"someNamespace.yml\")或 apollo.bootstrap.namespaces=someNamespace.yml 注入 yml 配置到 Spring/SpringBoot 中去", "Namespace.CreateNamespace": "创建 Namespace", "Namespace.AssociationPublicNamespace": "关联公共 Namespace", "Namespace.ChooseCluster": "选择集群", "Namespace.NamespaceName": "名称", "Namespace.AutoAddDepartmentPrefix": "自动添加部门前缀", "Namespace.AutoAddDepartmentPrefixTips": "(公共 Namespace 的名称需要全局唯一,添加部门前缀有助于保证全局唯一性)", "Namespace.NamespaceType": "类型", "Namespace.NamespaceType.Public": "public", "Namespace.NamespaceType.Private": "private", "Namespace.Remark": "备注", "Namespace.Namespace": "Namespace", "Namespace.PleaseChooseNamespace": "请选择 Namespace", "Namespace.LoadingPublicNamespaceError": "加载公共 Namespace 错误", "Namespace.LoadingAppInfoError": "加载 App 信息出错", "Namespace.PleaseChooseCluster": "请选择集群", "Namespace.CheckNamespaceNameLengthTip": "Namespace 名称不能大于 32 个字符。 部门前缀:'{{departmentLength}}' 个字符, 名称 {{namespaceLength}} 个字符", "ServiceConfig.Title": "应用配置", "ServiceConfig.PortalDB.Tips": "(维护 ApolloPortalDB.ServerConfig 表数据,编辑操作中如果已存在配置项则会覆盖,否则会创建配置项。配置更新后,一分钟后自动生效)", "ServiceConfig.ConfigDB.Tips": "(维护 ApolloConfigDB.ServerConfig 表数据,编辑操作中如果已存在配置项则会覆盖,否则会创建配置项。配置更新后,一分钟后自动生效)", "ServiceConfig.PortalDB.Tab": "PortalDB 配置管理", "ServiceConfig.ConfigDB.Tab": "ConfigDB 配置管理", "ServiceConfig.Switch.Env": "切换环境", "ServiceConfig.Key": "Key", "ServiceConfig.KeyTips": "(修改配置前请先查询该配置信息)", "ServiceConfig.Value": "Value", "ServiceConfig.Comment": "Comment", "ServiceConfig.Saved": "保存成功", "ServiceConfig.SaveFailed": "保存失败", "ServiceConfig.PleaseEnterKey": "请输入 Key", "ServiceConfig.KeyNotExistsAndCreateTip": "Key: '{{key}}' 不存在,点击保存后会创建该配置项", "ServiceConfig.KeyExistsAndSaveTip": "Key: '{{key}}' 已存在,点击保存后会覆盖该配置项", "AccessKey.Tips.1": "每个环境最多可添加 5 个访问密钥", "AccessKey.Tips.2": "一旦该环境有启用状态的访问密钥,客户端将被要求配置密钥,否则无法获取配置", "AccessKey.Tips.3": "观察状态密钥用于预校验,只做日志记录不拦截配置获取,注意:一旦该环境有启用状态的访问密钥,观察状态将不再生效", "AccessKey.Tips.4": "配置访问密钥防止非法客户端获取该应用配置,配置方式如下:(仅支持apollo-client 1.6.0+)", "AccessKey.Tips.4.1": "通过JVM参数配置: apollo-client >=1.9.0 推荐使用 -Dapollo.access-key.secret; 其它版本使用 -Dapollo.accesskey.secret", "AccessKey.Tips.4.2": "通过操作系统环境变量配置: apollo-client >=1.9.0 推荐使用 APOLLO_ACCESS_KEY_SECRET; 其它版本使用 APOLLO_ACCESSKEY_SECRET", "AccessKey.Tips.4.3": "通过 META-INF/app.properties 或 application.properties配置: apollo-client >=1.9.0 推荐使用 apollo.access-key.secret; 其它版本使用 apollo.accesskey.secret(注意多环境 secret 不一样)", "AccessKey.NoAccessKeyServiceTips": "该环境没有配置访问密钥", "AccessKey.ConfigAccessKeys.Secret": "访问密钥", "AccessKey.ConfigAccessKeys.Status": "状态", "AccessKey.ConfigAccessKeys.LastModify": "最后修改人", "AccessKey.ConfigAccessKeys.LastModifyTime": "最后修改时间", "AccessKey.ConfigAccessKeys.Operator": "操作", "AccessKey.Operator.Disable": "禁用", "AccessKey.Operator.Enable": "启用", "AccessKey.Operator.Observe": "观察", "AccessKey.Operator.Disabled": "已禁用", "AccessKey.Operator.Enabled": "已启用", "AccessKey.Operator.Observed": "已观察", "AccessKey.Operator.Remove": "删除", "AccessKey.Operator.CreateSuccess": "访问密钥创建成功", "AccessKey.Operator.DisabledSuccess": "访问密钥禁用成功", "AccessKey.Operator.EnabledSuccess": "访问密钥启用成功", "AccessKey.Operator.ObservedSuccess": "访问密钥观察成功", "AccessKey.Operator.RemoveSuccess": "访问密钥删除成功", "AccessKey.Operator.CreateError": "访问密钥创建失败", "AccessKey.Operator.DisabledError": "访问密钥禁用失败", "AccessKey.Operator.EnabledError": "访问密钥启用失败", "AccessKey.Operator.ObservedError": "访问密钥观察失败", "AccessKey.Operator.RemoveError": "访问密钥删除失败", "AccessKey.Operator.DisabledTips": "是否确定禁用该访问密钥?", "AccessKey.Operator.EnabledTips": " 是否确定启用该访问密钥?", "AccessKey.Operator.ObservedTips": " 是否确定观察该访问密钥?", "AccessKey.Operator.RemoveTips": " 是否确定删除该访问密钥?", "AccessKey.LoadError": "加载访问密钥出错", "SystemInfo.Title": "系统信息", "SystemInfo.SystemVersion": "系统版本", "SystemInfo.Tips1": "环境列表来自于 ApolloPortalDB.ServerConfig 中的 apollo.portal.envs 配置,可以到 系统参数页面配置,更多信息可以参考分布式部署指南中的 apollo.portal.envs - 可支持的环境列表章节。", "SystemInfo.Tips2": "Meta Server 地址展示了该环境配置的 Meta Server 信息,更多信息可以参考分布式部署指南中的配置 apollo-portal 的 meta service 信息章节。", "SystemInfo.Active": "Active", "SystemInfo.ActiveTips": "(当前环境状态异常,请结合下方系统信息和 AdminService 的 Check Health 结果排查)", "SystemInfo.MetaServerAddress": "Meta Server 地址", "SystemInfo.ConfigServices": "Config Services", "SystemInfo.ConfigServices.Name": "Name", "SystemInfo.ConfigServices.InstanceId": "Instance Id", "SystemInfo.ConfigServices.HomePageUrl": "Home Page Url", "SystemInfo.ConfigServices.CheckHealth": "Check Health", "SystemInfo.NoConfigServiceTips": "No config service found!", "SystemInfo.Check": "Check", "SystemInfo.AdminServices": "Admin Services", "SystemInfo.AdminServices.Name": "Name", "SystemInfo.AdminServices.InstanceId": "Instance Id", "SystemInfo.AdminServices.HomePageUrl": "Home Page Url", "SystemInfo.AdminServices.CheckHealth": "Check Health", "SystemInfo.NoAdminServiceTips": "No admin service found!", "SystemInfo.IsRootUser": "当前页面只对 Apollo 管理员开放", "SystemRole.Title": "系统权限管理", "SystemRole.AddCreateAppRoleToUser": "为用户添加创建应用权限", "SystemRole.AddCreateAppRoleToUserTips": "(系统参数中设置 role.create-application.enabled=true 会限制只有超级管理员和拥有创建应用权限的帐号可以创建应用)", "SystemRole.ChooseUser": "用户选择", "SystemRole.Add": "添加", "SystemRole.AuthorizedUser": "已拥有权限用户", "SystemRole.ModifyAppAdminUser": "修改应用管理员分配权限", "SystemRole.ModifyAppAdminUserTips": "(系统参数中设置 role.manage-app-master.enabled=true 会限制只有超级管理员和拥有管理员分配权限的帐号可以修改应用管理员)", "SystemRole.AppIdTips": "(请先查询应用信息)", "SystemRole.AppInfo": "应用信息", "SystemRole.AllowAppMasterAssignRole": "允许此用户作为管理员时添加 Master", "SystemRole.DeleteAppMasterAssignRole": "禁止此用户作为管理员时添加 Master", "SystemRole.IsRootUser": "当前页面只对 Apollo 管理员开放", "SystemRole.PleaseChooseUser": "请选择用户名", "SystemRole.Added": "添加成功", "SystemRole.AddFailed": "添加失败", "SystemRole.Deleted": "删除成功", "SystemRole.DeleteFailed": "删除失败", "SystemRole.GetCanCreateProjectUsersError": "获取拥有创建应用权限的用户列表出错", "SystemRole.PleaseEnterAppId": "请输入 AppId", "SystemRole.AppIdNotFound": "AppId: '{{appId}}' 不存在!", "SystemRole.AppInfoContent": "应用名:'{{appName}}' 部门:'{{departmentName}}({{departmentId}})' 负责人:'{{ownerName}}", "SystemRole.DeleteMasterAssignRoleTips": "确认删除 AppId: '{{appId}}' 的用户: '{{userId}}' 分配应用管理员的权限?", "SystemRole.DeletedMasterAssignRoleTips": "删除 AppId: '{{appId}}' 的用户: '{{userId}}' 分配应用管理员的权限成功", "SystemRole.AllowAppMasterAssignRoleTips": "确认添加 AppId: '{{appId}}' 的用户: '{{userId}}' 分配应用管理员的权限?", "SystemRole.AllowedAppMasterAssignRoleTips": "添加 AppId: '{{appId}}' 的用户: '{{userId}}' 分配应用管理员的权限成功", "UserMange.Title": "用户管理", "UserMange.TitleTips": "(仅对默认的 Spring Security 简单认证方式有效: -Dapollo_profile=github,auth)", "UserMange.UserName": "用户登录账户", "UserMange.UserDisplayName": "用户名称", "UserMange.Pwd": "密码", "UserMange.ConfirmPwd": "确认密码", "UserMange.PwdNotMatch": "密码不匹配", "UserMange.Email": "邮箱", "UserMange.Created": "创建用户成功", "UserMange.CreateFailed": "创建用户失败", "UserMange.Edited": "编辑用户成功", "UserMange.EditFailed": "编辑用户失败", "UserMange.Enabled.succeed": "修改用户状态成功", "UserMange.Enabled.failure": "修改用户状态失败", "UserMange.Enabled": "用户状态", "UserMange.Enable": "启用", "UserMange.Disable": "禁用", "UserMange.Operation": "操作", "UserMange.Edit": "编辑", "UserMange.Add": "添加用户", "UserMange.Back": "返回", "UserMange.SortByUserLoginName": "按用户登录名称过滤", "UserMange.FilterUser": "过滤用户", "UserMange.Reset": "重置", "UserMange.Save": "保存", "UserMange.Cancel": "取消", "Open.Manage.Title": "开放平台", "Open.Manage.CreateThirdApp": "创建第三方应用", "Open.Manage.CreateThirdAppTips": "(说明: 第三方应用可以通过 Apollo 开放平台来对配置进行管理)", "Open.Manage.ThirdAppId": "第三方应用ID", "Open.Manage.ThirdAppIdTips": "(创建前请先查询第三方应用是否已经申请过)", "Open.Manage.ThirdAppName": "第三方应用名称", "Open.Manage.ThirdAppNameTips": "(建议格式 xx-yy-zz 例:apollo-server)", "Open.Manage.ProjectOwner": "应用负责人", "Open.Manage.Create": "创建", "Open.Manage.GrantPermission": "赋权", "Open.Manage.GrantPermissionTips": "(Namespace 级别权限包括: 修改、发布Namespace。应用级别权限包括: 创建 Namespace、修改或发布应用下任何 Namespace)", "Open.Manage.Token": "Token", "Open.Manage.ManagedAppId": "被管理的 AppId", "Open.Manage.ManagedNamespace": "被管理的 Namespace", "Open.Manage.ManagedNamespaceTips": "(非 properties 类型的 Namespace 请加上类型后缀,例如 apollo.xml)", "Open.Manage.GrantType": "授权类型", "Open.Manage.GrantType.Namespace": "Namespace", "Open.Manage.GrantType.App": "App", "Open.Manage.GrantEnv": "环境", "Open.Manage.GrantEnvTips": "(不选择则所有环境都有权限,如果提示 Namespace's role does not exist,请先打开该 Namespace 的授权页面触发一下权限的初始化动作)", "Open.Manage.PleaseEnterAppId": "请输入 AppId", "Open.Manage.AppNotCreated": "App('{{appId}}')未创建,请先创建", "Open.Manage.GrantSuccessfully": "赋权成功", "Open.Manage.GrantFailed": "赋权失败", "Open.Manage.ViewAndGrantPermission": "查看Token并赋权", "Open.Manage.DeleteConsumer.Confirm": "您正在删除 AppId='{{toOperationConsumer.appId}}',应用名称='{{toOperationConsumer.name}}' 的第三方应用,
确定要删除吗?", "Open.Manage.DeleteConsumer.Success": "第三方应用删除成功", "Open.Manage.DeleteConsumer.Error": "第三方应用删除失败", "Open.Manage.CreateConsumer.Button": "创建第三方应用", "Open.Manage.Consumer.AllowCreateApplication": "允许创建app?", "Open.Manage.Consumer.AllowCreateApplicationTips": "(允许第三方应用创建app,并且对创建出的app,拥有应用管理员的权限)", "Open.Manage.Consumer.AllowCreateApplication.No": "否", "Open.Manage.Consumer.AllowCreateApplication.Yes": "是", "Open.Manage.Consumer.RateLimit.Enabled": "是否启用限流", "Open.Manage.Consumer.RateLimit.Enabled.Tips": "(开启后,第三方应用在 Apollo 上发布配置时,会根据配置的 QPS 限制,控制其流量)", "Open.Manage.Consumer.RateLimitValue": "限流QPS", "Open.Manage.Consumer.RateLimitValueTips": "(单位:次/秒,例如: 100 表示每秒最多发布 100 次配置)", "Open.Manage.Consumer.RateLimitValue.Error": "限流QPS最小为1", "Open.Manage.Consumer.RateLimitValue.Display": "无限制", "Namespace.Role.Title": "权限管理", "Namespace.Role.GrantModifyTo": "修改权", "Namespace.Role.GrantModifyTo2": "(可以修改配置)", "Namespace.Role.AllEnv": "所有环境", "Namespace.Role.GrantPublishTo": "发布权", "Namespace.Role.GrantPublishTo2": "(可以发布配置)", "Namespace.Role.Add": "添加", "Namespace.Role.NoPermission": "您没有权限哟!", "Namespace.Role.InitNamespacePermissionError": "初始化授权出错", "Namespace.Role.GetEnvGrantUserError": "加载 '{{env}}' 授权用户出错", "Namespace.Role.GetGrantUserError": "加载授权用户出错", "Namespace.Role.PleaseChooseUser": "请选择用户", "Namespace.Role.Added": "添加成功", "Namespace.Role.AddFailed": "添加失败", "Namespace.Role.Deleted": "删除成功", "Namespace.Role.DeleteFailed": "删除失败", "Config.Sync.Title": "同步配置", "Config.Sync.FistStep": "(第一步: 选择同步信息)", "Config.Sync.SecondStep": "(第二步: 检查 Diff)", "Config.Sync.PreviousStep": "上一步", "Config.Sync.NextStep": "下一步", "Config.Sync.Sync": "同步", "Config.Sync.Tips": "Tips", "Config.Sync.Tips1": "通过同步配置功能,可以使多个环境、集群间的配置保持一致", "Config.Sync.Tips2": "需要注意的是,同步完之后需要发布后才会对应用生效", "Config.Sync.SyncNamespace": "同步的 Namespace", "Config.Sync.SyncToCluster": "同步到哪个集群", "Config.Sync.NeedToSyncItem": "需要同步的配置", "Config.Sync.SortByLastModifyTime": "按最后更新时间过滤", "Config.Sync.BeginTime": "开始时间", "Config.Sync.EndTime": "结束时间", "Config.Sync.Filter": "过滤", "Config.Sync.Rest": "重置", "Config.Sync.ItemKey": "Key", "Config.Sync.ItemValue": "Value", "Config.Sync.ItemCreateTime": "Create Time", "Config.Sync.ItemUpdateTime": "Update Time", "Config.Sync.NoNeedSyncItem": "没有更新的配置", "Config.Sync.IgnoreSync": "忽略同步", "Config.Sync.Step2Type": "Type", "Config.Sync.Step2Key": "Key", "Config.Sync.Step2SyncBefore": "同步前", "Config.Sync.Step2SyncAfter": "同步后", "Config.Sync.Step2Comment": "Comment", "Config.Sync.Step2Operator": "操作", "Config.Sync.NewAdd": "新增", "Config.Sync.NoSyncItem": "不同步该配置", "Config.Sync.Delete": "删除", "Config.Sync.Update": "更新", "Config.Sync.SyncSuccessfully": "同步成功!", "Config.Sync.SyncFailed": "同步失败!", "Config.Sync.LoadingItemsError": "加载配置出错", "Config.Sync.PleaseChooseNeedSyncItems": "请选择需要同步的配置", "Config.Sync.PleaseChooseCluster": "请选择集群", "Config.History.Title": "发布历史", "Config.History.MasterVersionPublish": "主版本发布", "Config.History.MasterVersionRollback": "主版本回滚", "Config.History.GrayscaleOperator": "灰度操作", "Config.History.PublishHistory": "发布历史", "Config.History.OperationType0": "普通发布", "Config.History.OperationType1": "回滚", "Config.History.OperationType2": "灰度发布", "Config.History.OperationType3": "更新灰度规则", "Config.History.OperationType4": "灰度全量发布", "Config.History.OperationType5": "灰度发布(主版本发布)", "Config.History.OperationType6": "灰度发布(主版本回滚)", "Config.History.OperationType7": "放弃灰度", "Config.History.OperationType8": "删除灰度(全量发布)", "Config.History.UrgentPublish": "紧急发布", "Config.History.LoadMore": "加载更多", "Config.History.Abandoned": "已废弃", "Config.History.RollbackTo": "回滚到此版本", "Config.History.RollbackToTips": "回滚已发布的配置到此版本", "Config.History.ChangedItem": "变更的配置", "Config.History.ChangedItemTips": "查看此次发布与上次版本的变更", "Config.History.AllItem": "全部配置", "Config.History.AllItemTips": "查看此次发布的所有配置信息", "Config.History.ChangeType": "类型", "Config.History.ChangeKey": "Key", "Config.History.ChangeValue": "值", "Config.History.ChangeOldValue": "旧值", "Config.History.ChangeNewValue": "新值", "Config.History.ChangeTypeNew": "新增", "Config.History.ChangeTypeModify": "修改", "Config.History.ChangeTypeDelete": "删除", "Config.History.NoChange": "无配置更改", "Config.History.NoItem": "无配置", "Config.History.GrayscaleRule": "灰度规则", "Config.History.GrayscaleAppId": "灰度的 AppId", "Config.History.GrayscaleIp": "灰度的IP", "Config.History.NoGrayscaleRule": "无灰度规则", "Config.History.NoPermissionTips": "您不是该应用的管理员,也没有该 Namespace 的编辑或发布权限,无法查看发布历史", "Config.History.NoPublishHistory": "无发布历史信息", "Config.History.LoadingHistoryError": "无发布历史信息", "Config.Diff.Title": "比较配置", "Config.Diff.FirstStep": "(第一步:选择比较信息)", "Config.Diff.SecondStep": "(第二步:查看差异配置)", "Config.Diff.PreviousStep": "上一步", "Config.Diff.NextStep": "下一步", "Config.Diff.TipsTitle": "Tips", "Config.Diff.Tips": "通过比较配置功能,可以查看多个环境、集群间的配置差异", "Config.Diff.DiffCluster": "要比较的集群", "Config.Diff.DiffType": "比对方式", "Config.Diff.HasDiffComment": "是否比较注释", "Config.Diff.TextDiff": "文本", "Config.Diff.TableDiff": "表格", "Config.Diff.SearchKey": "搜索配置项", "Config.Diff.OnlyShowDiffKeys": "是否只显示值不一样的配置项", "Config.Diff.PleaseChooseTwoCluster": "请至少选择两个集群", "Config.Diff.TextDiffMostChooseTwoCluster": "文本比对至多选择两个集群", "ConfigExport.Title": "配置导出导入", "ConfigExport.Env.TitleTips" : "(通过导出导入配置,把一个集群的数据(应用、集群、Namespace)迁移到另外一个集群)", "ConfigExport.App.TitleTips" : "(通过导出导入配置,把一个应用在指定环境和集群下的所有配置迁移到另外一个集群)", "ConfigExport.SelectExportEnv" : "选择导出的环境", "ConfigExport.SelectImportEnv" : "选择导入的环境", "ConfigExport.ImportConflictLabel" : "导入时该如何处理已存在的 Namespace", "ConfigExport.ExportSuccess" : "正在导出数据,数据量大会导致速度慢,请耐心等待", "ConfigExport.ExportTips" : "数据量大的情况下,导出速度较慢请耐心等待", "ConfigExport.IgnoreExistedNamespace" : "跳过已存在的 Namespace", "ConfigExport.OverwriteExistedNamespace" : "覆盖已存在的 Namespace", "ConfigExport.UploadFile" : "上传导出的文件", "ConfigExport.UploadFileTip" : "请上传导出的压缩文件", "ConfigExport.ImportSuccess" : "导入成功", "ConfigExport.ImportingTip" : "正在导入,请耐心等待。导入完成后,请检查 Namespace 的配置是否正确,如果无误再发布 Namespace", "ConfigExport.ImportFailed" : "导入失败", "ConfigExport.ExportFailed" : "导出失败", "ConfigExport.NoPermissionTip" : "您不是应用管理员, 只有应用管理员才有导出/导入配置的权限", "ConfigExport.Export" : "导出", "ConfigExport.Import" : "导入", "ConfigExport.ImportTips" : "导入完成之后,请检查 Namespace 的配置是否正确,检查无误后需要发布才能生效", "ConfigExport.Download": "下载", "ConfigExport.Env.Tab": "按环境操作", "ConfigExport.App.Tab": "按应用集群操作", "ConfigExport.ClusterNameTips": "(导出/导入前请先查询应用集群信息)", "ConfigExport.PleaseEnterAppIdAndEnvAndCluster": "请输入 AppId、环境和集群名称,并查询", "ConfigExport.ClusterInfoContent": "AppId:'{{appId}}' 环境:'{{env}}' 集群名称:'{{clusterName}}'", "ConfigExport.EnvName": "环境名称", "ConfigExport.ClusterInfo": "集群信息", "ConfigImport.Title": "导入 Namespace", "ConfigImport.Tip1": "当配置项冲突时,导入的值将会覆盖已有的值", "ConfigImport.Tip2": "当配置项不冲突时,则会新增配置项", "ConfigImport.Tip3": "导入配置项后,需要发布才能生效", "App.CreateProject": "创建应用", "App.AppIdTips": "(应用唯一标识)", "App.AppNameTips": "(建议格式 xx-yy-zz 例: apollo-server)", "App.AppOwnerTips": "(开启应用管理员分配权限控制后,应用负责人和应用管理员默认为本账号,不可选择)", "App.AppAdminTips1": "(应用负责人默认具有应用管理员权限,", "App.AppAdminTips2": "应用管理员可以创建 Namespace 和集群、分配用户权限)", "App.AccessKey.NoPermissionTips": "您没有权限操作,请找 [{{users}}] 开通权限", "App.Setting.Title": "应用管理", "App.Setting.Admin": "管理员", "App.Setting.AdminTips": "(应用管理员具有以下权限: 1. 创建 Namespace 2. 创建集群 3. 管理应用、Namespace 权限)", "App.Setting.Add": "添加", "App.Setting.BasicInfo": "基本信息", "App.Setting.ProjectName": "应用名称", "App.Setting.ProjectNameTips": "(建议格式 xx-yy-zz 例: apollo-server)", "App.Setting.ProjectOwner": "应用负责人", "App.Setting.Modify": "修改应用信息", "App.Setting.Cancel": "取消修改", "App.Setting.NoPermissionTips": "您没有权限操作,请找 [{{users}}] 开通权限", "App.Setting.DeleteAdmin": "删除管理员", "App.Setting.CanNotDeleteAllAdmin": "不能删除所有的管理员", "App.Setting.PleaseChooseUser": "请选择用户", "App.Setting.Added": "添加成功", "App.Setting.AddFailed": "添加失败", "App.Setting.Deleted": "删除成功", "App.Setting.DeleteFailed": "删除失败", "App.Setting.Modified": "修改成功", "Valdr.App.AppId.Size": "AppId 长度不能多于 64 个字符", "Valdr.App.AppId.Required": "AppId 不能为空", "Valdr.App.appName.Size": "应用名称长度不能多于 128 个字符", "Valdr.App.appName.Required": "应用名称不能为空", "Valdr.Cluster.ClusterName.Size": "集群名称长度不能多于 32 个字符", "Valdr.Cluster.ClusterName.Required": "集群名称不能为空", "Valdr.AppNamespace.NamespaceName.Size": "Namespace 名称长度不能多于 32 个字符", "Valdr.AppNamespace.NamespaceName.Required": "Namespace 名称不能为空", "Valdr.AppNamespace.Comment.Size": "备注长度不能多于 64 个字符", "Valdr.Item.Key.Size": "Key 长度不能多于 128 个字符", "Valdr.Item.Key.Required": "Key 不能为空", "Valdr.Item.Comment.Size": "备注长度不能多于 256 个字符", "Valdr.Release.ReleaseName.Size": "Release Name 长度不能多于 64 个字符", "Valdr.Release.ReleaseName.Required": "Release Name 不能为空", "Valdr.Release.Comment.Size": "备注长度不能多于 256 个字符", "ApolloConfirmDialog.DefaultConfirmBtnName": "确认", "ApolloConfirmDialog.SearchPlaceHolder": "搜索 AppId、应用名、配置项 Key", "RulesModal.ChooseInstances": "从实例列表中选择", "RulesModal.InvalidIp": "不合法的 I P地址: '{{ip}}'", "RulesModal.GrayscaleAppIdCanNotBeNull": "灰度的 AppId 不能为空", "RulesModal.AppIdExistsRule": "已经存在 AppId = '{{appId}}' 的规则", "RulesModal.RuleListCanNotBeNull": "规则列表不能为空", "RulesModal.LabelListCanNotBeNull": "标签列表不能为空", "ItemModal.KeyExists": "key = '{{key}}' 已存在", "ItemModal.AddedTips": "添加成功,如需生效请发布", "ItemModal.AddFailed": "添加失败", "ItemModal.PleaseChooseCluster": "请选择集群", "ItemModal.ModifiedTips": "更新成功, 如需生效请发布", "ItemModal.ModifyFailed": "更新失败", "ItemModal.Tabs": "制表符", "ItemModal.NewLine": "换行符", "ItemModal.Space": "空格", "ItemModal.ChineseComma": "中文逗号", "ItemModal.JsonDuplicateKeyWarning": "JSON 中存在重复的 key,为避免数据丢失已跳过格式化", "ApolloNsPanel.LoadingHistoryError": "加载修改历史记录出错", "ApolloNsPanel.LoadingGrayscaleError": "加载修改历史记录出错", "ApolloNsPanel.Deleted": "删除成功", "ApolloNsPanel.GrayscaleModified": "灰度规则更新成功", "ApolloNsPanel.GrayscaleModifyFailed": "灰度规则更新失败", "ApolloNsPanel.ModifiedTips": "更新成功, 如需生效请发布", "ApolloNsPanel.ModifyFailed": "更新失败", "ApolloNsPanel.GrammarIsRight": "语法正确!", "ApolloNsPanel.JsonDuplicateKeyWarning": "JSON 中存在重复的 key,为避免数据丢失已切换为纯文本展示", "ReleaseModal.Published": "发布成功", "ReleaseModal.PublishFailed": "发布失败", "ReleaseModal.GrayscalePublished": "灰度发布成功", "ReleaseModal.GrayscalePublishFailed": "灰度发布失败", "ReleaseModal.AllPublished": "全量发布成功", "ReleaseModal.AllPublishFailed": "全量发布失败", "Rollback.NoRollbackList": "没有可以回滚的发布历史", "Rollback.SameAsCurrentRelease": "该版本与当前版本相同", "Rollback.RollbackSuccessfully": "回滚成功", "Rollback.RollbackFailed": "回滚失败", "Revoke.RevokeFailed": "撤销失败", "Revoke.RevokeSuccessfully": "撤销成功", "ApolloAuditLog.Disabled": "审计日志功能已关闭", "ApolloAuditLog.DisabledTips": "你可以通过添加\"apollo.audit.log.enabled = true\"到配置文件以打开审计日志功能", "ApolloAuditLog.MoreDetails": "更多细节请参考文档", "ApolloAuditLog.TraceAuditLogTips": "链路包含审计日志", "ApolloAuditLog.RelatedDataInfluenceTips": "审计日志相关数据变动", "ApolloAuditLog.DataInfluenceTips": "数据变动展示", "ApolloAuditLog.DataInfluence.EntityName": "实体名称", "ApolloAuditLog.DataInfluence.EntityId": "实体ID", "ApolloAuditLog.DataInfluence.AnyMatchedEntityId": "与条件匹配的实体", "ApolloAuditLog.DataInfluence.FieldName": "实体属性名", "ApolloAuditLog.DataInfluence.FieldNewValue": "属性值记录", "ApolloAuditLog.DataInfluence.Fields": "变化字段", "ApolloAuditLog.DataInfluence.MatchedFields": "匹配字段", "ApolloAuditLog.DataInfluence.HappenedTime": "记录时间", "ApolloAuditLog.TraceIdTips": "链路唯一ID", "ApolloAuditLog.OpName": "操作名称", "ApolloAuditLog.HappenedTime": "发生时间", "ApolloAuditLog.Description": "操作备注", "ApolloAuditLog.StartDate": "开始时间", "ApolloAuditLog.Title": "审计日志", "ApolloAuditLog.DoQuery": "查询", "ApolloAuditLog.OpNameTips": "输入操作名称", "ApolloAuditLog.OpType": "操作类型", "ApolloAuditLog.Operator": "操作人", "ApolloAuditLog.LoadMore": "加载更多", "ApolloAuditLog.DataInfluence.LoadMore": "加载更多", "ApolloAuditLog.EndDate": "结束时间", "ApolloAuditLog.NoTraceDetail": "没有链路记录", "ApolloAuditLog.NoDataInfluence": "没有相关数据变动", "ApolloAuditLog.TraceDetailTips": "链路记录展示", "ApolloAuditLog.SpanIdTips": "操作ID", "ApolloAuditLog.ParentSpan": "父操作", "ApolloAuditLog.FollowsFromSpan": "前操作", "ApolloAuditLog.FieldChangeHistory": "属性变动历史", "ApolloAuditLog.InfluenceEntity": "影响的审计实体", "Global.Title": "Value的全局搜索", "Global.App": "应用ID", "Global.Env": "环境", "Global.Cluster": "集群名", "Global.NameSpace": "命名空间", "Global.Key": "Key", "Global.Value": "Value", "Global.ValueSearch.Tips" : "(模糊搜索,key可为配置项名称或content,value为配置项值)", "Global.Operate" : "操作", "Global.Expand" : "展开", "Global.Abbreviate" : "缩略", "Global.JumpToEditPage" : "跳转到编辑页面", "Item.GlobalSearchByKey": "按照Key值检索", "Item.GlobalSearchByValue": "按照Value值检索", "Item.GlobalSearch": "查询", "Item.GlobalSearchSystemError": "系统出错,请重试或联系系统负责人", "Item.GlobalSearch.Tips": "搜索提示", "ApolloGlobalSearch.NoData" : "暂无数据,请进行检索或者添加", "Paging.TotalItems.part1" : "共", "Paging.TotalItems.part2" : "条记录", "Paging.DisplayNumber" : "条/页", "Paging.PageNumberOne" : "首页", "Paging.PageNumberLast" : "尾页" } ================================================ FILE: apollo-portal/src/main/resources/static/index.html ================================================ {{'Common.Title' | translate }}
================================================ FILE: apollo-portal/src/main/resources/static/login.html ================================================ {{ 'Common.Title' | translate }}
================================================ FILE: apollo-portal/src/main/resources/static/namespace/role.html ================================================ {{'Namespace.Role.Title' | translate }}
{{'Namespace.Role.AllEnv' | translate }}
{{env}}

{{'Namespace.Role.AllEnv' | translate }}
{{env}}

{{'Namespace.Role.NoPermission' | translate }}

================================================ FILE: apollo-portal/src/main/resources/static/namespace.html ================================================ {{'Namespace.Title' | translate }}
================================================ FILE: apollo-portal/src/main/resources/static/open/add-consumer.html ================================================ {{'Open.Manage.Title' | translate }}
{{'Open.Manage.CreateThirdApp' | translate }} {{'Open.Manage.CreateThirdAppTips' | translate }}

{{'Open.Manage.ThirdAppIdTips' | translate }}

{{'Open.Manage.Consumer.AllowCreateApplicationTips' | translate }}
{{ 'Open.Manage.Consumer.RateLimit.Enabled.Tips' | translate }}
{{'Open.Manage.Consumer.RateLimitValueTips' | translate }}
{{'Open.Manage.ThirdAppNameTips' | translate }}
{{'Open.Manage.GrantPermission' | translate }} {{'Open.Manage.GrantPermissionTips' | translate }}

{{'Open.Manage.ManagedNamespaceTips' | translate }}
{{'Open.Manage.GrantEnvTips' | translate }}

{{'Common.IsRootUser' | translate }}

================================================ FILE: apollo-portal/src/main/resources/static/open/grant-permission-modal.html ================================================ ================================================ FILE: apollo-portal/src/main/resources/static/open/manage.html ================================================ {{'Open.Manage.Title' | translate }}
{{'Open.Manage.CreateThirdApp' | translate }} {{'Open.Manage.CreateThirdAppTips' | translate }}
{{'Common.AppId' | translate }} {{'Common.AppName' | translate }} {{'Open.Manage.Consumer.AllowCreateApplication' | translate }} {{'Open.Manage.Consumer.RateLimitValue' | translate }} {{'Common.Department' | translate }} {{'Common.AppOwner' | translate }}/{{'Common.Email' | translate }} {{'Common.Operation' | translate}}
{{ consumer.appId }} {{ consumer.name }}
{{'Open.Manage.Consumer.AllowCreateApplication.Yes' | translate}}
{{'Open.Manage.Consumer.AllowCreateApplication.No' | translate}}
{{ consumer.rateLimit && consumer.rateLimit > 0 ? consumer.rateLimit : 'Open.Manage.Consumer.RateLimitValue.Display' | translate }} {{ consumer.orgName + '(' + consumer.orgId + ')' }} {{ consumer.ownerName }}/{{ consumer.ownerEmail }}
{{'Index.LoadMore' | translate }}

{{'Common.IsRootUser' | translate }}

================================================ FILE: apollo-portal/src/main/resources/static/scripts/AppUtils.js ================================================ /* * Copyright 2024 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ appUtil.service('AppUtil', ['toastr', '$window', '$q', '$translate', 'prefixLocation', function (toastr, $window, $q, $translate, prefixLocation) { function parseErrorMsg(response) { if (response.status == -1) { return $translate.instant('Common.LoginExpiredTips'); } var msg = "Code:" + response.status; if (response.data.message != null) { msg += " Msg:" + response.data.message; } return msg; } function parsePureErrorMsg(response) { if (response.status == -1) { return $translate.instant('Common.LoginExpiredTips'); } if (response.data.message != null) { return response.data.message; } return ""; } function ajax(resource, requestParams, requestBody) { var d = $q.defer(); if (requestBody) { resource(requestParams, requestBody, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); } else { resource(requestParams, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); } return d.promise; } /** * Check if a JSON string contains duplicate keys at any nesting level. * Performs a character-level scan to detect duplicate keys per object scope, * since JSON.parse silently deduplicates keys. * Returns true if duplicate keys are found. */ function hasDuplicateKeys(text) { try { // Character-level scan because JSON.parse reviver cannot detect // duplicates (browser already deduplicates). // Note: keys are compared after JSON decoding, so unicode escape equivalences are resolved. // Strategy: scan for "key": patterns respecting nesting depth. var i = 0; var len = text.length; var depth = 0; var keySets = []; while (i < len) { var ch = text.charAt(i); if (ch === '"') { // Read the full string var strStart = i; i++; // skip opening quote while (i < len) { if (text.charAt(i) === '\\') { i += 2; // skip escaped char } else if (text.charAt(i) === '"') { break; } else { i++; } } var strEnd = i; i++; // skip closing quote // Check if this string is a key (followed by ':') var j = i; while (j < len && (text.charAt(j) === ' ' || text.charAt(j) === '\t' || text.charAt(j) === '\n' || text.charAt(j) === '\r')) { j++; } if (j < len && text.charAt(j) === ':') { var rawKey = text.substring(strStart + 1, strEnd); var key; try { key = JSON.parse('"' + rawKey + '"'); } catch (e) { // If decoding fails, fall back to raw key (invalid JSON, but we still compare raw) key = rawKey; } if (depth >= 0 && depth < keySets.length) { if (key in keySets[depth]) { return true; } keySets[depth][key] = true; } } } else if (ch === '{') { depth++; while (keySets.length <= depth) { keySets.push(Object.create(null)); } keySets[depth] = Object.create(null); i++; } else if (ch === '}') { depth--; i++; } else { i++; } } return false; } catch (e) { return false; } } return { prefixPath: function(){ return prefixLocation; }, errorMsg: parseErrorMsg, pureErrorMsg: parsePureErrorMsg, ajax: ajax, showErrorMsg: function (response, title) { toastr.error(parseErrorMsg(response), title); }, parseParams: function (query, notJumpToHomePage) { if (!query) { //如果不传这个参数或者false则返回到首页(参数出错) if (!notJumpToHomePage) { $window.location.href = prefixLocation + '/index.html'; } else { return {}; } } if (query.indexOf('/') == 0) { query = query.substring(1, query.length); } var anchorIndex = query.indexOf('#'); if (anchorIndex >= 0) { query = query.substring(0, anchorIndex); } var params = query.split("&"); var result = {}; params.forEach(function (param) { var kv = param.split("="); result[kv[0]] = decodeURIComponent(kv[1]); }); return result; }, collectData: function (response) { var data = []; response.entities.forEach(function (entity) { if (entity.code == 200) { data.push(entity.body); } else { toastr.warning(entity.message); } }); return data; }, showModal: function (modal) { $(modal).modal("show"); }, hideModal: function (modal) { $(modal).modal("hide"); }, checkIPV4: function (ip) { return /^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$|^(([a-zA-Z]|[a-zA-Z][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z]|[A-Za-z][A-Za-z0-9\-]*[A-Za-z0-9])$|^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$/.test(ip); }, hasDuplicateKeys: hasDuplicateKeys } }]); ================================================ FILE: apollo-portal/src/main/resources/static/scripts/PageCommon.js ================================================ /* * Copyright 2024 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ $(document).ready(function () { // bootstrap tooltip & textarea scroll setInterval(function () { $('[data-tooltip="tooltip"]').tooltip({ trigger : 'hover' }); }, 1000); }); // (new Date()).Format("yyyy-MM-dd hh:mm:ss.S") ==> 2006-07-02 08:09:04.423 // (new Date()).Format("yyyy-M-d h:m:s.S") ==> 2006-7-2 8:9:4.18 Date.prototype.Format = function (fmt) { var o = { "M+": this.getMonth() + 1, //月份 "d+": this.getDate(), //日 "h+": this.getHours(), //小时 "m+": this.getMinutes(), //分 "s+": this.getSeconds(), //秒 "q+": Math.floor((this.getMonth() + 3) / 3), //季度 "S": this.getMilliseconds() //毫秒 }; if (/(y+)/.test(fmt)) { fmt = fmt.replace(RegExp.$1, (this.getFullYear() + "").substr(4 - RegExp.$1.length)); } for (var k in o) { if (new RegExp("(" + k + ")").test(fmt)) { fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length))); } } return fmt; }; ================================================ FILE: apollo-portal/src/main/resources/static/scripts/app.js ================================================ /* * Copyright 2024 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ var prefixPath = window.localStorage.getItem("prefixPath") || ""; /**utils*/ var appUtil = angular.module('app.util', ['toastr', 'ngCookies', 'pascalprecht.translate']) .constant("prefixLocation", prefixPath) // 前缀路径 .filter('prefixPath',['prefixLocation', function(prefixLocation) { // 前缀路径过滤器 return function(text) { return prefixLocation + text; } }]) .config(['$translateProvider','prefixLocation', function ($translateProvider,prefixLocation) { $translateProvider.useSanitizeValueStrategy(null); // disable sanitization by default $translateProvider.useCookieStorage(); $translateProvider.useStaticFilesLoader({ prefix: prefixLocation + '/i18n/', suffix: '.json' }); $translateProvider.registerAvailableLanguageKeys(['en', 'zh-CN'], { 'zh-*': 'zh-CN', 'zh': 'zh-CN', 'en-*': 'en', "*": "en" }) $translateProvider.uniformLanguageTag('bcp47').determinePreferredLanguage(); }]); /**service module 定义*/ var appService = angular.module('app.service', ['ngResource', 'app.util']) /** directive */ var directive_module = angular.module('apollo.directive', ['app.service', 'app.util', 'toastr', 'pascalprecht.translate']); /** page module 定义*/ // 首页 var index_module = angular.module('index', ['toastr', 'app.service', 'apollo.directive', 'app.util', 'angular-loading-bar', 'pascalprecht.translate']); //项目主页 var application_module = angular.module('application', ['app.service', 'apollo.directive', 'app.util', 'toastr', 'angular-loading-bar', 'valdr', 'ui.ace', 'ngSanitize']); //创建项目页面 var app_module = angular.module('create_app', ['apollo.directive', 'toastr', 'app.service', 'app.util', 'angular-loading-bar', 'valdr','pascalprecht.translate']); //配置同步页面 var sync_item_module = angular.module('sync_item', ['app.service', 'apollo.directive', 'app.util', 'toastr', 'angular-loading-bar']); // 比较页面 var diff_item_module = angular.module('diff_item', ['app.service', 'apollo.directive', 'app.util', 'toastr', 'angular-loading-bar']); //namespace var namespace_module = angular.module('namespace', ['app.service', 'apollo.directive', 'app.util', 'toastr', 'angular-loading-bar', 'valdr']); //server config var server_config_manage_module = angular.module('server_config_manage', ['app.service', 'apollo.directive', 'app.util', 'toastr', 'angular-loading-bar']); // Value的全局检索 var global_search_value_module = angular.module('global_search_value', ['app.service', 'apollo.directive', 'app.util', 'toastr', 'angular-loading-bar', 'ngSanitize']); //setting var setting_module = angular.module('setting', ['app.service', 'apollo.directive', 'app.util', 'toastr', 'angular-loading-bar', 'valdr']); //role var role_module = angular.module('role', ['app.service', 'apollo.directive', 'app.util', 'toastr', 'angular-loading-bar']); //cluster var cluster_module = angular.module('cluster', ['app.service', 'apollo.directive', 'app.util', 'toastr', 'angular-loading-bar', 'valdr']); //manage_cluster var manage_cluster_module = angular.module('manage_cluster', ['app.service', 'apollo.directive', 'app.util', 'toastr', 'angular-loading-bar', 'valdr']); //release history var release_history_module = angular.module('release_history', ['app.service', 'apollo.directive', 'app.util', 'toastr', 'angular-loading-bar']); //open manage var open_manage_module = angular.module('open_manage', ['app.service', 'apollo.directive', 'app.util', 'toastr', 'angular-loading-bar']); //user var user_module = angular.module('user', ['apollo.directive', 'toastr', 'app.service', 'app.util', 'angular-loading-bar', 'valdr']); //login var login_module = angular.module('login', ['app.service', 'toastr', 'app.util', 'pascalprecht.translate']); //delete app cluster namespace var delete_app_cluster_namespace_module = angular.module('delete_app_cluster_namespace', ['app.service', 'apollo.directive', 'app.util', 'toastr', 'angular-loading-bar']); //system info var system_info_module = angular.module('system_info', ['app.service', 'apollo.directive', 'app.util', 'toastr', 'angular-loading-bar']); //access secretKey var access_key_module = angular.module('access_key', ['app.service', 'apollo.directive', 'app.util', 'toastr', 'angular-loading-bar']); //config export var config_export_module = angular.module('config_export', ['app.service', 'apollo.directive', 'app.util', 'toastr', 'angular-loading-bar']); //audit log menu var audit_log_menu_module = angular.module('audit_log', ['app.service', 'apollo.directive', 'app.util', 'toastr', 'angular-loading-bar']); //audit log trace detail var audit_log_trace_detail_module = angular.module('audit_log_trace_detail', ['app.service', 'apollo.directive', 'app.util', 'toastr', 'angular-loading-bar']); ================================================ FILE: apollo-portal/src/main/resources/static/scripts/controller/AccessKeyController.js ================================================ /* * Copyright 2024 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ access_key_module.controller('AccessKeyController', ['$scope', '$location', '$translate', 'toastr', 'AppService', 'AppUtil', 'PermissionService', 'EnvService', 'UserService', 'AccessKeyService', AccessKeyController]); function AccessKeyController($scope, $location, $translate, toastr, AppService, AppUtil, PermissionService, EnvService, UserService, AccessKeyService) { var params = AppUtil.parseParams($location.$$url); $scope.pageContext = { appId: params.appid }; $scope.display = { app: { edit: false } }; $scope.addAccessKeySelectedEnv = ""; $scope.accessKeys = null; $scope.submitBtnDisabled = false; $scope.userSelectWidgetId = 'toAssignMasterRoleUser'; $scope.create = create; $scope.remove = remove; $scope.enable = enable; $scope.disable = disable; init(); function init() { initPermission(); initAdmins(); initApplication(); } function initPermission() { PermissionService.has_assign_user_permission($scope.pageContext.appId) .then(function (result) { $scope.hasAssignUserPermission = result.hasPermission; if (result.hasPermission) { initEnv(); } }); } function initEnv() { EnvService.find_all_envs() .then(function (result) { $scope.envs = result; initAccessKeys(); }); } function initAccessKeys() { $scope.accessKeys = {}; for (var iLoop = 0; iLoop < $scope.envs.length; iLoop++) { loadAccessKeys($scope.envs[iLoop]) } } function loadAccessKeys(env) { AccessKeyService.load_access_keys($scope.pageContext.appId, env) .then(function (result) { $scope.accessKeys[env] = result; }, function (result) { toastr.error(AppUtil.errorMsg(result), $translate.instant('AccessKey.LoadError', { env })); }); } function initAdmins() { PermissionService.get_app_role_users($scope.pageContext.appId) .then(function (result) { $scope.appRoleUsers = result; $scope.admins = []; $scope.appRoleUsers.masterUsers.forEach(function (user) { $scope.admins.push(_.escape(user.userId)); }); }); } function initApplication() { AppService.load($scope.pageContext.appId).then(function (app) { $scope.app = app; $scope.viewApp = _.clone(app); $('.project-setting .panel').removeClass('hidden'); }) } function create() { var env = $scope.addAccessKeySelectedEnv; UserService.load_user().then(function (result) { AccessKeyService.create_access_key($scope.pageContext.appId, env, result.userId) .then(function () { toastr.success($translate.instant('AccessKey.Operator.CreateSuccess', {env})); loadAccessKeys(env); }, function (result) { toastr.error(AppUtil.errorMsg(result), $translate.instant('AccessKey.Operator.CreateError', {env})); }); }); } function remove(id, env) { var confirmTips = $translate.instant('AccessKey.Operator.RemoveTips', { appId: $scope.pageContext.appId }); if (confirm(confirmTips)) { AccessKeyService.remove_access_key($scope.pageContext.appId, env, id) .then(function () { toastr.success($translate.instant('AccessKey.Operator.RemoveSuccess', {env})); loadAccessKeys(env); }, function (result) { toastr.error(AppUtil.errorMsg(result), $translate.instant('AccessKey.Operator.RemoveError', {env})); }); } } function enable(id, env, mode) { mode = (mode === 1) ? 1 : 0; var tipsPrefix = mode === 1 ? 'AccessKey.Operator.Observed' : 'AccessKey.Operator.Enabled'; var confirmTips = $translate.instant(tipsPrefix + 'Tips', { appId: $scope.pageContext.appId }); if (confirm(confirmTips)) { AccessKeyService.enable_access_key($scope.pageContext.appId, env, id, mode) .then(function () { toastr.success($translate.instant(tipsPrefix + 'Success', {env})); loadAccessKeys(env); }, function (result) { toastr.error(AppUtil.errorMsg(result), $translate.instant(tipsPrefix + 'Error', {env})); }); } } function disable(id, env) { var confirmTips = $translate.instant('AccessKey.Operator.DisabledTips', { appId: $scope.pageContext.appId }); if (confirm(confirmTips)) { AccessKeyService.disable_access_key($scope.pageContext.appId, env, id) .then(function () { toastr.success($translate.instant('AccessKey.Operator.DisabledSuccess', {env})); loadAccessKeys(env); }, function (result) { toastr.error(AppUtil.errorMsg(result), $translate.instant('AccessKey.Operator.DisabledError', {env})); }); } } } ================================================ FILE: apollo-portal/src/main/resources/static/scripts/controller/AppController.js ================================================ /* * Copyright 2024 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ app_module.controller('CreateAppController', ['$scope', '$window', '$translate', 'toastr', 'AppService', 'AppUtil', 'OrganizationService', 'SystemRoleService', 'UserService', createAppController]); function createAppController($scope, $window, $translate, toastr, AppService, AppUtil, OrganizationService, SystemRoleService, UserService) { $scope.app = {}; $scope.submitBtnDisabled = false; $scope.create = create; init(); function init() { initOrganization(); initSystemRole(); } function initOrganization() { OrganizationService.find_organizations().then(function (result) { var organizations = []; result.forEach(function (item) { var org = {}; org.id = item.orgId; org.text = item.orgName + '(' + item.orgId + ')'; org.name = item.orgName; organizations.push(org); }); $('#organization').select2({ placeholder: $translate.instant('Common.PleaseChooseDepartment'), width: '100%', data: organizations }); }, function (result) { toastr.error(AppUtil.errorMsg(result), "load organizations error"); }); } function initSystemRole() { SystemRoleService.has_open_manage_app_master_role_limit().then( function (value) { $scope.isOpenManageAppMasterRoleLimit = value.isManageAppMasterPermissionEnabled; UserService.load_user().then( function (value1) { $scope.currentUser = value1; }, function (reason) { toastr.error(AppUtil.errorMsg(reason), "load current user info failed"); }) }, function (reason) { toastr.error(AppUtil.errorMsg(reason), "init system role of manageAppMaster failed"); } ); } function create() { $scope.submitBtnDisabled = true; var selectedOrg = $('#organization').select2('data')[0]; if (!selectedOrg.id) { toastr.warning($translate.instant('Common.PleaseChooseDepartment')); $scope.submitBtnDisabled = false; return; } $scope.app.orgId = selectedOrg.id; $scope.app.orgName = selectedOrg.name; // owner var owner = $('.ownerSelector').select2('data')[0]; if ($scope.isOpenManageAppMasterRoleLimit) { owner = { id: $scope.currentUser.userId }; } if (!owner) { toastr.warning($translate.instant('Common.PleaseChooseOwner')); $scope.submitBtnDisabled = false; return; } $scope.app.ownerName = owner.id; //admins $scope.app.admins = []; var admins = $(".adminSelector").select2('data'); if ($scope.isOpenManageAppMasterRoleLimit) { admins = [{ id: $scope.currentUser.userId }]; } if (admins) { admins.forEach(function (admin) { $scope.app.admins.push(admin.id); }) } AppService.create($scope.app).then(function (result) { toastr.success($translate.instant('Common.Created')); setInterval(function () { $scope.submitBtnDisabled = false; $window.location.href = AppUtil.prefixPath() + '/config.html?#appid=' + result.appId; }, 1000); }, function (result) { $scope.submitBtnDisabled = false; toastr.error(AppUtil.errorMsg(result), $translate.instant('Common.CreateFailed')); }); } $(".J_ownerSelectorPanel").on("select2:select", ".ownerSelector", selectEventHandler); var $adminSelectorPanel = $(".J_adminSelectorPanel"); $adminSelectorPanel.on("select2:select", ".adminSelector", selectEventHandler); $adminSelectorPanel.on("select2:unselect", ".adminSelector", selectEventHandler); function selectEventHandler() { $('.J_owner').remove(); var owner = $('.ownerSelector').select2('data')[0]; if (owner) { $(".adminSelector").parent().find(".select2-selection__rendered").prepend( '
  • ' + _.escape(owner.text) + '
  • ') } } } ================================================ FILE: apollo-portal/src/main/resources/static/scripts/controller/AuditLogMenuController.js ================================================ /* * Copyright 2024 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ audit_log_menu_module.controller('AuditLogMenuController', ['$scope', '$window', '$translate', '$document', 'toastr', 'AppService', 'AppUtil', 'EventManager', 'AuditLogService', auditLogMenuController] ); function auditLogMenuController($scope, $window, $translate, $document, toastr, AppService, AppUtil, EventManager, AuditLogService) { $scope.auditEnabled = false; $scope.auditLogList = []; $scope.goToTraceDetailsPage = goToTraceDetailsPage; $scope.searchByOpNameAndDate = searchByOpNameAndDate; $scope.getMoreAuditLogs = getMoreAuditLogs; $scope.page = 0; var PAGE_SIZE = 10; $scope.opName = ''; $scope.startDate = null; $scope.endDate = null; $scope.startDateFmt = null; $scope.endDateFmt = null; $scope.hasLoadAll = false; $scope.options = []; $scope.showSearchDropdown = false; $scope.showOptions = function (query) { $scope.options = []; searchAuditLogs(query); }; $scope.selectOption = function (option) { $scope.opName = option.opName; $scope.showSearchDropdown = false; }; init(); function init() { getAuditProperties(); initSearchingMenu(); } function getAuditProperties() { AuditLogService.get_properties().then(function (result) { $scope.auditEnabled = result.enabled; }); } function initSearchingMenu() { AuditLogService.find_all_logs($scope.page, PAGE_SIZE).then(function (result) { if (!result || result.length < PAGE_SIZE) { $scope.hasLoadAll = true; } if (result.length === 0) { return; } $scope.auditLogList = $scope.auditLogList.concat(result); }); } function searchByOpNameAndDate(opName, startDate, endDate) { if (startDate !== null) { $scope.startDateFmt = new Date(startDate).Format("yyyy-MM-dd hh:mm:ss.S"); } if (endDate !== null) { $scope.endDateFmt = new Date(endDate).Format("yyyy-MM-dd hh:mm:ss.S"); } $scope.auditLogList = []; $scope.page = 0; $scope.opName = opName; $scope.startDate = startDate; $scope.endDate = endDate; AuditLogService.find_logs_by_opName( $scope.opName, $scope.startDateFmt, $scope.endDateFmt, $scope.page, PAGE_SIZE ).then(function (result) { if (!result || result.length < PAGE_SIZE) { $scope.hasLoadAll = true; } if (result.length === 0) { return; } $scope.auditLogList = $scope.auditLogList.concat(result); }); } function getMoreAuditLogs() { $scope.page = $scope.page + 1; if ($scope.opName === '') { AuditLogService.find_all_logs($scope.page, PAGE_SIZE).then(function (result) { if (!result || result.length < PAGE_SIZE) { $scope.hasLoadAll = true; } if (result.length === 0) { return; } $scope.auditLogList = $scope.auditLogList.concat(result); }); } else { AuditLogService.find_logs_by_opName( $scope.opName, $scope.startDateFmt, $scope.endDateFmt, $scope.page, PAGE_SIZE ).then(function (result) { if (!result || result.length < PAGE_SIZE) { $scope.hasLoadAll = true; } if (result.length === 0) { return; } $scope.auditLogList = $scope.auditLogList.concat(result); }); } } function searchAuditLogs(query) { AuditLogService.search_by_name_or_type_or_operator(query, 0, 20).then(function (result) { result.forEach(function (log) { var optionDisplay = log.opName + '-(' + log.opType + ').by:' + log.operator; var option = { id: log.id, display: optionDisplay, opName: log.opName }; $scope.options.push(option); }); $scope.showSearchDropdown = $scope.options.length > 0; }); } function goToTraceDetailsPage(traceId) { $window.location.href = AppUtil.prefixPath() + "/audit_log_trace_detail.html?#traceId=" + traceId; } $document.on('click', function (event) { if (!$scope.showSearchDropdown) { return; } var target = angular.element(event.target); // 检查点击的目标是否是输入框或下拉栏,如果不是,则隐藏下拉栏 if (!target.hasClass('form-control') && !target.hasClass('options-container')) { $scope.$apply(function () { $scope.showSearchDropdown = false; }); } }); } ================================================ FILE: apollo-portal/src/main/resources/static/scripts/controller/AuditLogTraceDetailController.js ================================================ /* * Copyright 2024 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ audit_log_trace_detail_module.controller('AuditLogTraceDetailController', ['$scope', '$location', '$window', '$translate', 'toastr', 'AppService', 'AppUtil', 'EventManager', 'AuditLogService', auditLogTraceDetailController] ); function auditLogTraceDetailController($scope, $location, $window, $translate, toastr, AppService, AppUtil, EventManager, AuditLogService) { var params = AppUtil.parseParams($location.$$url); $scope.traceId = params.traceId; $scope.traceDetailsTree = []; $scope.showingDetail = {}; $scope.dataInfluenceEntities = []; $scope.relatedDataInfluences = []; $scope.relatedDataInfluencePage = 0; $scope.relatedDataInfluenceHasLoadAll = true; var RelatedDataInfluencePageSize = 10; $scope.showText = showText; $scope.findMoreRelatedDataInfluence = findMoreRelatedDataInfluence; $scope.showRelatedDataInfluence = showRelatedDataInfluence; $scope.refreshDataInfluenceEntities = refreshDataInfluenceEntities; init(); function init() { buildTraceDetailsTree(); } function buildTraceDetailsTree() { AuditLogService.find_trace_details($scope.traceId).then( function (result) { $scope.traceDetails = result; $scope.traceDetailsTree = buildTree($scope.traceDetails); // 初始化 Bootstrap Treeview $(document).ready(function () { $('#treeview').treeview({ color: "#252525", showBorder: false, data: $scope.traceDetailsTree, levels: 99, showTags: true, onNodeSelected: function (event, data) { changeShowingDetail(data.metaDetail); } }); }); } ); function buildTree(data) { // 构建 spanId 到节点的映射 var nodeMap = new Map(); data.forEach(function (item) { nodeMap.set(item.logDTO.spanId, item); }); // 构建图的根节点列表 var roots = []; data.forEach(function (item) { var log = item.logDTO; var parentSpanId = log.parentSpanId; if (parentSpanId && nodeMap.has(parentSpanId)) { var parent = nodeMap.get(parentSpanId); if (!parent.children) { parent.children = []; } parent.children.push(item); } else { roots.push(item); } }); // 递归生成 Treeview 格式的节点 function buildTreeNode(node) { var log = node.logDTO; var treeNode = { text: log.opName, nodes: [], metaDetail: node }; if (node.children) { node.children.forEach(function (child) { treeNode.nodes.push(buildTreeNode(child)); }); } if (treeNode.nodes.length === 0) { delete treeNode.nodes; } return treeNode; } return roots.map(function (root) { return buildTreeNode(root); }); } function changeShowingDetail(data) { $scope.showingDetail = data; refreshDataInfluenceEntities(); } } function showRelatedDataInfluence(entityName, entityId, fieldName) { $scope.entityNameOfFindRelated = entityName; $scope.entityIdOfFindRelated = entityId; $scope.fieldNameOfFindRelated = fieldName; if (entityId === 'AnyMatched') { return; } AuditLogService.find_dataInfluences_by_field( $scope.entityNameOfFindRelated, $scope.entityIdOfFindRelated, $scope.fieldNameOfFindRelated, $scope.relatedDataInfluencePage, RelatedDataInfluencePageSize ).then(function (result) { if (!result || result.length < RelatedDataInfluencePageSize) { $scope.relatedDataInfluenceHasLoadAll = true; $scope.relatedDataInfluences = result; return; } if (result.length === 0) { return; } $scope.relatedDataInfluenceHasLoadAll = false; $scope.relatedDataInfluences = result; }); } function findMoreRelatedDataInfluence() { $scope.relatedDataInfluencePage = $scope.relatedDataInfluencePage + 1; AuditLogService.find_dataInfluences_by_field( $scope.entityNameOfFindRelated, $scope.entityIdOfFindRelated, $scope.fieldNameOfFindRelated, $scope.relatedDataInfluencePage, RelatedDataInfluencePageSize ).then(function (result) { if (!result || result.length < RelatedDataInfluencePageSize) { $scope.relatedDataInfluenceHasLoadAll = true; } if (result.length === 0) { return; } $scope.relatedDataInfluences = $scope.relatedDataInfluences.concat(result); }); } function refreshDataInfluenceEntities() { var entityMap = new Map(); $scope.showingDetail.dataInfluenceDTOList.forEach(function (dto) { var key = { name: dto.influenceEntityName, id: dto.influenceEntityId }; var keyString = JSON.stringify(key); var value = { name: dto.influenceEntityName, id: dto.influenceEntityId, dtoList: [] }; if (!entityMap.has(keyString)) { entityMap.set(keyString, value); } entityMap.get(keyString).dtoList.push(dto); }); $scope.dataInfluenceEntities = Array.from(entityMap); } function showText(text) { $scope.text = text; AppUtil.showModal("#showTextModal"); } } ================================================ FILE: apollo-portal/src/main/resources/static/scripts/controller/BackTopController.js ================================================ /* * Copyright 2024 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ appService.controller("BackTopController", ['$scope', BackTopController]); function BackTopController($scope) { // scroll status $scope.isScroll = false; $scope.backToTop = function () { window.scrollTo(0, 0) } window.addEventListener("scroll", function () { if(!window.scrollY && $scope.isScroll) { $scope.isScroll = false document.body.click() } if(window.scrollY != 0 && !$scope.isScroll) { $scope.isScroll = true document.body.click() } }) } ================================================ FILE: apollo-portal/src/main/resources/static/scripts/controller/ClusterController.js ================================================ /* * Copyright 2024 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ cluster_module.controller('ClusterController', ['$scope', '$location', '$window', '$translate', 'toastr', 'AppService', 'EnvService', 'ClusterService', 'AppUtil', function ($scope, $location, $window, $translate, toastr, AppService, EnvService, ClusterService, AppUtil) { var params = AppUtil.parseParams($location.$$url); $scope.appId = params.appid; $scope.step = 1; $scope.submitBtnDisabled = false; EnvService.find_all_envs().then(function (result) { $scope.envs = []; result.forEach(function (env) { $scope.envs.push({ name: env, checked: false }); }); $(".apollo-container").removeClass("hidden"); }, function (result) { toastr.error(AppUtil.errorMsg(result), $translate.instant('Cluster.LoadingEnvironmentError')); }); $scope.clusterName = ''; $scope.clusterComment = ''; $scope.switchChecked = function (env, $event) { env.checked = !env.checked; $event.stopPropagation(); }; $scope.toggleEnvCheckedStatus = function (env) { env.checked = !env.checked; }; $scope.create = function () { var noEnvChecked = true; $scope.envs.forEach(function (env) { if (env.checked) { noEnvChecked = false; $scope.submitBtnDisabled = true; ClusterService.create_cluster($scope.appId, env.name, { name: $scope.clusterName, comment: $scope.clusterComment, appId: $scope.appId }).then(function (result) { toastr.success(env.name, $translate.instant('Cluster.ClusterCreated')); $scope.step = 2; $scope.submitBtnDisabled = false; }, function (result) { toastr.error(AppUtil.errorMsg(result), $translate.instant('Cluster.ClusterCreateFailed')); $scope.submitBtnDisabled = false; }) } }); if (noEnvChecked) { toastr.warning($translate.instant('Cluster.PleaseChooseEnvironment')); } }; }]); ================================================ FILE: apollo-portal/src/main/resources/static/scripts/controller/ConfigExportController.js ================================================ /* * Copyright 2024 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ config_export_module.controller('ConfigExportController', ['$scope', '$location', '$window', '$http', '$translate', 'toastr', 'AppService', 'EnvService', 'ExportService', 'ClusterService', 'AppUtil', function ($scope, $location, $window, $http, $translate, toastr, AppService, EnvService, ExportService, ClusterService, AppUtil) { $scope.conflictAction = 'ignore'; $scope.cluster = {}; $scope.appConfigBtnDisabled = true; EnvService.find_all_envs().then(function (result) { $scope.exportEnvs = []; $scope.importEnvs = []; result.forEach(function (env) { $scope.exportEnvs.push({name: env, checked: false}); $scope.importEnvs.push({name: env, checked: false}); }); $(".apollo-container").removeClass("hidden"); }, function (result) { toastr.error(AppUtil.errorMsg(result), $translate.instant('Cluster.LoadingEnvironmentError')); }); $scope.switchChecked = function (env, $event) { env.checked = !env.checked; $event.stopPropagation(); }; $scope.toggleEnvCheckedStatus = function (env) { env.checked = !env.checked; }; $scope.export = function () { var selectedEnvs = []; $scope.exportEnvs.forEach(function (env) { if (env.checked) { selectedEnvs.push(env.name); } }); if (selectedEnvs.length === 0) { toastr.warning($translate.instant('Cluster.PleaseChooseEnvironment')); return; } var selectedEnvStr = selectedEnvs.join(","); $window.location.href = AppUtil.prefixPath() + '/configs/export?envs=' + selectedEnvStr; toastr.success($translate.instant('ConfigExport.ExportSuccess')); }; $scope.import = function () { var selectedEnvs = [] $scope.importEnvs.forEach(function (env) { if (env.checked) { selectedEnvs.push(env.name); } }); if (selectedEnvs.length === 0) { toastr.warning($translate.instant('Cluster.PleaseChooseEnvironment')); return } var selectedEnvStr = selectedEnvs.join(","); var file = document.getElementById("envFileUpload").files[0]; if (file == null) { toastr.warning($translate.instant('ConfigExport.UploadFileTip')) return } var form = new FormData(); form.append('file', file); $http({ method: 'POST', url: AppUtil.prefixPath() + '/configs/import?envs=' + selectedEnvStr + "&conflictAction=" + $scope.conflictAction, data: form, headers: {'Content-Type': undefined}, transformRequest: angular.identity }).success(function (data) { toastr.success(data, $translate.instant('ConfigExport.ImportSuccess')) }).error(function (data) { toastr.error(data, $translate.instant('ConfigExport.ImportFailed')) }) toastr.info($translate.instant('ConfigExport.ImportingTip')) }; $scope.getClusterInfo = function () { if (!$scope.cluster.appId || !$scope.cluster.env || !$scope.cluster.name) { $scope.appConfigBtnDisabled = true; toastr.warning($translate.instant('ConfigExport.PleaseEnterAppIdAndEnvAndCluster')); return; } $scope.cluster.info = ""; ClusterService.load_cluster($scope.cluster.appId, $scope.cluster.env, $scope.cluster.name).then(function (result) { $scope.cluster.info = $translate.instant('ConfigExport.ClusterInfoContent', { appId: result.appId, env: $scope.cluster.env, clusterName: result.name }); $scope.appConfigBtnDisabled = false; }, function (result) { $scope.appConfigBtnDisabled = true; AppUtil.showErrorMsg(result); }); }; $scope.exportAppConfig = function () { if (!$scope.cluster.appId || !$scope.cluster.env || !$scope.cluster.name || !$scope.cluster.info) { toastr.warning($translate.instant('ConfigExport.PleaseEnterAppIdAndEnvAndCluster')); return; } var exportUrl = AppUtil.prefixPath() + '/apps/' + $scope.cluster.appId + '/envs/' + $scope.cluster.env + '/clusters/' + $scope.cluster.name + '/export'; $http({ method: 'HEAD', url: exportUrl }).then(function(response) { $window.location.href = exportUrl; setTimeout(function() { toastr.success($translate.instant('ConfigExport.ExportSuccess')); }, 1000); }).catch(function(response) { if (response.status === 403) { toastr.warning($translate.instant('ConfigExport.NoPermissionTip')); return; } toastr.error($translate.instant('ConfigExport.ExportFailed')); }); }; $scope.importAppConfig = function () { if (!$scope.cluster.appId || !$scope.cluster.env || !$scope.cluster.name || !$scope.cluster.info) { toastr.warning($translate.instant('ConfigExport.PleaseEnterAppIdAndEnvAndCluster')); return; } var file = document.getElementById("appFileUpload").files[0]; if (file == null) { toastr.warning($translate.instant('ConfigExport.UploadFileTip')); return; } var form = new FormData(); form.append('file', file); $http({ method: 'POST', url: AppUtil.prefixPath() + '/apps/' + $scope.cluster.appId + '/envs/' + $scope.cluster.env + '/clusters/' + $scope.cluster.name + '/import?conflictAction=' + $scope.conflictAction, data: form, headers: {'Content-Type': undefined}, transformRequest: angular.identity }).success(function (data) { toastr.success(data, $translate.instant('ConfigExport.ImportSuccess')); }).error(function (data, status) { if (status === 403) { toastr.warning($translate.instant('ConfigExport.NoPermissionTip')); return; } toastr.error(data, $translate.instant('ConfigExport.ImportFailed')); }); toastr.info($translate.instant('ConfigExport.ImportingTip')); }; }]); ================================================ FILE: apollo-portal/src/main/resources/static/scripts/controller/DeleteAppClusterNamespaceController.js ================================================ /* * Copyright 2024 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ delete_app_cluster_namespace_module.controller('DeleteAppClusterNamespaceController', ['$scope', '$translate', 'toastr', 'AppUtil', 'AppService', 'ClusterService', 'NamespaceService', 'PermissionService', DeleteAppClusterNamespaceController]); function DeleteAppClusterNamespaceController($scope, $translate, toastr, AppUtil, AppService, ClusterService, NamespaceService, PermissionService) { $scope.app = {}; $scope.deleteAppBtnDisabled = true; $scope.getAppInfo = getAppInfo; $scope.deleteApp = deleteApp; $scope.cluster = {}; $scope.deleteClusterBtnDisabled = true; $scope.getClusterInfo = getClusterInfo; $scope.deleteCluster = deleteCluster; $scope.appNamespace = {}; $scope.deleteAppNamespaceBtnDisabled = true; $scope.getAppNamespaceInfo = getAppNamespaceInfo; $scope.deleteAppNamespace = deleteAppNamespace; initPermission(); function initPermission() { PermissionService.has_root_permission() .then(function (result) { $scope.isRootUser = result.hasPermission; }) } function getAppInfo() { if (!$scope.app.appId) { toastr.warning($translate.instant('Delete.PleaseEnterAppId')); return; } $scope.app.info = ""; AppService.load($scope.app.appId).then(function (result) { if (!result.appId) { toastr.warning($translate.instant('Delete.AppIdNotFound', { appId: $scope.app.appId })); $scope.deleteAppBtnDisabled = true; return; } $scope.app.info = $translate.instant('Delete.AppInfoContent', { appName: result.name, departmentName: result.orgName, departmentId: result.orgId, ownerName: result.ownerName }); $scope.deleteAppBtnDisabled = false; }, function (result) { AppUtil.showErrorMsg(result); }); } function deleteApp() { if (!$scope.app.appId) { toastr.warning($translate.instant('Delete.PleaseEnterAppId')); return; } if (confirm($translate.instant('Delete.ConfirmDeleteAppId', { appId: $scope.app.appId }))) { AppService.delete_app($scope.app.appId).then(function (result) { toastr.success($translate.instant('Delete.Deleted')); $scope.deleteAppBtnDisabled = true; }, function (result) { AppUtil.showErrorMsg(result); }) } } function getClusterInfo() { if (!$scope.cluster.appId || !$scope.cluster.env || !$scope.cluster.name) { toastr.warning($translate.instant('Delete.PleaseEnterAppIdAndEnvAndCluster')); return; } $scope.cluster.info = ""; ClusterService.load_cluster($scope.cluster.appId, $scope.cluster.env, $scope.cluster.name).then(function (result) { $scope.cluster.info = $translate.instant('Delete.ClusterInfoContent', { appId: result.appId, env: $scope.cluster.env, clusterName: result.name }); $scope.deleteClusterBtnDisabled = false; }, function (result) { AppUtil.showErrorMsg(result); }); } function deleteCluster() { if (!$scope.cluster.appId || !$scope.cluster.env || !$scope.cluster.name) { toastr.warning($translate.instant('Delete.PleaseEnterAppIdAndEnvAndCluster')); return; } var confirmTip = $translate.instant('Delete.ConfirmDeleteCluster', { appId: $scope.cluster.appId, env: $scope.cluster.env, clusterName: $scope.cluster.name }); if (confirm(confirmTip)) { ClusterService.delete_cluster($scope.cluster.appId, $scope.cluster.env, $scope.cluster.name).then(function (result) { toastr.success($translate.instant('Delete.Deleted')); $scope.deleteClusterBtnDisabled = true; }, function (result) { AppUtil.showErrorMsg(result); }) } } function getAppNamespaceInfo() { if (!$scope.appNamespace.appId || !$scope.appNamespace.name) { toastr.warning($translate.instant('Delete.PleaseEnterAppIdAndNamespace')); return; } $scope.appNamespace.info = ""; NamespaceService.loadAppNamespace($scope.appNamespace.appId, $scope.appNamespace.name).then(function (result) { $scope.appNamespace.info = $translate.instant('Delete.AppNamespaceInfoContent', { appId: result.appId, namespace: result.name, isPublic: result.isPublic }); $scope.deleteAppNamespaceBtnDisabled = false; }, function (result) { AppUtil.showErrorMsg(result); }); } function deleteAppNamespace() { if (!$scope.appNamespace.appId || !$scope.appNamespace.name) { toastr.warning($translate.instant('Delete.PleaseEnterAppIdAndNamespace')); return; } var confirmTip = $translate.instant('Delete.ConfirmDeleteNamespace', { appId: $scope.appNamespace.appId, namespace: $scope.appNamespace.name }); if (confirm(confirmTip)) { NamespaceService.deleteAppNamespace($scope.appNamespace.appId, $scope.appNamespace.name).then(function (result) { toastr.success($translate.instant('Delete.Deleted')); $scope.deleteAppNamespaceBtnDisabled = true; }, function (result) { AppUtil.showErrorMsg(result); }) } } } ================================================ FILE: apollo-portal/src/main/resources/static/scripts/controller/GlobalSearchValueController.js ================================================ /* * Copyright 2024 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ global_search_value_module.controller('GlobalSearchValueController', ['$scope', '$window', '$translate', 'toastr', 'AppUtil', 'GlobalSearchValueService', 'PermissionService', GlobalSearchValueController]); function GlobalSearchValueController($scope, $window, $translate, toastr, AppUtil, GlobalSearchValueService, PermissionService) { $scope.allItemInfo = []; $scope.pageItemInfo = []; $scope.itemInfoSearchKey = ''; $scope.itemInfoSearchValue = ''; $scope.needToBeHighlightedKey = ''; $scope.needToBeHighlightedValue = ''; $scope.isShowHighlightKeyword = []; $scope.isAllItemInfoDirectlyDisplayKeyWithoutShowHighlightKeyword = []; $scope.isAllItemInfoDirectlyDisplayValueWithoutShowHighlightKeyword = []; $scope.isAllItemInfoDisplayValueInARow = []; $scope.isPageItemInfoDirectlyDisplayKeyWithoutShowHighlightKeyword = []; $scope.isPageItemInfoDirectlyDisplayValueWithoutShowHighlightKeyword = []; $scope.isPageItemInfoDisplayValueInARow = []; $scope.currentPage = 1; $scope.pageSize = '10'; $scope.totalItems = 0; $scope.totalPages = 0; $scope.pagesArray = []; $scope.tempKey = ''; $scope.tempValue = ''; $scope.getItemInfoByKeyAndValue = getItemInfoByKeyAndValue; $scope.highlightKeyword = highlightKeyword; $scope.jumpToTheEditingPage = jumpToTheEditingPage; $scope.isShowAllValue = isShowAllValue; $scope.convertPageSizeToInt = convertPageSizeToInt; $scope.changePage = changePage; $scope.getPagesArray = getPagesArray; $scope.determineDisplayKeyOrValueWithoutShowHighlightKeyword = determineDisplayKeyOrValueWithoutShowHighlightKeyword; $scope.determineDisplayValueInARow = determineDisplayValueInARow; init(); function init() { initPermission(); } function initPermission() { PermissionService.has_root_permission() .then(function (result) { $scope.isRootUser = result.hasPermission; }); } function getItemInfoByKeyAndValue(itemInfoSearchKey, itemInfoSearchValue) { $scope.currentPage = 1; $scope.itemInfoSearchKey = itemInfoSearchKey || ''; $scope.itemInfoSearchValue = itemInfoSearchValue || ''; $scope.allItemInfo = []; $scope.pageItemInfo = []; $scope.isAllItemInfoDirectlyDisplayKeyWithoutShowHighlightKeyword = []; $scope.isAllItemInfoDirectlyDisplayValueWithoutShowHighlightKeyword = []; $scope.isAllItemInfoDisplayValueInARow = []; $scope.isPageItemInfoDirectlyDisplayKeyWithoutShowHighlightKeyword = []; $scope.isPageItemInfoDirectlyDisplayValueWithoutShowHighlightKeyword = []; $scope.isPageItemInfoDisplayValueInARow = []; $scope.tempKey = itemInfoSearchKey || ''; $scope.tempValue = itemInfoSearchValue || ''; $scope.isShowHighlightKeyword = []; GlobalSearchValueService.findItemInfoByKeyAndValue($scope.itemInfoSearchKey, $scope.itemInfoSearchValue) .then(handleSuccess).catch(handleError); function handleSuccess(result) { let allItemInfo = []; let isAllItemInfoDirectlyDisplayValueWithoutShowHighlightKeyword = []; let isAllItemInfoDirectlyDisplayKeyWithoutShowHighlightKeyword = []; let isAllItemInfoDisplayValueInARow = []; if(($scope.itemInfoSearchKey === '') && !($scope.itemInfoSearchValue === '')){ $scope.needToBeHighlightedValue = $scope.itemInfoSearchValue; $scope.needToBeHighlightedKey = ''; result.body.forEach((itemInfo, index) => { allItemInfo.push(itemInfo); isAllItemInfoDirectlyDisplayKeyWithoutShowHighlightKeyword[index] = '0'; isAllItemInfoDirectlyDisplayValueWithoutShowHighlightKeyword[index] = determineDisplayKeyOrValueWithoutShowHighlightKeyword(itemInfo.value, itemInfoSearchValue); isAllItemInfoDisplayValueInARow[index] = determineDisplayValueInARow(itemInfo.value, itemInfoSearchValue); }); }else if(!($scope.itemInfoSearchKey === '') && ($scope.itemInfoSearchValue === '')){ $scope.needToBeHighlightedKey = $scope.itemInfoSearchKey; $scope.needToBeHighlightedValue = ''; result.body.forEach((itemInfo, index) => { allItemInfo.push(itemInfo); isAllItemInfoDirectlyDisplayKeyWithoutShowHighlightKeyword[index] = determineDisplayKeyOrValueWithoutShowHighlightKeyword(itemInfo.key, itemInfoSearchKey); isAllItemInfoDirectlyDisplayValueWithoutShowHighlightKeyword[index] = '0'; }); }else{ $scope.needToBeHighlightedKey = $scope.itemInfoSearchKey; $scope.needToBeHighlightedValue = $scope.itemInfoSearchValue; result.body.forEach((itemInfo, index) => { allItemInfo.push(itemInfo); isAllItemInfoDirectlyDisplayValueWithoutShowHighlightKeyword[index] = determineDisplayKeyOrValueWithoutShowHighlightKeyword(itemInfo.value, itemInfoSearchValue); isAllItemInfoDirectlyDisplayKeyWithoutShowHighlightKeyword[index] = determineDisplayKeyOrValueWithoutShowHighlightKeyword(itemInfo.key, itemInfoSearchKey); isAllItemInfoDisplayValueInARow[index] = determineDisplayValueInARow(itemInfo.value, itemInfoSearchValue); }); } $scope.totalItems = allItemInfo.length; $scope.allItemInfo = allItemInfo; $scope.totalPages = Math.ceil($scope.totalItems / parseInt($scope.pageSize, 10)); const startIndex = ($scope.currentPage - 1) * parseInt($scope.pageSize, 10); const endIndex = Math.min(startIndex + parseInt($scope.pageSize, 10), allItemInfo.length); $scope.pageItemInfo = allItemInfo.slice(startIndex, endIndex); $scope.isAllItemInfoDirectlyDisplayValueWithoutShowHighlightKeyword = isAllItemInfoDirectlyDisplayValueWithoutShowHighlightKeyword; $scope.isAllItemInfoDirectlyDisplayKeyWithoutShowHighlightKeyword = isAllItemInfoDirectlyDisplayKeyWithoutShowHighlightKeyword; $scope.isAllItemInfoDisplayValueInARow = isAllItemInfoDisplayValueInARow; $scope.isPageItemInfoDirectlyDisplayValueWithoutShowHighlightKeyword = isAllItemInfoDirectlyDisplayValueWithoutShowHighlightKeyword.slice(startIndex, endIndex); $scope.isPageItemInfoDirectlyDisplayKeyWithoutShowHighlightKeyword = isAllItemInfoDirectlyDisplayKeyWithoutShowHighlightKeyword.slice(startIndex, endIndex); $scope.isPageItemInfoDisplayValueInARow = isAllItemInfoDisplayValueInARow.slice(startIndex, endIndex); getPagesArray(); if(result.hasMoreData){ toastr.warning(result.message, $translate.instant('Item.GlobalSearch.Tips')); } } function handleError(error) { $scope.itemInfo = []; toastr.error(AppUtil.errorMsg(error), $translate.instant('Item.GlobalSearchSystemError')); } } function convertPageSizeToInt() { getItemInfoByKeyAndValue($scope.tempKey, $scope.tempValue); } function changePage(page) { if (page >= 1 && page <= $scope.totalPages) { $scope.currentPage = page; $scope.isShowHighlightKeyword = []; $scope.isPageItemInfoDirectlyDisplayValueWithoutShowHighlightKeyword = []; $scope.isPageItemInfoDirectlyDisplayKeyWithoutShowHighlightKeyword = []; $scope.isPageItemInfoDisplayValueInARow = []; $scope.itemInfoSearchKey = $scope.tempKey; $scope.itemInfoSearchValue = $scope.tempValue; const startIndex = ($scope.currentPage - 1)* parseInt($scope.pageSize, 10); const endIndex = Math.min(startIndex + parseInt($scope.pageSize, 10), $scope.totalItems); $scope.pageItemInfo = $scope.allItemInfo.slice(startIndex, endIndex); $scope.isPageItemInfoDirectlyDisplayValueWithoutShowHighlightKeyword = $scope.isAllItemInfoDirectlyDisplayValueWithoutShowHighlightKeyword.slice(startIndex, endIndex); $scope.isPageItemInfoDirectlyDisplayKeyWithoutShowHighlightKeyword = $scope.isAllItemInfoDirectlyDisplayKeyWithoutShowHighlightKeyword.slice(startIndex, endIndex); $scope.isPageItemInfoDisplayValueInARow = $scope.isAllItemInfoDisplayValueInARow.slice(startIndex, endIndex); getPagesArray(); } } function getPagesArray() { const pageRange = 2; let pagesArray = []; let currentPage = $scope.currentPage; let totalPages = $scope.totalPages; if (totalPages <= (pageRange * 2) + 4) { for (let i = 1; i <= totalPages; i++) { pagesArray.push(i); } } else { if (currentPage <= (pageRange + 2)) { for (let i = 1; i <= pageRange * 2 + 2; i++) { pagesArray.push(i); } pagesArray.push('...'); pagesArray.push(totalPages); } else if (currentPage >= (totalPages - (pageRange + 1))) { for (let i = totalPages - pageRange * 2 - 1 ; i <= totalPages; i++) { pagesArray.push(i); } pagesArray.unshift('...'); pagesArray.unshift(1); } else { for (let i = (currentPage - pageRange); i <= currentPage + pageRange; i++) { pagesArray.push(i); } pagesArray.unshift('...'); pagesArray.unshift(1); pagesArray.push('...'); pagesArray.push(totalPages); } } $scope.pagesArray = pagesArray; } function determineDisplayValueInARow(value, highlight) { var valueColumn = document.getElementById('valueColumn'); var testElement = document.createElement('span'); setupTestElement(testElement, valueColumn); testElement.innerText = value; document.body.appendChild(testElement); const position = determinePosition(value, highlight); let displayValue = '0'; if (testElement.scrollWidth > testElement.offsetWidth) { displayValue = position; } else { if (testElement.scrollWidth === testElement.offsetWidth) { return '0'; } switch (position) { case '1': testElement.innerText = value + '...' + '| ' + $translate.instant('Global.Expand'); break; case '2': testElement.innerText = '...' + value + '| ' + $translate.instant('Global.Expand'); break; case '3': testElement.innerText = '...' + value + '...' + '| ' + $translate.instant('Global.Expand'); break; default: return '0'; } if (testElement.scrollWidth === testElement.offsetWidth) { displayValue = '0'; } else { displayValue = position; } } document.body.removeChild(testElement); return displayValue; } function setupTestElement(element, valueColumn) { element.style.visibility = 'hidden'; element.style.position = 'absolute'; element.style.whiteSpace = 'nowrap'; element.style.display = 'inline-block'; element.style.fontFamily = '"Open Sans", sans-serif'; const devicePixelRatio = window.devicePixelRatio; const zoomLevel = Math.round((window.outerWidth / window.innerWidth) * 100) / 100; element.style.fontSize = 13 * devicePixelRatio * zoomLevel + 'px'; element.style.padding = 8 * devicePixelRatio * zoomLevel + 'px'; element.style.width = valueColumn.offsetWidth * devicePixelRatio * zoomLevel + 'px'; } function determinePosition(value, highlight) { const position = value.indexOf(highlight); if (position === -1) return '-1'; if (position === 0) return '1'; if (position + highlight.length === value.length) return '2'; return "3"; } function determineDisplayKeyOrValueWithoutShowHighlightKeyword(keyorvalue, highlight) { return keyorvalue === highlight ? '0' : '-1'; } function jumpToTheEditingPage(appid,env,cluster){ let url = AppUtil.prefixPath() + "/config.html#/appid=" + appid + "&" +"env=" + env + "&" + "cluster=" + cluster; window.open(url, '_blank'); } function highlightKeyword(fulltext, keyword) { fulltext = fulltext || ''; if (!keyword || keyword.length === 0) { return escapeHtml(fulltext); } try { const escapedKeyword = keyword.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); const regex = new RegExp('(' + escapedKeyword + ')', 'g'); return fulltext.split(regex).map(function (part, index) { return (index % 2 === 1) ? '' + escapeHtml(part) + '' : escapeHtml(part); }).join(''); } catch (e) { return escapeHtml(fulltext); } } function escapeHtml(text) { return (text || '').replace(/[&<>"']/g, function (char) { switch (char) { case '&': return '&'; case '<': return '<'; case '>': return '>'; case '"': return '"'; case "'": return '''; default: return char; } }); } function isShowAllValue(index){ $scope.isShowHighlightKeyword[index] = !$scope.isShowHighlightKeyword[index]; } } ================================================ FILE: apollo-portal/src/main/resources/static/scripts/controller/IndexController.js ================================================ /* * Copyright 2024 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ index_module.controller('IndexController', ['$scope', '$window', '$translate', 'toastr', 'AppUtil', 'AppService', 'UserService', 'FavoriteService', 'NamespaceService', IndexController] ) function IndexController($scope, $window, $translate, toastr, AppUtil, AppService, UserService, FavoriteService, NamespaceService) { $scope.userId = ''; $scope.whichContent = '1'; $scope.getUserCreatedApps = getUserCreatedApps; $scope.getUserFavorites = getUserFavorites; $scope.getPublicNamespaces = getPublicNamespaces; $scope.goToAppHomePage = goToAppHomePage; $scope.goToCreateAppPage = goToCreateAppPage; $scope.toggleOperationBtn = toggleOperationBtn; $scope.toTop = toTop; $scope.deleteFavorite = deleteFavorite; $scope.morePublicNamespace = morePublicNamespace; $scope.changeContent = changeContent; function initCreateApplicationPermission() { AppService.has_create_application_role($scope.userId).then( function (value) { $scope.hasCreateApplicationPermission = value.hasCreateApplicationPermission; }, function (reason) { toastr.warning(AppUtil.errorMsg(reason), $translate.instant('Index.GetCreateAppRoleFailed')); } ) } UserService.load_user().then(function (result) { $scope.userId = result.userId; $scope.createdAppPage = 0; $scope.createdApps = []; $scope.hasMoreCreatedApps = false; $scope.favoritesPage = 0; $scope.favorites = []; $scope.hasMoreFavorites = false; $scope.publicNamespacePage = 0; $scope.publicNamespaces = []; $scope.hasMorePublicNamespaces = false; $scope.allPublicNamespaces = []; $scope.visitedApps = []; initCreateApplicationPermission(); getUserCreatedApps(); getUserFavorites(); getPublicNamespaces(); initUserVisitedApps(); }); function getUserCreatedApps() { var size = 10; AppService.find_app_by_self($scope.createdAppPage, size) .then(function (result) { $scope.createdAppPage += 1; $scope.hasMoreCreatedApps = result.length == size; if (!result || result.length == 0) { return; } result.forEach(function (app) { $scope.createdApps.push(app); }); }) } function getUserFavorites() { var size = 11; FavoriteService.findFavorites($scope.userId, '', $scope.favoritesPage, size) .then(function (result) { $scope.favoritesPage += 1; $scope.hasMoreFavorites = result.length == size; if ($scope.favoritesPage == 1) { $("#app-list").removeClass("hidden"); } if (!result || result.length == 0) { return; } var appIds = []; result.forEach(function (favorite) { appIds.push(favorite.appId); }); AppService.find_apps(appIds.join(",")) .then(function (apps) { //sort var appIdMapApp = {}; apps.forEach(function (app) { appIdMapApp[app.appId] = app; }); result.forEach(function (favorite) { var app = appIdMapApp[favorite.appId]; if (!app) { return; } app.favoriteId = favorite.id; $scope.favorites.push(app); }); }); }) } function getPublicNamespaces() { NamespaceService.find_public_namespaces() .then(function (result) { $scope.allPublicNamespaces = result; morePublicNamespace(); var selectResult = []; angular.forEach(result,function (app) { selectResult.push({ id: app.appId, text: app.appId + ' / ' + app.name }) }); $('#public-name-spaces-search-list').select2({ data: selectResult, }); $('#public-name-spaces-search-list').on('select2:select', function () { var selected = $('#public-name-spaces-search-list').select2('data'); if (selected && selected.length) { goToAppHomePage(selected[0].id) } }); }) } function initUserVisitedApps() { var VISITED_APPS_STORAGE_KEY = "VisitedAppsV2"; var visitedAppsObject = JSON.parse(localStorage.getItem(VISITED_APPS_STORAGE_KEY)); if (!visitedAppsObject) { visitedAppsObject = {}; } var userVisitedApps = visitedAppsObject[$scope.userId]; if (userVisitedApps && userVisitedApps.length > 0) { AppService.find_apps(userVisitedApps.join(",")) .then(function (apps) { //sort var appIdMapApp = {}; apps.forEach(function (app) { appIdMapApp[app.appId] = app; }); userVisitedApps.forEach(function (appId) { var app = appIdMapApp[appId]; if (app) { $scope.visitedApps.push(app); } }); }); } } function goToCreateAppPage() { $window.location.href = AppUtil.prefixPath() + "/app.html"; } function goToAppHomePage(appId) { $window.location.href = AppUtil.prefixPath() + "/config.html?#/appid=" + appId; } function toggleOperationBtn(app) { app.showOperationBtn = !app.showOperationBtn; } function toTop(favoriteId) { FavoriteService.toTop(favoriteId).then(function () { toastr.success($translate.instant('Index.Topped')); refreshFavorites(); }) } function deleteFavorite(favoriteId) { FavoriteService.deleteFavorite(favoriteId).then(function () { toastr.success($translate.instant('Index.CancelledFavorite')); refreshFavorites(); }) } function refreshFavorites() { $scope.favoritesPage = 0; $scope.favorites = []; $scope.hasMoreFavorites = true; getUserFavorites(); } function morePublicNamespace() { var rest = $scope.allPublicNamespaces.length - $scope.publicNamespacePage * 10; if (rest <= 10) { for (var i = 0; i < rest; i++) { $scope.publicNamespaces.push($scope.allPublicNamespaces[$scope.publicNamespacePage * 10 + i]) } $scope.hasMorePublicNamespaces = false; } else { for (var j = 0; j < 10; j++) { $scope.publicNamespaces.push($scope.allPublicNamespaces[$scope.publicNamespacePage * 10 + j]) } $scope.hasMorePublicNamespaces = true; } $scope.publicNamespacePage += 1; } function changeContent(contentIndex) { $scope.whichContent = contentIndex; } } ================================================ FILE: apollo-portal/src/main/resources/static/scripts/controller/LoginController.js ================================================ /* * Copyright 2024 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ login_module.controller('LoginController', ['$scope', '$window', '$location', '$translate', 'toastr', 'AppUtil', LoginController]); function LoginController($scope, $window, $location, $translate, toastr, AppUtil) { if ($location.$$url) { var params = AppUtil.parseParams($location.$$url); if (params.error) { $translate('Login.UserNameOrPasswordIncorrect').then(function(result) { $scope.info = result; }); } if (params.logout) { $translate('Login.LogoutSuccessfully').then(function(result) { $scope.info = result; }); } } } ================================================ FILE: apollo-portal/src/main/resources/static/scripts/controller/ManageClusterController.js ================================================ /* * Copyright 2024 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ manage_cluster_module.controller('ManageClusterController', ['$scope', '$location', '$window', '$translate', 'toastr', 'AppService', 'EnvService', 'ClusterService', 'AppUtil', function ($scope, $location, $window, $translate, toastr, AppService, EnvService, ClusterService, AppUtil) { var params = AppUtil.parseParams($location.$$url); $scope.appId = params.appid; $scope.envs = []; function loadClusters() { AppService.load_nav_tree($scope.appId).then(function (result) { var nodes = AppUtil.collectData(result); if (!nodes || nodes.length == 0) { toastr.error($translate.instant('Config.SystemError')); return; } nodes.forEach(function (node) { $scope.envs.push({ name: node.env, clusters: node.clusters }); }); console.log($scope.envs); }); } loadClusters(); } ] ); ================================================ FILE: apollo-portal/src/main/resources/static/scripts/controller/NamespaceController.js ================================================ /* * Copyright 2024 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ namespace_module.controller("LinkNamespaceController", ['$scope', '$location', '$window', '$translate', 'toastr', 'AppService', 'AppUtil', 'NamespaceService', 'PermissionService', 'CommonService', function ($scope, $location, $window, $translate, toastr, AppService, AppUtil, NamespaceService, PermissionService, CommonService) { var params = AppUtil.parseParams($location.$$url); $scope.appId = params.appid; $scope.type = 'create'; $scope.step = 1; $scope.submitBtnDisabled = false; $scope.appendNamespacePrefix = true; PermissionService.has_root_permission().then(function (result) { $scope.hasRootPermission = result.hasPermission; }); CommonService.getPageSetting().then(function (setting) { $scope.pageSetting = setting; }); NamespaceService.findPublicNamespaceNames().then(function (result) { var publicNamespaces = []; result.forEach(function (item) { var namespace = {}; namespace.id = item; namespace.text = item; publicNamespaces.push(namespace); }); $('#namespaces').select2({ placeholder: $translate.instant('Namespace.PleaseChooseNamespace'), width: '100%', data: publicNamespaces }); $(".apollo-container").removeClass("hidden"); }, function (result) { toastr.error(AppUtil.errorMsg(result), $translate.instant('Namespace.LoadingPublicNamespaceError')); }); AppService.load($scope.appId).then(function (result) { $scope.appBaseInfo = result; $scope.appBaseInfo.namespacePrefix = result.orgId + '.'; }, function (result) { toastr.error(AppUtil.errorMsg(result), $translate.instant('Namespace.LoadingAppInfoError')); }); $scope.appNamespace = { appId: $scope.appId, name: '', comment: '', isPublic: false, format: 'properties' }; $scope.switchNSType = function (type) { $scope.appNamespace.isPublic = type; }; $scope.concatNamespace = function () { if (!$scope.appBaseInfo) { return ''; } var appNamespaceName = $scope.appNamespace.name ? $scope.appNamespace.name : ''; if (shouldAppendNamespacePrefix()) { return $scope.appBaseInfo.namespacePrefix + appNamespaceName; } return appNamespaceName; }; function shouldAppendNamespacePrefix() { return $scope.appNamespace.isPublic ? $scope.appendNamespacePrefix : false; } var selectedClusters = []; $scope.collectSelectedClusters = function (data) { selectedClusters = data; }; $scope.createNamespace = function () { if ($scope.type === 'link') { if (selectedClusters.length === 0) { toastr.warning($translate.instant('Namespace.PleaseChooseCluster')); return; } if ($scope.namespaceType === 1) { var selectedNamespaceNames = $('#namespaces').select2('data'); var ids = [] selectedNamespaceNames.forEach(function (namespace) { ids.push(namespace.id) }) if (ids.length === 0) { toastr.warning($translate.instant('Namespace.PleaseChooseNamespace')); return; } $scope.namespaceNames = ids; } var namespaceCreationModels = []; selectedClusters.forEach(function (cluster) { $scope.namespaceNames.forEach(function (namespace) { namespaceCreationModels.push({ env: cluster.env, namespace: { appId: $scope.appId, clusterName: cluster.clusterName, namespaceName: namespace } }); }) }); $scope.submitBtnDisabled = true; NamespaceService.createNamespace($scope.appId, namespaceCreationModels) .then(function (result) { toastr.success($translate.instant('Common.Created')); $scope.step = 2; setInterval(function () { $scope.submitBtnDisabled = false; if ($scope.namespaceNames.length === 1) { $window.location.href = AppUtil.prefixPath() + '/namespace/role.html?#appid=' + $scope.appId + "&namespaceName=" + $scope.namespaceNames[0]; } else { $window.location.href = AppUtil.prefixPath() + '/config.html?#/appid=' + $scope.appId; } }, 1000); }, function (result) { $scope.submitBtnDisabled = false; toastr.error(AppUtil.errorMsg(result)); }); } else { var namespaceNameLength = $scope.concatNamespace().length; if (namespaceNameLength > 32) { var errorTip = $translate.instant('Namespace.CheckNamespaceNameLengthTip', { departmentLength: namespaceNameLength - $scope.appNamespace.name.length, namespaceLength: $scope.appNamespace.name.length }); toastr.error(errorTip); return; } $scope.submitBtnDisabled = true; //only append namespace prefix for public app namespace var appendNamespacePrefix = shouldAppendNamespacePrefix(); NamespaceService.createAppNamespace($scope.appId, $scope.appNamespace, appendNamespacePrefix).then( function (result) { $scope.step = 2; setTimeout(function () { $scope.submitBtnDisabled = false; $window.location.href = AppUtil.prefixPath() + "/namespace/role.html?#/appid=" + $scope.appId + "&namespaceName=" + result.name; }, 1000); }, function (result) { $scope.submitBtnDisabled = false; toastr.error(AppUtil.errorMsg(result), $translate.instant('Common.CreateFailed')); }); } }; $scope.namespaceType = 1; $scope.selectNamespaceType = function (type) { $scope.namespaceType = type; }; $scope.back = function () { $window.location.href = AppUtil.prefixPath() + '/config.html?#appid=' + $scope.appId; }; $scope.switchType = function (type) { $scope.type = type; }; }]); ================================================ FILE: apollo-portal/src/main/resources/static/scripts/controller/ServerConfigController.js ================================================ /* * Copyright 2024 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ server_config_manage_module.controller('ServerConfigController', ['$scope', '$window', '$translate', 'toastr', 'AppUtil', 'ServerConfigService', 'PermissionService', 'EnvService', ServerConfigController]); function ServerConfigController($scope, $window, $translate, toastr, AppUtil, ServerConfigService, PermissionService, EnvService) { $scope.serverConfig = {}; $scope.portalDBConfigs = []; $scope.portalDBFilterConfigs = []; $scope.configDBConfigs = []; $scope.configDBFilterConfigs = []; $scope.envs = []; $scope.selectedEnv = ''; $scope.displayModule = 'home'; $scope.portalDBConfigSearchKey = ''; $scope.configDBConfigSearchKey = ''; $scope.configEdit = configEdit; $scope.createPortalDBConfig = createPortalDBConfig; $scope.createConfigDBConfig = createConfigDBConfig; $scope.gobackPortalDBTabs = gobackPortalDBTabs; $scope.gobackConfigDBTabs = gobackConfigDBTabs; $scope.portalDBConfigFilter = portalDBConfigFilter; $scope.configDBConfigFilter = configDBConfigFilter; $scope.resetPortalDBConfigSearchKey = resetPortalDBConfigSearchKey; $scope.resetConfigDBConfigSearchKey = resetConfigDBConfigSearchKey; $scope.switchConfigDBEnvs = switchConfigDBEnvs; $scope.allowSwitchingTabs = true; init(); function init() { initPermission(); getPortalDBConfig(); initEnv(); } function initEnv() { EnvService.find_all_envs().then(function (result) { $scope.envs = result; $scope.selectedEnv = result[0]; getConfigDBConfig(); }); } function initPermission() { PermissionService.has_root_permission() .then(function (result) { $scope.isRootUser = result.hasPermission; }); } function getPortalDBConfig() { ServerConfigService.findPortalDBConfig() .then(function (result) { $scope.portalDBConfigs = []; $scope.portalDBFilterConfigs = []; result.forEach(function (user) { $scope.portalDBConfigs.push(user); $scope.portalDBFilterConfigs.push(user); }); },function (result) { $scope.portalDBConfigs = []; $scope.portalDBFilterConfigs = []; toastr.error(AppUtil.errorMsg(result), $translate.instant('Config.SystemError')); }); } function getConfigDBConfig() { ServerConfigService.findConfigDBConfig($scope.selectedEnv) .then(function (result) { $scope.configDBConfigs = []; $scope.configDBFilterConfigs = []; result.forEach(function (user) { $scope.configDBConfigs.push(user); $scope.configDBFilterConfigs.push(user); }); },function (result) { $scope.configDBConfigs = []; $scope.configDBFilterConfigs = []; toastr.error(AppUtil.errorMsg(result), $translate.instant('Config.SystemError')); }); } function configEdit (displayModule,config) { $scope.displayModule = displayModule; $scope.allowSwitchingTabs = false; $scope.serverConfig = {}; if (config != null) { $scope.serverConfig = { key: config.key, value: config.value, comment: config.comment }; } } function switchConfigDBEnvs(env) { $scope.selectedEnv = env; getConfigDBConfig(); } function createPortalDBConfig() { ServerConfigService.createPortalDBConfig($scope.serverConfig).then(function (result) { toastr.success($translate.instant('ServiceConfig.Saved')); getPortalDBConfig(); $scope.displayModule = 'home'; $scope.allowSwitchingTabs = true; }, function (result) { toastr.error(AppUtil.errorMsg(result), $translate.instant('ServiceConfig.SaveFailed')); }); } function createConfigDBConfig() { ServerConfigService.createConfigDBConfig($scope.selectedEnv, $scope.serverConfig).then(function (result) { toastr.success($translate.instant('ServiceConfig.Saved')); getConfigDBConfig(); $scope.displayModule = 'home'; $scope.allowSwitchingTabs = true; }, function (result) { toastr.error(AppUtil.errorMsg(result), $translate.instant('ServiceConfig.SaveFailed')); }); } function gobackPortalDBTabs(){ $scope.displayModule = 'home'; $scope.allowSwitchingTabs = true; getPortalDBConfig(); } function gobackConfigDBTabs(){ $scope.displayModule = 'home'; $scope.allowSwitchingTabs = true; getConfigDBConfig(); } function portalDBConfigFilter() { $scope.portalDBConfigSearchKey = $scope.portalDBConfigSearchKey.toLowerCase(); let filterConfig = []; $scope.portalDBConfigs.forEach(function (item) { let keyName = item.key; if (keyName && keyName.toLowerCase().indexOf( $scope.portalDBConfigSearchKey) >= 0) { filterConfig.push(item); } }); $scope.portalDBFilterConfigs = filterConfig; } function resetPortalDBConfigSearchKey() { $scope.portalDBConfigSearchKey = ''; portalDBConfigFilter(); } function configDBConfigFilter() { $scope.configDBConfigSearchKey = $scope.configDBConfigSearchKey.toLowerCase(); let filterConfig = []; $scope.configDBConfigs.forEach(function (item) { let keyName = item.key; if (keyName && keyName.toLowerCase().indexOf( $scope.configDBConfigSearchKey) >= 0) { filterConfig.push(item); } }); $scope.configDBFilterConfigs = filterConfig; } function resetConfigDBConfigSearchKey() { $scope.configDBConfigSearchKey = ''; configDBConfigFilter(); } } ================================================ FILE: apollo-portal/src/main/resources/static/scripts/controller/SettingController.js ================================================ /* * Copyright 2024 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ setting_module.controller('SettingController', ['$scope', '$location', '$translate', 'toastr', 'AppService', 'AppUtil', 'PermissionService', 'OrganizationService', SettingController]); function SettingController($scope, $location, $translate, toastr, AppService, AppUtil, PermissionService, OrganizationService) { var params = AppUtil.parseParams($location.$$url); var $orgWidget = $('#organization'); $scope.pageContext = { appId: params.appid }; $scope.display = { app: { edit: false } }; $scope.submitBtnDisabled = false; $scope.userSelectWidgetId = 'toAssignMasterRoleUser'; $scope.assignMasterRoleToUser = assignMasterRoleToUser; $scope.removeMasterRoleFromUser = removeMasterRoleFromUser; $scope.toggleEditStatus = toggleEditStatus; $scope.updateAppInfo = updateAppInfo; init(); function init() { initOrganization(); initPermission(); initAdmins(); } function initOrganization() { OrganizationService.find_organizations().then(function (result) { var organizations = []; result.forEach(function (item) { var org = {}; org.id = item.orgId; org.text = item.orgName + '(' + item.orgId + ')'; org.name = item.orgName; organizations.push(org); }); $orgWidget.select2({ placeholder: $translate.instant('Common.PleaseChooseDepartment'), width: '100%', data: organizations }); initApplication(); }, function (result) { toastr.error(AppUtil.errorMsg(result), "load organizations error"); }); } function initPermission() { PermissionService.has_assign_user_permission($scope.pageContext.appId) .then(function (result) { $scope.hasAssignUserPermission = result.hasPermission; PermissionService.has_open_manage_app_master_role_limit().then(function (value) { if (!value.isManageAppMasterPermissionEnabled) { $scope.hasManageAppMasterPermission = $scope.hasAssignUserPermission; return; } PermissionService.has_manage_app_master_permission($scope.pageContext.appId).then(function (res) { $scope.hasManageAppMasterPermission = res.hasPermission && $scope.hasAssignUserPermission; PermissionService.has_root_permission().then(function (value) { $scope.hasManageAppMasterPermission = value.hasPermission || $scope.hasManageAppMasterPermission; }); }); }); }); } function initAdmins() { PermissionService.get_app_role_users($scope.pageContext.appId) .then(function (result) { $scope.appRoleUsers = result; $scope.admins = []; $scope.appRoleUsers.masterUsers.forEach(function (user) { $scope.admins.push(_.escape(user.userId)); }); }); } function initApplication() { AppService.load($scope.pageContext.appId).then(function (app) { $scope.app = app; $scope.viewApp = _.clone(app); initAppForm(app); $('.project-setting .panel').removeClass('hidden'); }) } function initAppForm(app) { $orgWidget.val(app.orgId).trigger("change"); var $ownerSelector = $('.ownerSelector'); var defaultSelectedDOM = ''; $ownerSelector.append(defaultSelectedDOM); $ownerSelector.trigger('change'); } function assignMasterRoleToUser() { var user = $('.' + $scope.userSelectWidgetId).select2('data')[0]; if (!user) { toastr.warning($translate.instant('App.Setting.PleaseChooseUser')); return; } var toAssignMasterRoleUser = user.id; $scope.submitBtnDisabled = true; PermissionService.assign_master_role($scope.pageContext.appId, toAssignMasterRoleUser) .then(function (result) { $scope.submitBtnDisabled = false; toastr.success($translate.instant('App.Setting.Added')); $scope.appRoleUsers.masterUsers.push({ userId: toAssignMasterRoleUser }); $('.' + $scope.userSelectWidgetId).select2("val", ""); }, function (result) { $scope.submitBtnDisabled = false; toastr.error(AppUtil.errorMsg(result), $translate.instant('App.Setting.AddFailed')); }); } function removeMasterRoleFromUser(user) { if ($scope.appRoleUsers.masterUsers.length <= 1) { $('#warning').modal('show'); return; } PermissionService.remove_master_role($scope.pageContext.appId, user) .then(function (result) { toastr.success($translate.instant('App.Setting.Deleted')); removeUserFromList($scope.appRoleUsers.masterUsers, user); }, function (result) { toastr.error(AppUtil.errorMsg(result), $translate.instant('App.Setting.DeleteFailed')); }); } function removeUserFromList(list, user) { var index = 0; for (var i = 0; i < list.length; i++) { if (list[i].userId == user) { index = i; break; } } list.splice(index, 1); } function toggleEditStatus() { if ($scope.display.app.edit) {//cancel edit $scope.viewApp = _.clone($scope.app); initAppForm($scope.viewApp); } else {//edit } $scope.display.app.edit = !$scope.display.app.edit; } function updateAppInfo() { $scope.submitBtnDisabled = true; var app = $scope.viewApp; var selectedOrg = $orgWidget.select2('data')[0]; if (!selectedOrg.id) { toastr.warning($translate.instant('Common.PleaseChooseDepartment')); return; } app.orgId = selectedOrg.id; app.orgName = selectedOrg.name; // owner var owner = $('.ownerSelector').select2('data')[0]; if (!owner) { toastr.warning($translate.instant('Common.PleaseChooseOwner')); return; } app.ownerName = owner.id; AppService.update(app).then(function (app) { toastr.success($translate.instant('App.Setting.Modified')); initApplication(); $scope.display.app.edit = false; $scope.submitBtnDisabled = false; }, function (result) { AppUtil.showErrorMsg(result); $scope.submitBtnDisabled = false; }) } } ================================================ FILE: apollo-portal/src/main/resources/static/scripts/controller/SystemInfoController.js ================================================ /* * Copyright 2024 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ system_info_module.controller('SystemInfoController', ['$scope', 'toastr', 'AppUtil', 'AppService', 'ClusterService', 'NamespaceService', 'PermissionService', 'SystemInfoService', SystemInfoController]); function SystemInfoController($scope, toastr, AppUtil, AppService, ClusterService, NamespaceService, PermissionService, SystemInfoService) { $scope.systemInfo = {}; $scope.check = check; initPermission(); function initPermission() { PermissionService.has_root_permission() .then(function (result) { $scope.isRootUser = result.hasPermission; if (result.hasPermission) { loadSystemInfo(); } }) } function loadSystemInfo() { SystemInfoService.load_system_info().then(function (result) { $scope.systemInfo = result; }, function (result) { AppUtil.showErrorMsg(result); }); } function check(instanceId, host) { SystemInfoService.check_health(instanceId, host).then(function (result) { var status = result.status.code; if (status === 'UP') { toastr.success(host + ' is healthy!'); } else { toastr.error(host + ' is not healthy, please check ' + host + '/health for more information!'); } }, function (result) { AppUtil.showErrorMsg(result); }); } } ================================================ FILE: apollo-portal/src/main/resources/static/scripts/controller/UserController.js ================================================ /* * Copyright 2024 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ user_module.controller('UserController', ['$scope', '$window', '$translate', 'toastr', 'AppUtil', 'UserService', 'PermissionService', UserController]); function UserController($scope, $window, $translate, toastr, AppUtil, UserService, PermissionService) { $scope.user = {}; $scope.createdUsers = []; $scope.filterUser = []; $scope.status = '1' $scope.searchKey = '' $scope.changeStatus = changeStatus $scope.searchUsers = searchUsers $scope.resetSearchUser = resetSearchUser $scope.validatePwdMatch = validatePwdMatch initPermission(); function initPermission() { PermissionService.has_root_permission() .then(function (result) { $scope.isRootUser = result.hasPermission; getCreatedUsers(); }) } function getCreatedUsers() { if ($scope.isRootUser) { UserService.find_users("",true) .then(function (result) { if (!result || result.length === 0) { return; } $scope.createdUsers = []; $scope.filterUser = []; result.forEach(function (user) { $scope.createdUsers.push(user); $scope.filterUser.push(user); }); }); } else { UserService.load_user() .then(function (result) { if (!result) { return; } $scope.createdUsers = [result]; $scope.filterUser = [result]; }); } } function changeStatus(status, user){ $scope.status = status $scope.user = {} if (user != null) { $scope.user = { username: user.userId, userDisplayName: user.name, email: user.email, enabled: user.enabled, } } } function searchUsers() { $scope.searchKey = $scope.searchKey.toLowerCase(); var filterUser = [] $scope.createdUsers.forEach(function (item) { var userLoginName = item.userId; if (userLoginName && userLoginName.toLowerCase().indexOf( $scope.searchKey) >= 0) { filterUser.push(item); } }); $scope.filterUser = filterUser } function resetSearchUser() { $scope.searchKey = '' searchUsers() } function validatePwdMatch() { $scope.pwdNotMatch = false; if ($scope.user.password && $scope.user.password != $scope.user.confirmPassword) { $scope.pwdNotMatch = true; } } $scope.changeUserEnabled = function (user) { var newUser={} if (user != null) { newUser = { username: user.userId, userDisplayName: user.name, email: user.email, enabled: user.enabled === 1 ? 0 : 1, } } UserService.change_user_enabled(newUser).then(function (result) { toastr.success($translate.instant('UserMange.Enabled.succeed')); getCreatedUsers() }, function (result) { AppUtil.showErrorMsg(result, $translate.instant('UserMange.Enabled.failure')); }) } $scope.createOrUpdateUser = function () { validatePwdMatch(); if ($scope.pwdNotMatch) { return; } if ($scope.status === '2') { UserService.createOrUpdateUser(true, $scope.user).then(function (result) { toastr.success($translate.instant('UserMange.Created')); getCreatedUsers() changeStatus('1') }, function (result) { AppUtil.showErrorMsg(result, $translate.instant('UserMange.CreateFailed')); }) } else { UserService.createOrUpdateUser(false,$scope.user).then(function (result) { toastr.success($translate.instant('UserMange.Edited')); getCreatedUsers() changeStatus('1') }, function (result) { AppUtil.showErrorMsg(result, $translate.instant('UserMange.EditFailed')); }) } } } ================================================ FILE: apollo-portal/src/main/resources/static/scripts/controller/config/ConfigBaseInfoController.js ================================================ /* * Copyright 2024 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ application_module.controller("ConfigBaseInfoController", ['$rootScope', '$scope', '$window', '$location', '$translate', 'toastr', 'EventManager', 'UserService', 'AppService', 'FavoriteService', 'PermissionService', 'AppUtil', ConfigBaseInfoController]); function ConfigBaseInfoController($rootScope, $scope, $window, $location, $translate, toastr, EventManager, UserService, AppService, FavoriteService, PermissionService, AppUtil) { var urlParams = AppUtil.parseParams($location.$$url); var appId = urlParams.appid; if (!appId) { $window.location.href = AppUtil.prefixPath() + '/index.html'; return; } initPage(); function initPage() { $rootScope.hideTip = JSON.parse(localStorage.getItem("hideTip")); //load session storage to recovery scene var scene = JSON.parse(sessionStorage.getItem(appId)); $rootScope.pageContext = { appId: appId, env: urlParams.env ? urlParams.env : (scene ? scene.env : ''), clusterName: urlParams.cluster ? urlParams.cluster : (scene ? scene.cluster : 'default'), namespaceName: urlParams.namespace, item: urlParams.item, }; //storage page context to session storage sessionStorage.setItem( $rootScope.pageContext.appId, JSON.stringify({ env: $rootScope.pageContext.env, cluster: $rootScope.pageContext.clusterName })); UserService.load_user().then(function (result) { $rootScope.pageContext.userId = result.userId; loadAppInfo(); handleFavorite(); }, function (result) { toastr.error(AppUtil.errorMsg(result), $translate.instant('Config.GetUserInfoFailed')); }); handlePermission(); } function loadAppInfo() { $scope.notFoundApp = true; AppService.load($rootScope.pageContext.appId).then(function (result) { $scope.notFoundApp = false; $scope.appBaseInfo = result; $scope.appBaseInfo.orgInfo = result.orgName + '(' + result.orgId + ')'; $scope.appBaseInfo.ownerInfo = result.ownerDisplayName + '(' + result.ownerName + ')'; loadNavTree(); recordVisitApp(); findMissEnvs(); $(".J_appFound").removeClass("hidden"); }, function (result) { $(".J_appNotFound").removeClass("hidden"); }); } $scope.createAppInMissEnv = function () { var count = 0; $scope.missEnvs.forEach(function (env) { AppService.create_remote(env, $scope.appBaseInfo).then(function (result) { toastr.success(env, $translate.instant('Common.Created')); count++; if (count == $scope.missEnvs.length) { location.reload(true); } }, function (result) { toastr.error(AppUtil.errorMsg(result), `${$translate.instant('Common.CreateFailed')}:${env}`); count++; if (count == $scope.missEnvs.length) { location.reload(true); } }); }); }; $scope.createMissingNamespaces = function () { AppService.create_missing_namespaces($rootScope.pageContext.appId, $rootScope.pageContext.env, $rootScope.pageContext.clusterName).then(function (result) { toastr.success($translate.instant('Common.Created')); location.reload(true); }, function (result) { toastr.error(AppUtil.errorMsg(result), $translate.instant('Common.CreateFailed')); } ); }; function findMissEnvs() { $scope.missEnvs = []; AppService.find_miss_envs($rootScope.pageContext.appId).then(function (result) { $scope.missEnvs = AppUtil.collectData(result); if ($scope.missEnvs.length > 0) { toastr.warning($translate.instant('Config.ProjectMissEnvInfos')); } $scope.findMissingNamespaces(); }); } EventManager.subscribe(EventManager.EventType.CHANGE_ENV_CLUSTER, function () { $scope.findMissingNamespaces(); }); $scope.findMissingNamespaces = function () { $scope.missingNamespaces = []; // only check missing private namespaces when app exists in current env if ($rootScope.pageContext.env && $scope.missEnvs.indexOf($rootScope.pageContext.env) === -1) { AppService.find_missing_namespaces($rootScope.pageContext.appId, $rootScope.pageContext.env, $rootScope.pageContext.clusterName).then(function (result) { $scope.missingNamespaces = AppUtil.collectData(result); if ($scope.missingNamespaces.length > 0) { toastr.warning($translate.instant('Config.ProjectMissNamespaceInfos')); } }); } }; function recordVisitApp() { //save user recent visited apps var VISITED_APPS_STORAGE_KEY = "VisitedAppsV2"; var visitedAppsObject = JSON.parse(localStorage.getItem(VISITED_APPS_STORAGE_KEY)); var hasSaved = false; if (!visitedAppsObject) { visitedAppsObject = {}; } if (!visitedAppsObject[$rootScope.pageContext.userId]) { visitedAppsObject[$rootScope.pageContext.userId] = []; } var visitedApps = visitedAppsObject[$rootScope.pageContext.userId]; if (visitedApps && visitedApps.length > 0) { visitedApps.forEach(function (app) { if (app == appId) { hasSaved = true; return; } }); } var currentUserVisitedApps = visitedAppsObject[$rootScope.pageContext.userId]; if (!hasSaved) { //if queue's length bigger than 6 will remove oldest app if (currentUserVisitedApps.length >= 6) { currentUserVisitedApps.splice(0, 1); } currentUserVisitedApps.push($rootScope.pageContext.appId); localStorage.setItem(VISITED_APPS_STORAGE_KEY, JSON.stringify(visitedAppsObject)); } } function loadNavTree() { AppService.load_nav_tree($rootScope.pageContext.appId).then(function (result) { var navTree = []; var nodes = AppUtil.collectData(result); if (!nodes || nodes.length == 0) { toastr.error($translate.instant('Config.SystemError')); return; } //default first env if session storage is empty if (!$rootScope.pageContext.env) { $rootScope.pageContext.env = nodes[0].env; } EventManager.emit(EventManager.EventType.REFRESH_NAMESPACE, {firstLoad: true}); nodes.forEach(function (env) { if (!env.clusters || env.clusters.length == 0) { return; } var node = {}; node.text = env.env; var clusterNodes = []; //如果env下面只有一个default集群则不显示集群列表 if (env.clusters && env.clusters.length == 1 && env.clusters[0].name == 'default') { if ($rootScope.pageContext.env == env.env) { node.state = {}; node.state.selected = true; } node.selectable = true; } else { node.selectable = false; //cluster list env.clusters.forEach(function (cluster) { var clusterNode = {}, parentNode = []; //default selection from session storage or first env & first cluster if ($rootScope.pageContext.env == env.env && $rootScope.pageContext.clusterName == cluster.name) { clusterNode.state = {}; clusterNode.state.selected = true; } clusterNode.text = cluster.name; parentNode.push(node.text); clusterNode.tags = [$translate.instant('Common.Cluster')]; clusterNode.parentNode = parentNode; clusterNode.comment = cluster.comment; clusterNodes.push(clusterNode); }); } node.nodes = clusterNodes; navTree.push(node); }); //init treeview $('#treeview').treeview({ color: "#797979", showBorder: true, data: navTree, levels: 99, expandIcon: '', collapseIcon: '', showTags: true, onNodeSelected: function (event, data) { if (!data.parentNode) {//first nav node $rootScope.pageContext.env = data.text; $rootScope.pageContext.clusterName = 'default'; } else {//second cluster node $rootScope.pageContext.env = data.parentNode[0]; $rootScope.pageContext.clusterName = data.text; } //storage scene sessionStorage.setItem( $rootScope.pageContext.appId, JSON.stringify({ env: $rootScope.pageContext.env, cluster: $rootScope.pageContext.clusterName })); $window.location.href = AppUtil.prefixPath() + "/config.html#/appid=" + $rootScope.pageContext.appId + "&env=" + $rootScope.pageContext.env + "&cluster=" + $rootScope.pageContext.clusterName; EventManager.emit(EventManager.EventType.REFRESH_NAMESPACE); EventManager.emit(EventManager.EventType.CHANGE_ENV_CLUSTER); $rootScope.showSideBar = false; } }); $('#treeview .node-treeview').hover( function () { // get comment by nodeid var commentValue = $('#treeview').treeview('getNode', $(this).data('nodeid')).comment; if (typeof commentValue !== 'undefined') { $(this).attr('title', commentValue); } }, function () { } ); var envMapClusters = {}; navTree.forEach(function (node) { if (node.nodes && node.nodes.length > 0) { var clusterNames = []; node.nodes.forEach(function (cluster) { if (cluster.text != 'default') { clusterNames.push(cluster.text); } }); envMapClusters[node.text] = clusterNames.join(","); } }); $rootScope.envMapClusters = envMapClusters; }, function (result) { toastr.error(AppUtil.errorMsg(result), $translate.instant('Config.SystemError')); }); } function handleFavorite() { FavoriteService.findFavorites($rootScope.pageContext.userId, $rootScope.pageContext.appId) .then(function (result) { if (result && result.length) { $scope.favoriteId = result[0].id; } }); $scope.addFavorite = function () { var favorite = { userId: $rootScope.pageContext.userId, appId: $rootScope.pageContext.appId }; FavoriteService.addFavorite(favorite) .then(function (result) { $scope.favoriteId = result.id; toastr.success($translate.instant('Config.FavoriteSuccessfully')); }, function (result) { toastr.error(AppUtil.errorMsg(result), $translate.instant('Config.FavoriteFailed')); }) }; $scope.deleteFavorite = function () { FavoriteService.deleteFavorite($scope.favoriteId) .then(function (result) { $scope.favoriteId = 0; toastr.success($translate.instant('Config.CancelledFavorite')); }, function (result) { toastr.error(AppUtil.errorMsg(result), $translate.instant('Config.CancelFavoriteFailed')); }) }; } function handlePermission() { //permission PermissionService.has_create_namespace_permission(appId).then(function (result) { $scope.hasCreateNamespacePermission = result.hasPermission; }, function (result) { }); PermissionService.has_create_cluster_permission(appId).then(function (result) { $scope.hasCreateClusterPermission = result.hasPermission; }, function (result) { }); PermissionService.has_assign_user_permission(appId).then(function (result) { $scope.hasAssignUserPermission = result.hasPermission; }, function (result) { }); $scope.showMasterPermissionTips = function () { $("#masterNoPermissionDialog").modal('show'); }; } var VIEW_MODE_SWITCH_WIDTH = 1156; if (window.innerWidth <= VIEW_MODE_SWITCH_WIDTH) { $rootScope.viewMode = 2; $rootScope.showSideBar = false; } else { $rootScope.viewMode = 1; } $rootScope.adaptScreenSize = function () { if (window.innerWidth <= VIEW_MODE_SWITCH_WIDTH) { $rootScope.viewMode = 2; } else { $rootScope.viewMode = 1; $rootScope.showSideBar = false; } }; $(window).resize(function () { $scope.$apply(function () { $rootScope.adaptScreenSize(); }); }); } ================================================ FILE: apollo-portal/src/main/resources/static/scripts/controller/config/ConfigNamespaceController.js ================================================ /* * Copyright 2024 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ application_module.controller("ConfigNamespaceController", ['$rootScope', '$scope', '$translate', 'toastr', 'AppUtil', 'EventManager', 'ConfigService', 'PermissionService', 'UserService', 'NamespaceBranchService', 'NamespaceService', controller]); function controller($rootScope, $scope, $translate, toastr, AppUtil, EventManager, ConfigService, PermissionService, UserService, NamespaceBranchService, NamespaceService) { $scope.rollback = rollback; $scope.preDeleteItem = preDeleteItem; $scope.deleteItem = deleteItem; $scope.editItem = editItem; $scope.createItem = createItem; $scope.preRevokeItem = preRevokeItem; $scope.revokeItem = revokeItem; $scope.closeTip = closeTip; $scope.showText = showText; $scope.createBranch = createBranch; $scope.preCreateBranch = preCreateBranch; $scope.preDeleteBranch = preDeleteBranch; $scope.deleteBranch = deleteBranch; $scope.showNoModifyPermissionDialog = showNoModifyPermissionDialog; $scope.lockCheck = lockCheck; $scope.emergencyPublish = emergencyPublish; init(); function init() { initRole(); initUser(); initPublishInfo(); } function initRole() { PermissionService.get_app_role_users($rootScope.pageContext.appId) .then(function (result) { var masterUsers = ''; result.masterUsers.forEach(function (user) { masterUsers += _.escape(user.userId) + ','; }); $scope.masterUsers = masterUsers.substring(0, masterUsers.length - 1); }, function (result) { }); } function initUser() { UserService.load_user().then(function (result) { $scope.currentUser = result.userId; }); } function initPublishInfo() { NamespaceService.getNamespacePublishInfo($rootScope.pageContext.appId) .then(function (result) { if (!result) { return; } $scope.hasNotPublishNamespace = false; var namespacePublishInfo = []; Object.keys(result).forEach(function (env) { if (env.indexOf("$") >= 0) { return; } var envPublishInfo = result[env]; Object.keys(envPublishInfo).forEach(function (cluster) { var clusterPublishInfo = envPublishInfo[cluster]; if (clusterPublishInfo) { $scope.hasNotPublishNamespace = true; if (Object.keys(envPublishInfo).length > 1) { namespacePublishInfo.push("[" + env + ", " + cluster + "]"); } else { namespacePublishInfo.push("[" + env + "]"); } } }) }); $scope.namespacePublishInfo = namespacePublishInfo; }); } EventManager.subscribe(EventManager.EventType.REFRESH_NAMESPACE, function (context) { if (context.namespace) { refreshSingleNamespace(context.namespace); } else { refreshAllNamespaces(context); } }); function refreshAllNamespaces(context) { if ($rootScope.pageContext.env == '') { return; } ConfigService.load_all_namespaces($rootScope.pageContext.appId, $rootScope.pageContext.env, $rootScope.pageContext.clusterName).then( function (result) { $scope.namespaces = result; $('.config-item-container').removeClass('hide'); initPublishInfo(); //If there is a namespace parameter in the URL, expand the corresponding namespace directly if (context && context.firstLoad && $rootScope.pageContext.namespaceName) { refreshSingleNamespace({ baseInfo: { namespaceName: $rootScope.pageContext.namespaceName }, searchInfo: { showSearchInput: true, searchItemKey: $rootScope.pageContext.item, } }); } }, function (result) { toastr.error(AppUtil.errorMsg(result), $translate.instant('Config.LoadingAllNamespaceError')); }); } function refreshSingleNamespace(namespace) { if ($rootScope.pageContext.env == '') { return; } const showSearchItemInput = namespace.searchInfo ? namespace.searchInfo.showSearchInput : false; const searchItemKey = namespace.searchInfo ? namespace.searchInfo.searchItemKey : ''; ConfigService.load_namespace($rootScope.pageContext.appId, $rootScope.pageContext.env, $rootScope.pageContext.clusterName, namespace.baseInfo.namespaceName).then( function (result) { $scope.namespaces.forEach(function (namespace, index) { if (namespace.baseInfo.namespaceName === result.baseInfo.namespaceName) { result.showNamespaceBody = true; result.initialized = true; result.show = namespace.show; $scope.namespaces[index] = result; result.showSearchItemInput = showSearchItemInput; result.searchItemKey = searchItemKey; } }); initPublishInfo(); }, function (result) { toastr.error(AppUtil.errorMsg(result), $translate.instant('Config.LoadingAllNamespaceError')); }); } function rollback() { EventManager.emit(EventManager.EventType.ROLLBACK_NAMESPACE); } $scope.tableViewOperType = '', $scope.item = {}; $scope.toOperationNamespace; var toDeleteItemId = 0; function preDeleteItem(namespace, item) { if (!lockCheck(namespace)) { return; } $scope.config = {}; $scope.config.key = _.escape(item.key); $scope.config.value = _.escape(item.value); $scope.toOperationNamespace = namespace; toDeleteItemId = item.id; $("#deleteConfirmDialog").modal("show"); } function deleteItem() { ConfigService.delete_item($rootScope.pageContext.appId, $rootScope.pageContext.env, $scope.toOperationNamespace.baseInfo.clusterName, $scope.toOperationNamespace.baseInfo.namespaceName, toDeleteItemId).then( function (result) { toastr.success($translate.instant('Config.Deleted')); EventManager.emit(EventManager.EventType.REFRESH_NAMESPACE, { namespace: $scope.toOperationNamespace }); }, function (result) { toastr.error(AppUtil.errorMsg(result), $translate.instant('Config.DeleteFailed')); }); } function preRevokeItem(namespace) { if (!lockCheck(namespace)) { return; } $scope.toOperationNamespace = namespace; toRevokeItemId = namespace.baseInfo.id; $("#revokeItemConfirmDialog").modal("show"); } function revokeItem() { ConfigService.revoke_item($rootScope.pageContext.appId, $rootScope.pageContext.env, $scope.toOperationNamespace.baseInfo.clusterName, $scope.toOperationNamespace.baseInfo.namespaceName).then( function (result) { toastr.success($translate.instant('Revoke.RevokeSuccessfully')); EventManager.emit(EventManager.EventType.REFRESH_NAMESPACE, { namespace: $scope.toOperationNamespace }); }, function (result) { toastr.error(AppUtil.errorMsg(result), $translate.instant('Revoke.RevokeFailed')); } ); } //修改配置 function editItem(namespace, toEditItem) { if (!lockCheck(namespace)) { return; } $scope.item = _.clone(toEditItem); $scope.item.type = String($scope.item.type || 0) if (namespace.isBranch || namespace.isLinkedNamespace) { var existedItem = false; namespace.items.forEach(function (item) { if (!item.isDeleted && item.item.key == toEditItem.key) { existedItem = true; } }); if (!existedItem) { $scope.item.lineNum = 0; $scope.item.tableViewOperType = 'create'; } else { $scope.item.tableViewOperType = 'update'; } } else { $scope.item.tableViewOperType = 'update'; } $scope.toOperationNamespace = namespace; AppUtil.showModal('#itemModal'); } //新增配置 function createItem(namespace) { if (!lockCheck(namespace)) { return; } $scope.item = { tableViewOperType: 'create' }; $scope.item.type = '0'; $scope.showNumberError = false; $scope.showJsonError = false; $scope.toOperationNamespace = namespace; AppUtil.showModal('#itemModal'); } var selectedClusters = []; $scope.collectSelectedClusters = function (data) { selectedClusters = data; }; function lockCheck(namespace) { if (namespace.lockOwner && $scope.currentUser != namespace.lockOwner) { $scope.lockOwner = namespace.lockOwner; $('#namespaceLockedDialog').modal('show'); return false; } return true; } function closeTip(clusterName) { var hideTip = JSON.parse(localStorage.getItem("hideTip")); if (!hideTip) { hideTip = {}; hideTip[$rootScope.pageContext.appId] = {}; } if (!hideTip[$rootScope.pageContext.appId]) { hideTip[$rootScope.pageContext.appId] = {}; } hideTip[$rootScope.pageContext.appId][clusterName] = true; $rootScope.hideTip = hideTip; localStorage.setItem("hideTip", JSON.stringify(hideTip)); } function showText(text) { $scope.text = text; $('#showTextModal').modal('show'); } function showNoModifyPermissionDialog() { $("#modifyNoPermissionDialog").modal('show'); } var toCreateBranchNamespace = {}; function preCreateBranch(namespace) { toCreateBranchNamespace = namespace; AppUtil.showModal("#createBranchTips"); } function createBranch() { NamespaceBranchService.createBranch($rootScope.pageContext.appId, $rootScope.pageContext.env, $rootScope.pageContext.clusterName, toCreateBranchNamespace.baseInfo.namespaceName) .then(function (result) { toastr.success($translate.instant('Config.GrayscaleCreated')); EventManager.emit(EventManager.EventType.REFRESH_NAMESPACE, { namespace: toCreateBranchNamespace }); }, function (result) { toastr.error(AppUtil.errorMsg(result), $translate.instant('Config.GrayscaleCreateFailed')); }) } function preDeleteBranch(branch) { //normal delete branch.branchStatus = 0; $scope.toDeleteBranch = branch; AppUtil.showModal('#deleteBranchDialog'); } function deleteBranch() { NamespaceBranchService.deleteBranch($rootScope.pageContext.appId, $rootScope.pageContext.env, $rootScope.pageContext.clusterName, $scope.toDeleteBranch.baseInfo.namespaceName, $scope.toDeleteBranch.baseInfo.clusterName ) .then(function (result) { toastr.success($translate.instant('Config.BranchDeleted')); EventManager.emit(EventManager.EventType.REFRESH_NAMESPACE, { namespace: $scope.toDeleteBranch.parentNamespace }); }, function (result) { toastr.error(AppUtil.errorMsg(result), $translate.instant('Config.BranchDeleteFailed')); }) } EventManager.subscribe(EventManager.EventType.EMERGENCY_PUBLISH, function (context) { AppUtil.showModal("#emergencyPublishAlertDialog"); $scope.emergencyPublishContext = context; }); function emergencyPublish() { if ($scope.emergencyPublishContext.mergeAndPublish) { EventManager.emit(EventManager.EventType.MERGE_AND_PUBLISH_NAMESPACE, { branch: $scope.emergencyPublishContext.namespace, isEmergencyPublish: true }); } else { EventManager.emit(EventManager.EventType.PUBLISH_NAMESPACE, { namespace: $scope.emergencyPublishContext.namespace, isEmergencyPublish: true }); } } EventManager.subscribe(EventManager.EventType.SYNTAX_CHECK_TEXT_FAILED, function (context) { $scope.syntaxCheckContext = context; AppUtil.showModal('#syntaxCheckFailedDialog'); }); new Clipboard('.clipboard'); } ================================================ FILE: apollo-portal/src/main/resources/static/scripts/controller/config/DiffConfigController.js ================================================ /* * Copyright 2024 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ diff_item_module.controller("DiffItemController", ['$scope', '$location', '$window', '$translate', 'toastr', 'AppService', 'AppUtil', 'ConfigService', function ($scope, $location, $window, $translate, toastr, AppService, AppUtil, ConfigService) { var params = AppUtil.parseParams($location.$$url); $scope.pageContext = { appId: params.appid, env: params.env, clusterName: params.clusterName, namespaceName: params.namespaceName }; var sourceItems = []; $scope.diff = diff; $scope.searchKey = '' $scope.syncBtnDisabled = false; $scope.showCommentDiff = false; $scope.onlyShowDiffKeys = true; $scope.collectSelectedClusters = collectSelectedClusters; $scope.syncItemNextStep = syncItemNextStep; $scope.backToAppHomePage = backToAppHomePage; $scope.switchSelect = switchSelect; $scope.showTextDiff = showTextDiff; $scope.itemsKeyedByKey = {}; $scope.allNamespaceValueEqualed = {}; $scope.isTableDiff = true; $scope.isPropertiesFormat = true; $scope.syncData = { syncToNamespaces: [], syncItems: [] }; function diff() { $scope.syncData = parseSyncSourceData(); if ($scope.syncData.syncToNamespaces.length < 2) { toastr.warning($translate.instant('Config.Diff.PleaseChooseTwoCluster')); return; } if (!$scope.isTableDiff && $scope.syncData.syncToNamespaces.length > 2) { toastr.warning($translate.instant('Config.Diff.TextDiffMostChooseTwoCluster')); return; } const namespaceCnt = $scope.syncData.syncToNamespaces.length; let loadedNamespaceCnt = 0; $scope.syncData.syncToNamespaces.forEach(function (namespace) { ConfigService.find_items(namespace.appId, namespace.env, namespace.clusterName, namespace.namespaceName).then(function (result) { loadedNamespaceCnt++; let suffix = '' if (namespace.namespaceName.includes('.')) { suffix = namespace.namespaceName.match(/[^.]+$/)[0]; $scope.isPropertiesFormat = false } let res = []; let propTextInfo = ""; let propTextArr = []; result.forEach(function (originItem) { if (originItem.key === "") { return } // prop if (suffix === '') { res.push(originItem) propTextArr.push(originItem) } else { namespace.originTextInfo = originItem.value if (suffix === 'yml') { res = Obj2Prop( YAML.parse(originItem.value)) } else if (suffix === 'json') { res = Obj2Prop( JSON.parse(originItem.value)) } else if (suffix === 'xml') { const x2js = new X2JS(); res = Obj2Prop( x2js.xml_str2json(originItem.value)) } else { //txt res.push(originItem) } } }); // For prop textDiff,need parse prop to text if (suffix === '') { // to align key propTextArr.sort((item1, item2) => item1.key.localeCompare(item2.key)); propTextArr.forEach(function (item) { if (item.key) { //use string \n to display as new line var itemValue = item.value.replace(/\n/g, "\\n"); propTextInfo += item.key + " = " + itemValue + "\n"; } else { propTextInfo += item.comment + "\n"; } }); namespace.originTextInfo = propTextInfo } res.forEach(function (item) { const itemsKeyedByClusterName = $scope.itemsKeyedByKey[item.key] || {}; itemsKeyedByClusterName[namespace.env + ':' + namespace.clusterName + ':' + namespace.namespaceName] = item; $scope.itemsKeyedByKey[item.key] = itemsKeyedByClusterName; }) //After loading all the compared namespaces, check whether the values are consistent //itemsKeyedByKey struct : itemKey => namespace => item if (loadedNamespaceCnt === namespaceCnt) { Object.keys($scope.itemsKeyedByKey).forEach( function (key) { let lastValue = null; let allEqualed = true; // some namespace lack key,determined as not allEqual if (Object.keys($scope.itemsKeyedByKey[key]).length !== namespaceCnt) { allEqualed = false; } else { // check key items allEqual Object.values($scope.itemsKeyedByKey[key]).forEach( function (item) { if (lastValue == null) { lastValue = item.value; } if (lastValue !== item.value) { allEqualed = false; } }) } $scope.allNamespaceValueEqualed[key] = allEqualed; }) } }); }); $scope.syncItemNextStep(1); } var selectedClusters = []; function collectSelectedClusters(data) { selectedClusters = data; } function parseSyncSourceData() { var syncData = { syncToNamespaces: [], syncItems: [], firstClusterKey: "", }; var namespaceName = $scope.pageContext.namespaceName; selectedClusters.forEach(function (cluster) { if (cluster.checked) { cluster.clusterName = cluster.name; cluster.namespaceName = namespaceName; cluster.compositedKey = cluster.env + ':' + cluster.clusterName + ':' + cluster.namespaceName; syncData.syncToNamespaces.push(cluster); } }); syncData.firstClusterKey = selectedClusters[0].compositedKey; return syncData; } ////// flow control /////// $scope.syncItemStep = 1; function syncItemNextStep(offset) { $scope.syncItemStep += offset; } function backToAppHomePage() { $window.location.href = AppUtil.prefixPath() + '/config.html?#appid=' + $scope.pageContext.appId; } function switchSelect(o) { o.checked = !o.checked; } function showTextDiff(oldStr, newStr) { $scope.oldStr = oldStr; $scope.newStr = newStr; AppUtil.showModal('#showTextModal'); } }]); // transfer js obj to properties function Obj2Prop(obj,prefix){ let result = [] const keys = Object.keys(obj) keys.forEach(function (key){ let keyPrefix; if(obj[key] && typeof obj[key]=='object'){ const currentPrefix = key.concat('.'); keyPrefix = prefix? prefix.concat(currentPrefix) : currentPrefix result = result.concat(Obj2Prop(obj[key],keyPrefix)) }else{ keyPrefix = prefix? prefix.concat(key):key result.push({ key:keyPrefix, value:(obj[key] || '') }) } }) return result } ================================================ FILE: apollo-portal/src/main/resources/static/scripts/controller/config/ReleaseHistoryController.js ================================================ /* * Copyright 2024 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ release_history_module.controller("ReleaseHistoryController", ['$scope', '$location', '$translate', 'AppUtil', 'EventManager', 'ReleaseService', 'ConfigService', 'PermissionService', 'ReleaseHistoryService', releaseHistoryController ]); function releaseHistoryController($scope, $location, $translate, AppUtil, EventManager, ReleaseService, ConfigService, PermissionService, ReleaseHistoryService) { var params = AppUtil.parseParams($location.$$url); $scope.pageContext = { appId: params.appid, env: params.env, clusterName: params.clusterName, namespaceName: params.namespaceName, releaseId: params.releaseId, releaseHistoryId: params.releaseHistoryId }; var PAGE_SIZE = 10; var CONFIG_VIEW_TYPE = { DIFF: 'diff', ALL: 'all' }; var selectedReleaseId = -1; $scope.namespace = {}; $scope.page = 0; $scope.releaseHistories = []; $scope.hasLoadAll = false; $scope.selectedReleaseHistory = 0; $scope.isTextNamespace = false; // whether current user can view config $scope.isConfigHidden = false; $scope.showReleaseHistoryDetail = showReleaseHistoryDetail; $scope.rollback = rollback; $scope.preRollback = preRollback; $scope.switchConfigViewType = switchConfigViewType; $scope.findReleaseHistory = findReleaseHistory; $scope.showText = showText; $scope.showTextDiff = showTextDiff; EventManager.subscribe(EventManager.EventType.REFRESH_RELEASE_HISTORY, function () { location.reload(true); }); init(); function init() { findReleaseHistory(); loadNamespace(); } function preRollback() { EventManager.emit(EventManager.EventType.PRE_ROLLBACK_NAMESPACE, { namespace: $scope.namespace, toReleaseId: selectedReleaseId }); } function rollback() { EventManager.emit(EventManager.EventType.ROLLBACK_NAMESPACE, { toReleaseId: selectedReleaseId }); } function findReleaseHistory() { if ($scope.hasLoadAll) { return; } ReleaseHistoryService.findReleaseHistoryByNamespace($scope.pageContext.appId, $scope.pageContext.env, $scope.pageContext.clusterName, $scope.pageContext.namespaceName, $scope.page, PAGE_SIZE) .then(function (result) { if ($scope.page == 0) { $(".release-history").removeClass('hidden'); } if (!result || result.length < PAGE_SIZE) { $scope.hasLoadAll = true; } if (result.length == 0) { return; } $scope.releaseHistories = $scope.releaseHistories.concat(result); if ($scope.page == 0) { var defaultToShowReleaseHistory = result[0]; $scope.releaseHistories.forEach(function (history) { if ($scope.pageContext.releaseHistoryId == history.id) { defaultToShowReleaseHistory = history; } else if ($scope.pageContext.releaseId == history.releaseId) { // text namespace doesn't support ALL view if (!$scope.isTextNamespace) { history.viewType = CONFIG_VIEW_TYPE.ALL; } defaultToShowReleaseHistory = history; } }); showReleaseHistoryDetail(defaultToShowReleaseHistory); } $scope.page = $scope.page + 1; }, function (result) { AppUtil.showErrorMsg(result, $translate.instant('Config.History.LoadingHistoryError')); }); } function loadNamespace() { ConfigService.load_namespace($scope.pageContext.appId, $scope.pageContext.env, $scope.pageContext.clusterName, $scope.pageContext.namespaceName) .then(function (result) { $scope.namespace = result; $scope.isTextNamespace = result.format != "properties"; if ($scope.isTextNamespace) { fixTextNamespaceViewType(); } $scope.isConfigHidden = result.isConfigHidden; initPermission(); }) } function showReleaseHistoryDetail(history) { $scope.history = history; $scope.selectedReleaseHistory = history.id; selectedReleaseId = history.releaseId; if (!history.viewType) {//default view type history.viewType = CONFIG_VIEW_TYPE.DIFF; getReleaseDiffConfiguration(history); } } function initPermission() { PermissionService.has_release_namespace_permission( $scope.pageContext.appId, $scope.namespace.baseInfo.namespaceName) .then(function (result) { if (!result.hasPermission) { PermissionService.has_release_namespace_env_permission( $scope.pageContext.appId, $scope.pageContext.env, $scope.namespace.baseInfo.namespaceName) .then(function (result) { $scope.namespace.hasReleasePermission = result.hasPermission; }); } else { $scope.namespace.hasReleasePermission = result.hasPermission; } }); } function fixTextNamespaceViewType() { $scope.releaseHistories.forEach(function (history) { // text namespace doesn't support ALL view if (history.viewType == CONFIG_VIEW_TYPE.ALL) { switchConfigViewType(history, CONFIG_VIEW_TYPE.DIFF); } }); } function switchConfigViewType(history, viewType) { history.viewType = viewType; if (viewType == CONFIG_VIEW_TYPE.DIFF) { getReleaseDiffConfiguration(history); } } function getReleaseDiffConfiguration(history) { if (!history.changes) { //Set previous release id to master latest release id when branch first gray release. if (history.operation == 2 && history.previousReleaseId == 0) { history.previousReleaseId = history.operationContext.baseReleaseId; } ReleaseService.compare($scope.pageContext.env, history.previousReleaseId, history.releaseId) .then(function (result) { history.changes = result.changes; }) } } function showText(text) { $scope.enableTextDiff = false; $scope.text = text; AppUtil.showModal("#showTextModal"); } function showTextDiff(oldStr, newStr) { $scope.enableTextDiff = true; $scope.oldStr = oldStr; $scope.newStr = newStr; AppUtil.showModal('#showTextModal'); } } ================================================ FILE: apollo-portal/src/main/resources/static/scripts/controller/config/SyncConfigController.js ================================================ /* * Copyright 2024 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ sync_item_module.controller("SyncItemController", ['$scope', '$location', '$window', '$translate', 'toastr', 'AppService', 'AppUtil', 'ConfigService', function ($scope, $location, $window, $translate, toastr, AppService, AppUtil, ConfigService) { var params = AppUtil.parseParams($location.$$url); $scope.pageContext = { appId: params.appid, env: params.env, clusterName: params.clusterName, namespaceName: params.namespaceName }; var sourceItems = []; $scope.syncBtnDisabled = false; $scope.viewItems = []; $scope.toggleItemsCheckedStatus = toggleItemsCheckedStatus; $scope.diff = diff; $scope.removeItem = removeItem; $scope.syncItems = syncItems; $scope.collectSelectedClusters = collectSelectedClusters; $scope.syncItemNextStep = syncItemNextStep; $scope.backToAppHomePage = backToAppHomePage; $scope.switchSelect = switchSelect; $scope.filter = filter; $scope.resetFilter = resetFilter; $scope.showText = showText; init(); function init() { ////// load items ////// ConfigService.find_items($scope.pageContext.appId, $scope.pageContext.env, $scope.pageContext.clusterName, $scope.pageContext.namespaceName, "lastModifiedTime") .then(function (result) { sourceItems = []; result.forEach(function (item) { if (item.key) { item.checked = false; sourceItems.push(item); } }); $scope.viewItems = sourceItems; $(".apollo-container").removeClass("hidden"); }, function (result) { toastr.error(AppUtil.errorMsg(result), $translate.instant('Config.Sync.LoadingItemsError')); }); } var itemAllSelected = false; function toggleItemsCheckedStatus() { itemAllSelected = !itemAllSelected; $scope.viewItems.forEach(function (item) { item.checked = itemAllSelected; }) } var syncData = { syncToNamespaces: [], syncItems: [] }; function diff() { parseSyncSourceData(); if (syncData.syncItems.length == 0) { toastr.warning($translate.instant('Config.Sync.PleaseChooseNeedSyncItems')); return; } if (syncData.syncToNamespaces.length == 0) { toastr.warning($translate.instant('Config.Sync.PleaseChooseCluster')); return; } $scope.hasDiff = false; ConfigService.diff($scope.pageContext.namespaceName, syncData).then( function (result) { $scope.clusterDiffs = result; $scope.clusterDiffs.forEach(function (clusterDiff) { if (!$scope.hasDiff) { $scope.hasDiff = clusterDiff.diffs.createItems.length + clusterDiff.diffs.updateItems.length > 0; } if (clusterDiff.diffs.updateItems.length > 0) { //赋予同步前的值 ConfigService.find_items(clusterDiff.namespace.appId, clusterDiff.namespace.env, clusterDiff.namespace.clusterName, clusterDiff.namespace.namespaceName) .then(function (result) { var oldItemMap = {}; result.forEach(function (item) { oldItemMap[item.key] = item.value; }); clusterDiff.diffs.updateItems.forEach(function (item) { item.oldValue = oldItemMap[item.key]; }) }); } }); $scope.syncItemNextStep(1); }, function (result) { toastr.error(AppUtil.errorMsg(result)); }); } function removeItem(diff, type, toRemoveItem) { var syncDataResult = [], diffSetResult = [], diffSet; if (type == 'create') { diffSet = diff.createItems; } else { diffSet = diff.updateItems; } diffSet.forEach(function (item) { if (item.key != toRemoveItem.key) { diffSetResult.push(item); } }); if (type == 'create') { diff.createItems = diffSetResult; } else { diff.updateItems = diffSetResult; } syncData.syncItems.forEach(function (item) { if (item.key != toRemoveItem.key) { syncDataResult.push(item); } }); syncData.syncItems = syncDataResult; } function syncItems() { $scope.syncBtnDisabled = true; ConfigService.sync_items($scope.pageContext.appId, $scope.pageContext.namespaceName, syncData).then(function (result) { $scope.syncItemStep += 1; $scope.syncSuccess = true; $scope.syncBtnDisabled = false; }, function (result) { $scope.syncSuccess = false; $scope.syncBtnDisabled = false; toastr.error(AppUtil.errorMsg(result)); }); } var selectedClusters = []; function collectSelectedClusters(data) { selectedClusters = data; } function parseSyncSourceData() { syncData = { syncToNamespaces: [], syncItems: [] }; var namespaceName = $scope.pageContext.namespaceName; selectedClusters.forEach(function (cluster) { if (cluster.checked) { cluster.clusterName = cluster.name; cluster.namespaceName = namespaceName; syncData.syncToNamespaces.push(cluster); } }); $scope.viewItems.forEach(function (item) { if (item.checked) { syncData.syncItems.push(item); } }); return syncData; } ////// flow control /////// $scope.syncItemStep = 1; function syncItemNextStep(offset) { $scope.syncItemStep += offset; } function backToAppHomePage() { $window.location.href = AppUtil.prefixPath() + '/config.html?#appid=' + $scope.pageContext.appId; } function switchSelect(o) { o.checked = !o.checked; } function filter() { var beginTime = $scope.filterBeginTime; var endTime = $scope.filterEndTime; var result = []; sourceItems.forEach(function (item) { var updateTime = new Date(item.dataChangeLastModifiedTime); if ((!beginTime || updateTime > beginTime) && (!endTime || updateTime < endTime)) { result.push(item); } }); $scope.viewItems = result; } function resetFilter() { $scope.filterBeginTime = null; $scope.filterEndTime = null; filter(); } function showText(text) { $scope.text = text; AppUtil.showModal('#showTextModal'); } }]); ================================================ FILE: apollo-portal/src/main/resources/static/scripts/controller/open/OpenManageController.js ================================================ /* * Copyright 2024 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ open_manage_module.controller('OpenManageController', ['$scope', '$translate', 'toastr', 'AppUtil', 'OrganizationService', 'ConsumerService', 'PermissionService', 'EnvService', OpenManageController]); function OpenManageController($scope, $translate, toastr, AppUtil, OrganizationService, ConsumerService, PermissionService, EnvService) { var $orgWidget = $('#organization'); $scope.consumer = {}; $scope.consumerRole = { type: 'NamespaceRole' }; $scope.submitBtnDisabled = false; $scope.userSelectWidgetId = 'toAssignMasterRoleUser'; $scope.consumerListPage = 0; $scope.consumerList = []; $scope.hasMoreconsumerList = false; $scope.toOperationConsumer={} $scope.getTokenByAppId = getTokenByAppId; $scope.createConsumer = createConsumer; $scope.assignRoleToConsumer = assignRoleToConsumer; $scope.getConsumerList = getConsumerList; $scope.preDeleteConsumer = preDeleteConsumer; $scope.deleteConsumer = deleteConsumer; $scope.preGrantPermission = preGrantPermission; $scope.toggleRateLimitEnabledInput = function() { if (!$scope.consumer.rateLimitEnabled) { $scope.consumer.rateLimit = 0; } }; function init() { initOrganization(); initPermission(); initEnv(); getConsumerList(); } function initOrganization() { OrganizationService.find_organizations().then(function (result) { var organizations = []; result.forEach(function (item) { var org = {}; org.id = item.orgId; org.text = item.orgName + '(' + item.orgId + ')'; org.name = item.orgName; organizations.push(org); }); $orgWidget.select2({ placeholder: $translate.instant('Common.PleaseChooseDepartment'), width: '100%', data: organizations }); }, function (result) { toastr.error(AppUtil.errorMsg(result), "load organizations error"); }); } function getConsumerList() { var size = 10; ConsumerService.getConsumerList($scope.consumerListPage, size) .then(function (result) { $scope.consumerListPage += 1; $scope.hasMoreconsumerList = result.length === size; if (!result || result.length === 0) { return; } result.forEach(function (app) { $scope.consumerList.push(app); }); }) } let toDeleteAppId = ''; function preDeleteConsumer(app) { $scope.toOperationConsumer = app; toDeleteAppId = app.appId; $("#deleteConsumerDialog").modal("show"); } function deleteConsumer(){ ConsumerService.deleteConsumer(toDeleteAppId) .then(function () { toastr.success($translate.instant('Open.Manage.DeleteConsumer.Success')); $scope.consumerList = $scope.consumerList.filter(consumer => consumer.appId !== toDeleteAppId); },function (error) { toastr.error(AppUtil.errorMsg(error), $translate.instant('Open.Manage.DeleteConsumer.Error')); }) } function initPermission() { PermissionService.has_root_permission() .then(function (result) { $scope.isRootUser = result.hasPermission; }); } function initEnv() { EnvService.find_all_envs() .then(function (result) { $scope.envs = new Array(); for (var iLoop = 0; iLoop < result.length; iLoop++) { $scope.envs.push({ checked: false, env: result[iLoop] }); $scope.envsChecked = new Array(); } $scope.switchSelect = function (item) { item.checked = !item.checked; $scope.envsChecked = new Array(); for (var iLoop = 0; iLoop < $scope.envs.length; iLoop++) { var env = $scope.envs[iLoop]; if (env.checked) { $scope.envsChecked.push(env.env); } } }; }); } function getTokenByAppId() { if (!$scope.consumer.appId) { toastr.warning($translate.instant('Open.Manage.PleaseEnterAppId')); return; } ConsumerService.getConsumerTokenByAppId($scope.consumer.appId) .then(function (consumerToken) { if (consumerToken.token) { $scope.consumerToken = consumerToken; $scope.consumerRole.token = consumerToken.token; } else { $scope.consumerToken = { token: $translate.instant('Open.Manage.AppNotCreated', { appId: $scope.consumer.appId }) }; } }, function (response) { AppUtil.showErrorMsg(response); }); } function createConsumer() { $scope.submitBtnDisabled = true; if (!$scope.consumer.appId) { toastr.warning($translate.instant('Open.Manage.PleaseEnterAppId')); $scope.submitBtnDisabled = false; return; } if ($scope.consumer.rateLimitEnabled) { if (!$scope.consumer.rateLimit || $scope.consumer.rateLimit < 1) { toastr.warning($translate.instant('Open.Manage.Consumer.RateLimitValue.Error')); $scope.submitBtnDisabled = false; return; } } else { $scope.consumer.rateLimit = 0; } var selectedOrg = $orgWidget.select2('data')[0]; if (!selectedOrg.id) { toastr.warning($translate.instant('Common.PleaseChooseDepartment')); $scope.submitBtnDisabled = false; return; } $scope.consumer.orgId = selectedOrg.id; $scope.consumer.orgName = selectedOrg.name; // owner var owner = $('.ownerSelector').select2('data')[0]; if (!owner) { toastr.warning($translate.instant('Common.PleaseChooseOwner')); $scope.submitBtnDisabled = false; return; } $scope.consumer.ownerName = owner.id; ConsumerService.createConsumer($scope.consumer) .then(function (consumerToken) { toastr.success($translate.instant('Common.Created')); $scope.consumerToken = consumerToken; $scope.consumerRole.token = consumerToken.token; $scope.submitBtnDisabled = false; $scope.consumer = {}; }, function (response) { AppUtil.showErrorMsg(response, $translate.instant('Common.CreateFailed')); $scope.submitBtnDisabled = false; }) } function preGrantPermission(app){ $scope.consumer.appId = app.appId; getTokenByAppId(); AppUtil.showModal('#grantPermissionModal'); } function assignRoleToConsumer() { ConsumerService.assignRoleToConsumer($scope.consumerRole.token, $scope.consumerRole.type, $scope.consumerRole.appId, $scope.consumerRole.namespaceName, $scope.envsChecked) .then(function (consumerRoles) { toastr.success($translate.instant('Open.Manage.GrantSuccessfully')); }, function (response) { AppUtil.showErrorMsg(response, $translate.instant('Open.Manage.GrantFailed')); }) } init(); } ================================================ FILE: apollo-portal/src/main/resources/static/scripts/controller/role/ClusterNamespaceRoleController.js ================================================ /* * Copyright 2024 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ role_module.controller('ClusterNamespaceRoleController', ['$scope', '$location', '$window', '$translate', 'toastr', 'AppService', 'UserService', 'AppUtil', 'EnvService', 'PermissionService', function ($scope, $location, $window, $translate, toastr, AppService, UserService, AppUtil, EnvService, PermissionService) { var params = AppUtil.parseParams($location.$$url); $scope.pageContext = { appId: params.appid, env: params.env, clusterName: params.clusterName }; $scope.modifyRoleSubmitBtnDisabled = false; $scope.releaseRoleSubmitBtnDisabled = false; $scope.releaseRoleWidgetId = 'releaseRoleWidgetId'; $scope.modifyRoleWidgetId = 'modifyRoleWidgetId'; PermissionService.init_cluster_ns_permission($scope.pageContext.appId, $scope.pageContext.env, $scope.pageContext.clusterName) .then(function (result) { }, function (result) { toastr.warning(AppUtil.errorMsg(result), $translate.instant('Cluster.Role.InitClusterPermissionError')); }); PermissionService.has_assign_user_permission($scope.pageContext.appId) .then(function (result) { $scope.hasAssignUserPermission = result.hasPermission; }, function (result) { }); PermissionService.get_cluster_ns_role_users($scope.pageContext.appId, $scope.pageContext.env, $scope.pageContext.clusterName) .then(function (result) { $scope.rolesAssignedUsers = result; }, function (result) { toastr.error(AppUtil.errorMsg(result), $translate.instant('Cluster.Role.GetGrantUserError')); }); $scope.assignRoleToUser = function (roleType) { if ("ReleaseNamespacesInCluster" === roleType) { var user = $('.' + $scope.releaseRoleWidgetId).select2('data')[0]; if (!user) { toastr.warning($translate.instant('Cluster.Role.PleaseChooseUser')); return; } $scope.releaseRoleSubmitBtnDisabled = true; var toAssignReleaseNamespacesInClusterRoleUser = user.id; var assignReleaseNamespacesInClusterRoleFunc = function (appId, env, clusterName, user) { return PermissionService.assign_release_cluster_ns_role(appId, env, clusterName, user); }; assignReleaseNamespacesInClusterRoleFunc( $scope.pageContext.appId, $scope.pageContext.env, $scope.pageContext.clusterName, toAssignReleaseNamespacesInClusterRoleUser ).then(function () { toastr.success($translate.instant('Cluster.Role.Added')); $scope.releaseRoleSubmitBtnDisabled = false; $scope.rolesAssignedUsers.releaseRoleUsers.push({ userId: toAssignReleaseNamespacesInClusterRoleUser }); $('.' + $scope.releaseRoleWidgetId).select2("val", ""); }, function (result) { $scope.releaseRoleSubmitBtnDisabled = false; toastr.error(AppUtil.errorMsg(result), $translate.instant('Cluster.Role.AddFailed')); }); } else if ("ModifyNamespacesInCluster" === roleType) { var user1 = $('.' + $scope.modifyRoleWidgetId).select2('data')[0]; if (!user1) { toastr.warning($translate.instant('Cluster.Role.PleaseChooseUser')); return; } $scope.modifyRoleSubmitBtnDisabled = true; var toAssignModifyNamespacesInClusterRoleUser = user1.id; var assignModifyNamespacesInClusterRoleFunc = function (appId, env, clusterName, user) { return PermissionService.assign_modify_cluster_ns_role(appId, env, clusterName, user); }; assignModifyNamespacesInClusterRoleFunc( $scope.pageContext.appId, $scope.pageContext.env, $scope.pageContext.clusterName, toAssignModifyNamespacesInClusterRoleUser ).then(function () { toastr.success($translate.instant('Cluster.Role.Added')); $scope.modifyRoleSubmitBtnDisabled = false; $scope.rolesAssignedUsers.modifyRoleUsers.push({ userId: toAssignModifyNamespacesInClusterRoleUser }); $('.' + $scope.modifyRoleWidgetId).select2("val", ""); }, function (result) { $scope.modifyRoleSubmitBtnDisabled = false; toastr.error(AppUtil.errorMsg(result), $translate.instant('Cluster.Role.AddFailed')); }); } }; $scope.removeUserRole = function (roleType, user) { if ("ReleaseNamespacesInCluster" === roleType) { var removeReleaseNamespacesInClusterRoleFunc = function (appId, env, clusterName, user) { return PermissionService.remove_release_cluster_ns_role(appId, env, clusterName, user); }; removeReleaseNamespacesInClusterRoleFunc( $scope.pageContext.appId, $scope.pageContext.env, $scope.pageContext.clusterName, user ).then(function () { toastr.success($translate.instant('Cluster.Role.Deleted')); removeUserFromList($scope.rolesAssignedUsers.releaseRoleUsers, user); }, function (result) { toastr.error(AppUtil.errorMsg(result), $translate.instant('Namespace.Role.DeleteFailed')); }); } else if ("ModifyNamespacesInCluster" === roleType) { var removeModifyNamespacesInClusterRoleFunc = function (appId, env, clusterName, user) { return PermissionService.remove_modify_cluster_ns_role(appId, env, clusterName, user); }; removeModifyNamespacesInClusterRoleFunc( $scope.pageContext.appId, $scope.pageContext.env, $scope.pageContext.clusterName, user ).then(function () { toastr.success($translate.instant('Cluster.Role.Deleted')); removeUserFromList($scope.rolesAssignedUsers.modifyRoleUsers, user); }, function (result) { toastr.error(AppUtil.errorMsg(result), $translate.instant('Cluster.Role.DeleteFailed')); }); } }; function removeUserFromList(list, user) { var index = 0; for (var i = 0; i < list.length; i++) { if (list[i].userId === user) { index = i; break; } } list.splice(index, 1); } }]); ================================================ FILE: apollo-portal/src/main/resources/static/scripts/controller/role/NamespaceRoleController.js ================================================ /* * Copyright 2024 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ role_module.controller('NamespaceRoleController', ['$scope', '$location', '$window', '$translate', 'toastr', 'AppService', 'UserService', 'AppUtil', 'EnvService', 'PermissionService', function ($scope, $location, $window, $translate, toastr, AppService, UserService, AppUtil, EnvService, PermissionService) { var params = AppUtil.parseParams($location.$$url); $scope.pageContext = { appId: params.appid, namespaceName: params.namespaceName }; $scope.modifyRoleSubmitBtnDisabled = false; $scope.ReleaseRoleSubmitBtnDisabled = false; $scope.releaseRoleWidgetId = 'releaseRoleWidgetId'; $scope.modifyRoleWidgetId = 'modifyRoleWidgetId'; $scope.modifyRoleSelectedEnv = ""; $scope.releaseRoleSelectedEnv = ""; PermissionService.init_app_namespace_permission($scope.pageContext.appId, $scope.pageContext.namespaceName) .then(function (result) { }, function (result) { toastr.warning(AppUtil.errorMsg(result), $translate.instant('Namespace.Role.InitNamespacePermissionError')); }); PermissionService.has_assign_user_permission($scope.pageContext.appId) .then(function (result) { $scope.hasAssignUserPermission = result.hasPermission; }, function (reslt) { }); EnvService.find_all_envs() .then(function (result) { $scope.envs = result; $scope.envRolesAssignedUsers = {}; for (var iLoop = 0; iLoop < result.length; iLoop++) { var env = result[iLoop]; PermissionService.get_namespace_env_role_users($scope.pageContext.appId, env, $scope.pageContext.namespaceName) .then(function (result) { $scope.envRolesAssignedUsers[result.env] = result; }, function (result) { toastr.error(AppUtil.errorMsg(result), $translate.instant('Namespace.Role.GetEnvGrantUserError', { env })); }); } }); PermissionService.get_namespace_role_users($scope.pageContext.appId, $scope.pageContext.namespaceName) .then(function (result) { $scope.rolesAssignedUsers = result; }, function (result) { toastr.error(AppUtil.errorMsg(result), $translate.instant('Namespace.Role.GetGrantUserError')); }); $scope.assignRoleToUser = function (roleType) { if ("ReleaseNamespace" === roleType) { var user = $('.' + $scope.releaseRoleWidgetId).select2('data')[0]; if (!user) { toastr.warning($translate.instant('Namespace.Role.PleaseChooseUser')); return; } $scope.ReleaseRoleSubmitBtnDisabled = true; var toAssignReleaseNamespaceRoleUser = user.id; var assignReleaseNamespaceRoleFunc = $scope.releaseRoleSelectedEnv === "" ? PermissionService.assign_release_namespace_role : function (appId, namespaceName, user) { return PermissionService.assign_release_namespace_env_role(appId, $scope.releaseRoleSelectedEnv, namespaceName, user); }; assignReleaseNamespaceRoleFunc($scope.pageContext.appId, $scope.pageContext.namespaceName, toAssignReleaseNamespaceRoleUser) .then(function (result) { toastr.success($translate.instant('Namespace.Role.Added')); $scope.ReleaseRoleSubmitBtnDisabled = false; if ($scope.releaseRoleSelectedEnv === "") { $scope.rolesAssignedUsers.releaseRoleUsers.push( { userId: toAssignReleaseNamespaceRoleUser }); } else { $scope.envRolesAssignedUsers[$scope.releaseRoleSelectedEnv].releaseRoleUsers.push( { userId: toAssignReleaseNamespaceRoleUser }); } $('.' + $scope.releaseRoleWidgetId).select2("val", ""); $scope.releaseRoleSelectedEnv = ""; }, function (result) { $scope.ReleaseRoleSubmitBtnDisabled = false; toastr.error(AppUtil.errorMsg(result), $translate.instant('Namespace.Role.AddFailed')); }); } else { var user = $('.' + $scope.modifyRoleWidgetId).select2('data')[0]; if (!user) { toastr.warning($translate.instant('Namespace.Role.PleaseChooseUser')); return; } $scope.modifyRoleSubmitBtnDisabled = true; var toAssignModifyNamespaceRoleUser = user.id; var assignModifyNamespaceRoleFunc = $scope.modifyRoleSelectedEnv === "" ? PermissionService.assign_modify_namespace_role : function (appId, namespaceName, user) { return PermissionService.assign_modify_namespace_env_role(appId, $scope.modifyRoleSelectedEnv, namespaceName, user); }; assignModifyNamespaceRoleFunc($scope.pageContext.appId, $scope.pageContext.namespaceName, toAssignModifyNamespaceRoleUser) .then(function (result) { toastr.success($translate.instant('Namespace.Role.Added')); $scope.modifyRoleSubmitBtnDisabled = false; if ($scope.modifyRoleSelectedEnv === "") { $scope.rolesAssignedUsers.modifyRoleUsers.push( { userId: toAssignModifyNamespaceRoleUser }); } else { $scope.envRolesAssignedUsers[$scope.modifyRoleSelectedEnv].modifyRoleUsers.push( { userId: toAssignModifyNamespaceRoleUser }); } $('.' + $scope.modifyRoleWidgetId).select2("val", ""); $scope.modifyRoleSelectedEnv = ""; }, function (result) { $scope.modifyRoleSubmitBtnDisabled = false; toastr.error(AppUtil.errorMsg(result), $translate.instant('Namespace.Role.AddFailed')); }); } }; $scope.removeUserRole = function (roleType, user, env) { if ("ReleaseNamespace" === roleType) { var removeReleaseNamespaceRoleFunc = !env ? PermissionService.remove_release_namespace_role : function (appId, namespaceName, user) { return PermissionService.remove_release_namespace_env_role(appId, env, namespaceName, user); }; removeReleaseNamespaceRoleFunc($scope.pageContext.appId, $scope.pageContext.namespaceName, user) .then(function (result) { toastr.success($translate.instant('Namespace.Role.Deleted')); if (!env) { removeUserFromList($scope.rolesAssignedUsers.releaseRoleUsers, user); } else { removeUserFromList($scope.envRolesAssignedUsers[env].releaseRoleUsers, user); } }, function (result) { toastr.error(AppUtil.errorMsg(result), $translate.instant('Namespace.Role.DeleteFailed')); }); } else { var removeModifyNamespaceRoleFunc = !env ? PermissionService.remove_modify_namespace_role : function (appId, namespaceName, user) { return PermissionService.remove_modify_namespace_env_role(appId, env, namespaceName, user); }; removeModifyNamespaceRoleFunc($scope.pageContext.appId, $scope.pageContext.namespaceName, user) .then(function (result) { toastr.success($translate.instant('Namespace.Role.Deleted')); if (!env) { removeUserFromList($scope.rolesAssignedUsers.modifyRoleUsers, user); } else { removeUserFromList($scope.envRolesAssignedUsers[env].modifyRoleUsers, user); } }, function (result) { toastr.error(AppUtil.errorMsg(result), $translate.instant('Namespace.Role.DeleteFailed')); }); } }; function removeUserFromList(list, user) { var index = 0; for (var i = 0; i < list.length; i++) { if (list[i].userId === user) { index = i; break; } } list.splice(index, 1); } }]); ================================================ FILE: apollo-portal/src/main/resources/static/scripts/controller/role/SystemRoleController.js ================================================ /* * Copyright 2024 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ angular.module('systemRole', ['app.service', 'apollo.directive', 'app.util', 'toastr', 'angular-loading-bar']) .controller('SystemRoleController', ['$scope', '$location', '$window', '$translate', 'toastr', 'AppService', 'UserService', 'AppUtil', 'EnvService', 'PermissionService', 'SystemRoleService', function SystemRoleController($scope, $location, $window, $translate, toastr, AppService, UserService, AppUtil, EnvService, PermissionService, SystemRoleService) { $scope.addCreateApplicationBtnDisabled = false; $scope.deleteCreateApplicationBtnDisabled = false; $scope.modifySystemRoleWidgetId = 'modifySystemRoleWidgetId'; $scope.modifyManageAppMasterRoleWidgetId = 'modifyManageAppMasterRoleWidgetId'; $scope.hasCreateApplicationPermissionUserList = []; $scope.operateManageAppMasterRoleBtn = true; $scope.app = { appId: "", info: "" }; initPermission(); $scope.addCreateApplicationRoleToUser = function () { var user = $('.' + $scope.modifySystemRoleWidgetId).select2('data')[0]; if (!user) { toastr.warning($translate.instant('SystemRole.PleaseChooseUser')); return; } SystemRoleService.add_create_application_role(user.id) .then( function (value) { toastr.info($translate.instant('SystemRole.Added')); getCreateApplicationRoleUsers(); }, function (reason) { toastr.warning(AppUtil.errorMsg(reason), $translate.instant('SystemRole.AddFailed')); } ); }; $scope.deleteCreateApplicationRoleFromUser = function (userId) { SystemRoleService.delete_create_application_role(userId) .then( function (value) { toastr.info($translate.instant('SystemRole.Deleted')); getCreateApplicationRoleUsers(); }, function (reason) { toastr.warning(AppUtil.errorMsg(reason), $translate.instant('SystemRole.DeleteFailed')); } ); }; function getCreateApplicationRoleUsers() { SystemRoleService.get_create_application_role_users() .then( function (result) { $scope.hasCreateApplicationPermissionUserList = result; }, function (reason) { toastr.warning(AppUtil.errorMsg(reason), $translate.instant('SystemRole.GetCanCreateProjectUsersError')); } ) } function initPermission() { PermissionService.has_root_permission() .then(function (result) { $scope.isRootUser = result.hasPermission; if ($scope.isRootUser) { getCreateApplicationRoleUsers(); } }); } $scope.getAppInfo = function () { if (!$scope.app.appId) { toastr.warning($translate.instant('SystemRole.PleaseEnterAppId')); $scope.operateManageAppMasterRoleBtn = true; return; } $scope.app.info = ""; AppService.load($scope.app.appId).then(function (result) { if (!result.appId) { toastr.warning($translate.instant('SystemRole.AppIdNotFound', { appId: $scope.app.appId })); $scope.operateManageAppMasterRoleBtn = true; return; } $scope.app.info = $translate.instant('SystemRole.AppInfoContent', { appName: result.name, departmentName: result.orgName, departmentId: result.orgId, ownerName: result.ownerName }); $scope.operateManageAppMasterRoleBtn = false; }, function (result) { AppUtil.showErrorMsg(result); $scope.operateManageAppMasterRoleBt = true; }); }; $scope.deleteAppMasterAssignRole = function () { if (!$scope.app.appId) { toastr.warning($translate.instant('SystemRole.PleaseEnterAppId')); return; } var user = $('.' + $scope.modifyManageAppMasterRoleWidgetId).select2('data')[0]; if (!user) { toastr.warning($translate.instant('SystemRole.PleaseChooseUser')); return; } var confirmTips = $translate.instant('SystemRole.DeleteMasterAssignRoleTips', { appId: $scope.app.appId, userId: user.id }); if (confirm(confirmTips)) { AppService.delete_app_master_assign_role($scope.app.appId, user.id).then(function (result) { var deletedTips = $translate.instant('SystemRole.DeletedMasterAssignRoleTips', { appId: $scope.app.appId, userId: user.id }); toastr.success(deletedTips); $scope.operateManageAppMasterRoleBtn = true; }, function (result) { AppUtil.showErrorMsg(result); }) } }; $scope.allowAppMasterAssignRole = function () { if (!$scope.app.appId) { toastr.warning($translate.instant('SystemRole.PleaseEnterAppId')); return; } var user = $('.' + $scope.modifyManageAppMasterRoleWidgetId).select2('data')[0]; if (!user) { toastr.warning($translate.instant('SystemRole.PleaseChooseUser')); return; } var confirmTips = $translate.instant('SystemRole.AllowAppMasterAssignRoleTips', { appId: $scope.app.appId, userId: user.id }); if (confirm(confirmTips)) { AppService.allow_app_master_assign_role($scope.app.appId, user.id).then(function (result) { var allowedTips = $translate.instant('SystemRole.AllowedAppMasterAssignRoleTips', { appId: $scope.app.appId, userId: user.id }); toastr.success(allowedTips); $scope.operateManageAppMasterRoleBtn = true; }, function (result) { AppUtil.showErrorMsg(result); }) } }; }]); ================================================ FILE: apollo-portal/src/main/resources/static/scripts/directive/delete-namespace-modal-directive.js ================================================ /* * Copyright 2024 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ directive_module.directive('deletenamespacemodal', deleteNamespaceModalDirective); function deleteNamespaceModalDirective($window, $q, $translate, toastr, AppUtil, EventManager, PermissionService, UserService, NamespaceService) { return { restrict: 'E', templateUrl: AppUtil.prefixPath() + '/views/component/delete-namespace-modal.html', transclude: true, replace: true, scope: { env: '=' }, link: function (scope) { scope.doDeleteNamespace = doDeleteNamespace; EventManager.subscribe(EventManager.EventType.PRE_DELETE_NAMESPACE, function (context) { var toDeleteNamespace = context.namespace; scope.toDeleteNamespace = toDeleteNamespace; //1. check operator has master permission checkPermission(toDeleteNamespace).then(function () { if (toDeleteNamespace.isLinkedNamespace) { NamespaceService.getLinkedNamespaceUsage(toDeleteNamespace.baseInfo.appId, scope.env, toDeleteNamespace.baseInfo.clusterName, toDeleteNamespace.baseInfo.namespaceName ).then(function (usage) { scope.toDeleteNamespace.namespaceUsage = usage; if (usage[0].instanceCount > 0 || usage[0].branchInstanceCount > 0) { scope.toDeleteNamespace.forceDeleteButton = true; } showDeleteNamespaceConfirmDialog(); }); } else { NamespaceService.getNamespaceUsage(toDeleteNamespace.baseInfo.appId, toDeleteNamespace.baseInfo.namespaceName ).then(function (usage) { scope.toDeleteNamespace.namespaceUsage = usage; if (usage.length > 0) { scope.toDeleteNamespace.forceDeleteButton = true; } showDeleteNamespaceConfirmDialog(); }); } }) }); function checkPermission(namespace) { var d = $q.defer(); UserService.load_user().then(function (currentUser) { var isAppMasterUser = false; PermissionService.get_app_role_users(namespace.baseInfo.appId) .then(function (appRoleUsers) { var masterUsers = []; appRoleUsers.masterUsers.forEach(function (user) { masterUsers.push(_.escape(user.userId)); if (currentUser.userId == user.userId) { isAppMasterUser = true; } }); scope.masterUsers = masterUsers; scope.isAppMasterUser = isAppMasterUser; if (!isAppMasterUser) { toastr.error($translate.instant('Config.DeleteNamespaceNoPermissionFailedTips', { users: scope.masterUsers.join(", ") }), $translate.instant('Config.DeleteNamespaceNoPermissionFailedTitle')); d.reject(); } else { d.resolve(); } }); }); return d.promise; } function showDeleteNamespaceConfirmDialog() { AppUtil.showModal('#deleteNamespaceModal'); } function doDeleteNamespace() { var toDeleteNamespace = scope.toDeleteNamespace; if(toDeleteNamespace.isLinkedNamespace){ NamespaceService.deleteLinkedNamespace(toDeleteNamespace.baseInfo.appId, scope.env, toDeleteNamespace.baseInfo.clusterName, toDeleteNamespace.baseInfo.namespaceName) .then(function () { toastr.success($translate.instant('Common.Deleted')); setTimeout(function () { $window.location.reload(); }, 1000); }, function (result) { AppUtil.showErrorMsg(result, $translate.instant('Common.DeleteFailed')); }) } else { NamespaceService.deleteAppNamespace(toDeleteNamespace.baseInfo.appId, toDeleteNamespace.baseInfo.namespaceName) .then(function () { toastr.success($translate.instant('Common.Deleted')); setTimeout(function () { $window.location.reload(); }, 1000); }, function (result) { AppUtil.showErrorMsg(result, $translate.instant('Common.DeleteFailed')); }) } } } } } ================================================ FILE: apollo-portal/src/main/resources/static/scripts/directive/diff-directive.js ================================================ /* * Copyright 2024 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ directive_module.directive('apollodiff', function ($compile, $window, AppUtil) { return { restrict: 'E', templateUrl: AppUtil.prefixPath() + '/views/component/diff.html', transclude: true, replace: true, scope: { oldStr: '=', newStr: '=', apolloId: '=' }, link: function (scope, element, attrs) { scope.$watch('oldStr', makeDiff); scope.$watch('newStr', makeDiff); function makeDiff() { var displayArea = document.getElementById(scope.apolloId); if (!displayArea) { return; } //clear displayArea.innerHTML = ''; var color = '', span = null, pre = ''; var oldStr = scope.oldStr == undefined ? '' : scope.oldStr; var newStr = scope.newStr == undefined ? '' : scope.newStr; var oldStrRes = oldStr.replace(/\r/g, ""); var newStrRes = newStr.replace(/\r/g, ""); var diff = JsDiff.diffLines(oldStrRes, newStrRes), fragment = document.createDocumentFragment(); diff.forEach(function (part) { // green for additions, red for deletions // grey for common parts color = part.added ? 'green' : part.removed ? 'red' : 'grey'; span = document.createElement('span'); span.style.color = color; span.style.display = 'block'; pre = part.added ? '+' : part.removed ? '-' : ''; span.appendChild(document.createTextNode(pre + part.value)); fragment.appendChild(span); }); displayArea.appendChild(fragment); } } } }); ================================================ FILE: apollo-portal/src/main/resources/static/scripts/directive/directive.js ================================================ /* * Copyright 2024 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ /** navbar */ directive_module.directive('apollonav', function ($compile, $window, $translate, toastr, AppUtil, AppService, EnvService, UserService, CommonService, PermissionService) { return { restrict: 'E', templateUrl: AppUtil.prefixPath() + '/views/common/nav.html', transclude: true, replace: true, link: function (scope, element, attrs) { CommonService.getPageSetting().then(function (setting) { scope.pageSetting = setting; }); // Looks like a trick to make xml/yml/json namespaces display right, but why? $(document).on('click', function () { scope.$apply(function () {}); }); $translate('ApolloConfirmDialog.SearchPlaceHolder').then(function(placeholderLabel) { $('#app-search-list').select2({ placeholder: placeholderLabel, ajax: { url: AppUtil.prefixPath() + "/apps/search/by-appid-or-name", dataType: 'json', delay: 400, data: function (params) { return { query: params.term || '', page: params.page ? params.page - 1 : 0, size: 20 }; }, processResults: function (data) { if (data && data.content) { var hasMore = data.content.length === data.size; var result = []; data.content.forEach(function (app) { result.push({ id: app.appId, text: app.appId + ' / ' + app.name, orgId: app.orgId, orgName: app.orgName }) }); return { results: result, pagination: { more: hasMore } }; } else { return { results: [], pagination: { more: false } }; } } } }); $('#app-search-list').on('select2:select', function () { var selected = $('#app-search-list').select2('data'); if (selected && selected.length) { jumpToConfigPage(selected[0].id, selected[0].name, selected[0].orgName, selected[0].orgId) } }); }); function jumpToConfigPage(selectedAppId, name, type, namespaceInfo) { if ($window.location.href.indexOf("config.html") > -1) { if (type && type.startsWith('SearchByItem')) { var namespaceInfos = namespaceInfo.split('+'); var env = namespaceInfos[0]; var cluster = namespaceInfos[1]; var namespaceName = namespaceInfos[2]; var searchKey = type.split("+")[1]; $window.location.hash = "appid=" + selectedAppId + "&env=" + env + "&cluster=" + cluster + "&namespace=" + namespaceName + "&item=" + searchKey; } else { $window.location.hash = "appid=" + selectedAppId; } $window.location.reload(); } else { if (type && type.startsWith('SearchByItem')) { var namespaceInfos = namespaceInfo.split('+'); var env = namespaceInfos[0]; var cluster = namespaceInfos[1]; var namespaceName = namespaceInfos[2]; var searchKey = type.split("+")[1]; $window.location.href = AppUtil.prefixPath() + '/config.html?#appid=' + selectedAppId + "&env=" + env + "&cluster=" + cluster + "&namespace=" + namespaceName + "&item=" + searchKey; } else { $window.location.href = AppUtil.prefixPath() + '/config.html?#appid=' + selectedAppId; } } }; UserService.load_user().then(function (result) { scope.userName = result.userId; scope.userDisplayName = result.name; }, function (result) { }); PermissionService.has_root_permission().then(function (result) { scope.hasRootPermission = result.hasPermission; }) scope.changeLanguage = function (lang) { $translate.use(lang) } } } }); /** env cluster selector*/ directive_module.directive('apolloclusterselector', function ($compile, $window, AppService, AppUtil, toastr) { return { restrict: 'E', templateUrl: AppUtil.prefixPath() + '/views/component/env-selector.html', transclude: true, replace: true, scope: { appId: '=apolloAppId', defaultAllChecked: '=apolloDefaultAllChecked', select: '=apolloSelect', defaultCheckedEnv: '=apolloDefaultCheckedEnv', defaultCheckedCluster: '=apolloDefaultCheckedCluster', notCheckedEnv: '=apolloNotCheckedEnv', notCheckedCluster: '=apolloNotCheckedCluster' }, link: function (scope, element, attrs) { scope.$watch("defaultCheckedEnv", refreshClusterList); scope.$watch("defaultCheckedCluster", refreshClusterList); refreshClusterList(); function refreshClusterList() { AppService.load_nav_tree(scope.appId).then(function (result) { scope.clusters = []; var envClusterInfo = AppUtil.collectData(result); envClusterInfo.forEach(function (node) { var env = node.env; node.clusters.forEach(function (cluster) { cluster.env = env; //default checked cluster.checked = scope.defaultAllChecked || (cluster.env == scope.defaultCheckedEnv && cluster.name == scope.defaultCheckedCluster); //not checked if (cluster.env == scope.notCheckedEnv && cluster.name == scope.notCheckedCluster) { cluster.checked = false; } scope.clusters.push(cluster); }) }); scope.select(collectSelectedClusters()); }); } scope.envAllSelected = scope.defaultAllChecked; scope.toggleEnvsCheckedStatus = function () { scope.envAllSelected = !scope.envAllSelected; scope.clusters.forEach(function (cluster) { cluster.checked = scope.envAllSelected; }); scope.select(collectSelectedClusters()); }; scope.switchSelect = function (o, $event) { o.checked = !o.checked; $event.stopPropagation(); scope.select(collectSelectedClusters()); }; scope.toggleClusterCheckedStatus = function (cluster) { cluster.checked = !cluster.checked; scope.select(collectSelectedClusters()); }; function collectSelectedClusters() { var selectedClusters = []; scope.clusters.forEach(function (cluster) { if (cluster.checked) { cluster.clusterName = cluster.name; selectedClusters.push(cluster); } }); return selectedClusters; } } } }); /** 必填项*/ directive_module.directive('apollorequiredfield', function ($compile, $window) { return { restrict: 'E', template: '*', transclude: true, replace: true } }); /** 确认框 */ directive_module.directive('apolloconfirmdialog', function ($compile, $window, $sce,$translate,AppUtil) { return { restrict: 'E', templateUrl: AppUtil.prefixPath() + '/views/component/confirm-dialog.html', transclude: true, replace: true, scope: { dialogId: '=apolloDialogId', title: '=apolloTitle', detail: '=apolloDetail', showCancelBtn: '=apolloShowCancelBtn', doConfirm: '=apolloConfirm', extraClass: '=apolloExtraClass', confirmBtnText: '=?', cancel: '=' }, link: function (scope, element, attrs) { scope.$watch("detail", function () { scope.detailAsHtml = $sce.trustAsHtml(scope.detail); }); if (!scope.confirmBtnText) { scope.confirmBtnText = $translate.instant('ApolloConfirmDialog.DefaultConfirmBtnName'); } scope.confirm = function () { if (scope.doConfirm) { scope.doConfirm(); } }; } } }); /** entrance */ directive_module.directive('apolloentrance', function ($compile, $window,AppUtil) { return { restrict: 'E', templateUrl: AppUtil.prefixPath() + '/views/component/entrance.html', transclude: true, replace: true, scope: { imgSrc: '=apolloImgSrc', title: '=apolloTitle', href: '=apolloHref' }, link: function (scope, element, attrs) { } } }); /** entrance */ directive_module.directive('apollouserselector', function ($compile, $window,AppUtil) { return { restrict: 'E', templateUrl: AppUtil.prefixPath() + '/views/component/user-selector.html', transclude: true, replace: true, scope: { id: '=apolloId', disabled: '=' }, link: function (scope, element, attrs) { scope.$watch("id", initSelect2); var select2Options = { ajax: { url: AppUtil.prefixPath() + '/users', dataType: 'json', delay: 250, data: function (params) { return { keyword: params.term ? params.term : '', limit: 100 } }, processResults: function (data, params) { var users = []; data.forEach(function (user) { users.push({ id: user.userId, text: user.userId + " | " + user.name + " | " + user.email }) }); return { results: users } }, cache: true, minimumInputLength: 5 } }; function initSelect2() { $('.' + scope.id).select2(select2Options); } } } }); directive_module.directive('apollomultipleuserselector', function ($compile, $window,AppUtil) { return { restrict: 'E', templateUrl: AppUtil.prefixPath() + '/views/component/multiple-user-selector.html', transclude: true, replace: true, scope: { id: '=apolloId' }, link: function (scope, element, attrs) { scope.$watch("id", initSelect2); var searchUsersAjax = { ajax: { url: AppUtil.prefixPath() + '/users', dataType: 'json', delay: 250, data: function (params) { return { keyword: params.term ? params.term : '', limit: 100 } }, processResults: function (data, params) { var users = []; data.forEach(function (user) { users.push({ id: user.userId, text: user.userId + " | " + user.name + " | " + user.email }) }); return { results: users } }, cache: true, minimumInputLength: 5 } }; function initSelect2() { $('.' + scope.id).select2(searchUsersAjax); } } } }); ================================================ FILE: apollo-portal/src/main/resources/static/scripts/directive/gray-release-rules-modal-directive.js ================================================ /* * Copyright 2024 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ directive_module.directive('rulesmodal', rulesModalDirective); function rulesModalDirective($translate, toastr, AppUtil, EventManager, InstanceService) { return { restrict: 'E', templateUrl: AppUtil.prefixPath() + '/views/component/gray-release-rules-modal.html', transclude: true, replace: true, scope: { appId: '=', env: '=', cluster: '=' }, link: function (scope) { scope.completeEditBtnDisable = false; scope.batchAddIPs = batchAddIPs; scope.batchAddLabels = batchAddLabels; scope.addRules = addRules; scope.removeRule = removeRule; scope.removeRuleLabel = removeRuleLabel; scope.completeEditItem = completeEditItem; scope.cancelEditItem = cancelEditItem; scope.initSelectIps = initSelectIps; scope.changeApplyToAllInstancesToTrue = changeApplyToAllInstancesToTrue; scope.changeApplyToAllInstancesToFalse = changeApplyToAllInstancesToFalse; EventManager.subscribe(EventManager.EventType.EDIT_GRAY_RELEASE_RULES, function (context) { var branch = context.branch; scope.branch = branch; if (branch.editingRuleItem.clientIpList && branch.editingRuleItem.clientIpList[0] == '*' && branch.editingRuleItem.clientLabelList && branch.editingRuleItem.clientLabelList[0] == '*') { branch.editingRuleItem.ApplyToAllInstances = true; } else { branch.editingRuleItem.ApplyToAllInstances = false; } $('.rules-ip-selector').select2({ placeholder: $translate.instant('RulesModal.ChooseInstances'), allowClear: true }); AppUtil.showModal('#rulesModal'); }); $('.rules-ip-selector').on('select2:select', function () { addRules(scope.branch); }); function changeApplyToAllInstancesToTrue(branch) { branch.editingRuleItem.ApplyToAllInstances = true; } function changeApplyToAllInstancesToFalse(branch) { branch.editingRuleItem.ApplyToAllInstances = false; if (branch.editingRuleItem.draftIpList[0] == '*') { branch.editingRuleItem.draftIpList = []; } if (branch.editingRuleItem.draftLabelList[0] == '*') { branch.editingRuleItem.draftLabelList = []; } } function addRules(branch) { var newRules, selector = $('.rules-ip-selector'); newRules = selector.select2('data'); var parsedIPs = []; newRules.forEach(function (rule) { parsedIPs.push(rule.text); }); selector.select2("val", ""); addRuleItemIP(branch, parsedIPs); scope.$apply(); } function batchAddIPs(branch, newIPs) { if (!newIPs) { return; } addRuleItemIP(branch, newIPs.split(',')); } function batchAddLabels(branch, newLabels) { if (!newLabels) { return; } addRuleItemLabel(branch, newLabels.split(',')); } function addRuleItemIP(branch, newIps) { var oldIPs = branch.editingRuleItem.draftIpList; if (newIps && newIps.length > 0) { newIps.forEach(function (IP) { if (!AppUtil.checkIPV4(IP)) { toastr.error($translate.instant('RulesModal.ChooseInstances', { ip: IP })); } else if (oldIPs.indexOf(IP) < 0) { oldIPs.push(IP); } }) } //remove IP:all oldIPs.forEach(function (IP, index) { if (IP == "*") { oldIPs.splice(index, 1); } }); } function addRuleItemLabel(branch, newLabels) { var oldLabels = branch.editingRuleItem.draftLabelList; if (newLabels && newLabels.length > 0) { newLabels.forEach(function (Label) { if (oldLabels.indexOf(Label) < 0) { oldLabels.push(Label); } }) } //remove Label:all oldLabels.forEach(function (Label, index) { if (Label == "*") { oldLabels.splice(index, 1); } }); } function removeRule(ruleItem, IP) { ruleItem.draftIpList.forEach(function (existedRule, index) { if (existedRule == IP) { ruleItem.draftIpList.splice(index, 1); } }) } function removeRuleLabel(ruleItem, Label) { ruleItem.draftLabelList.forEach(function (existedRule, index) { if (existedRule == Label) { ruleItem.draftLabelList.splice(index, 1); } }) } function completeEditItem(branch) { scope.completeEditBtnDisable = true; if (!branch.editingRuleItem.clientAppId) { toastr.error($translate.instant('RulesModal.GrayscaleAppIdCanNotBeNull')); scope.completeEditBtnDisable = false; return; } if (branch.editingRuleItem.isNew && branch.rules && branch.rules.ruleItems) { var errorRuleItem = false; branch.rules.ruleItems.forEach(function (ruleItem) { if (ruleItem.clientAppId == branch.editingRuleItem.clientAppId) { toastr.error($translate.instant('RulesModal.AppIdExistsRule', { appId: branch.editingRuleItem.clientAppId })); errorRuleItem = true; } }); if (errorRuleItem) { scope.completeEditBtnDisable = false; return; } } if (!branch.editingRuleItem.ApplyToAllInstances) { if ((branch.editingRuleItem.draftIpList.length == 0)&&(branch.editingRuleItem.draftLabelList.length == 0)) { toastr.error($translate.instant('RulesModal.RuleListCanNotBeNull')); scope.completeEditBtnDisable = false; return; } else { branch.editingRuleItem.clientIpList = branch.editingRuleItem.draftIpList; branch.editingRuleItem.clientLabelList = branch.editingRuleItem.draftLabelList; } } else { branch.editingRuleItem.clientIpList = ['*']; branch.editingRuleItem.clientLabelList = ['*']; } if (!branch.rules) { branch.rules = { appId: scope.appId, clusterName: scope.cluster, namespaceName: branch.baseInfo.namespaceName, branchName: branch.baseInfo.clusterName }; } if (!branch.rules.ruleItems) { branch.rules.ruleItems = []; } if (branch.editingRuleItem.isNew) { branch.rules.ruleItems.push(branch.editingRuleItem); } branch.editingRuleItem = undefined; scope.toAddIPs = ''; scope.toAddLabels = ''; AppUtil.hideModal('#rulesModal'); EventManager.emit(EventManager.EventType.UPDATE_GRAY_RELEASE_RULES, { branch: branch }, branch.baseInfo.namespaceName); scope.completeEditBtnDisable = false; } function cancelEditItem(branch) { branch.editingRuleItem.isEdit = false; branch.editingRuleItem = undefined; scope.toAddIPs = ''; scope.toAddLabels = ''; AppUtil.hideModal('#rulesModal'); } $('#rulesModal').on('shown.bs.modal', function (e) { initSelectIps(); }); function initSelectIps() { scope.selectIps = []; if (!scope.branch.parentNamespace.isPublic || scope.branch.parentNamespace.isLinkedNamespace) { scope.branch.editingRuleItem.clientAppId = scope.branch.baseInfo.appId; } if (!scope.branch.editingRuleItem.clientAppId) { return; } InstanceService.findInstancesByNamespace(scope.appId, scope.env, scope.cluster, scope.branch.baseInfo.namespaceName, scope.branch.editingRuleItem.clientAppId, 0, 2000) .then(function (result) { scope.selectIps = result.content; }); } } } } ================================================ FILE: apollo-portal/src/main/resources/static/scripts/directive/import-namespace-modal-directive.js ================================================ /* * Copyright 2024 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ directive_module.directive('importnamespacemodal', importNamespaceModalDirective); function importNamespaceModalDirective($window, $q, $translate, $http, toastr, AppUtil, EventManager, PermissionService, UserService, NamespaceService) { return { restrict: 'E', templateUrl: AppUtil.prefixPath() + '/views/component/import-namespace-modal.html', transclude: true, replace: true, scope: { env: '=' }, link: function (scope) { scope.doImportNamespace = doImportNamespace; EventManager.subscribe(EventManager.EventType.PRE_IMPORT_NAMESPACE, function (context) { scope.toImportNamespace = context.namespace; showImportNamespaceConfirmDialog(); }); function showImportNamespaceConfirmDialog() { AppUtil.showModal('#importNamespaceModal'); } function doImportNamespace() { var file = document.getElementById("fileUpload").files[0]; if (file == null) { toastr.warning($translate.instant('ConfigExport.UploadFileTip')) return } var toImportNamespace = scope.toImportNamespace; var form = new FormData(); form.append('file', file); $http({ method: 'POST', url: AppUtil.prefixPath() + '/apps/' + toImportNamespace.baseInfo.appId + '/envs/' + scope.env + '/clusters/' + toImportNamespace.baseInfo.clusterName + '/namespaces/' + toImportNamespace.baseInfo.namespaceName + "/items/import", data: form, headers: {'Content-Type': undefined}, transformRequest: angular.identity }).success(function (data) { toastr.success(data, $translate.instant('ConfigExport.ImportSuccess')) //refresh namespace EventManager.emit(EventManager.EventType.REFRESH_NAMESPACE, { namespace: toImportNamespace }); }).error(function (data) { toastr.error(data, $translate.instant('ConfigExport.ImportFailed')) }) } } } } ================================================ FILE: apollo-portal/src/main/resources/static/scripts/directive/item-modal-directive.js ================================================ /* * Copyright 2024 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ directive_module.directive('itemmodal', itemModalDirective); function itemModalDirective($translate, toastr, $sce, AppUtil, EventManager, ConfigService) { return { restrict: 'E', templateUrl: AppUtil.prefixPath() + '/views/component/item-modal.html', transclude: true, replace: true, scope: { appId: '=', env: '=', cluster: '=', toOperationNamespace: '=', item: '=' }, link: function (scope) { var TABLE_VIEW_OPER_TYPE = { CREATE: 'create', UPDATE: 'update' }; scope.doItem = doItem; scope.collectSelectedClusters = collectSelectedClusters; scope.showHiddenChars = showHiddenChars; scope.changeType = changeType; scope.validateItemValue = validateItemValue; scope.formatContent = formatContent; $('#itemModal').on('show.bs.modal', function (e) { scope.showHiddenCharsContext = false; scope.hiddenCharCounter = 0; scope.valueWithHiddenChars = $sce.trustAsHtml(''); }); $("#valueEditor").textareafullscreen(); function validateItemValue() { if (scope.item.type === '1') { //check whether the Number format is correct let regNumber = /-[0-9]+(\\.[0-9]+)?|[0-9]+(\\.[0-9]+)?/; if (regNumber.test(Number(scope.item.value)) === true && !(scope.item.value.trim() === '')) { scope.showNumberError = false; } else { scope.showNumberError = true; } } else if (scope.item.type === '3') { detectJSON(); } else { scope.showNumberError = false; scope.showJsonError = false; } } function doItem() { if (!scope.item.value) { scope.item.value = ""; } if (scope.item.tableViewOperType == TABLE_VIEW_OPER_TYPE.CREATE) { //check key unique var hasRepeatKey = false; scope.toOperationNamespace.items.forEach(function (item) { if (!item.isDeleted && scope.item.key == item.item.key) { toastr.error($translate.instant('ItemModal.KeyExists', { key: scope.item.key })); hasRepeatKey = true; } }); if (hasRepeatKey) { return; } scope.item.addItemBtnDisabled = true; if (scope.toOperationNamespace.isBranch) { ConfigService.create_item(scope.appId, scope.env, scope.toOperationNamespace.baseInfo.clusterName, scope.toOperationNamespace.baseInfo.namespaceName, scope.item).then( function (result) { toastr.success($translate.instant('ItemModal.AddedTips')); scope.item.addItemBtnDisabled = false; AppUtil.hideModal('#itemModal'); EventManager.emit(EventManager.EventType.REFRESH_NAMESPACE, { namespace: scope.toOperationNamespace }); }, function (result) { toastr.error(AppUtil.errorMsg(result), $translate.instant('ItemModal.AddFailed')); scope.item.addItemBtnDisabled = false; }); } else { if (selectedClusters.length == 0) { toastr.error($translate.instant('ItemModal.PleaseChooseCluster')); scope.item.addItemBtnDisabled = false; return; } selectedClusters.forEach(function (cluster) { ConfigService.create_item(scope.appId, cluster.env, cluster.name, scope.toOperationNamespace.baseInfo.namespaceName, scope.item).then( function (result) { scope.item.addItemBtnDisabled = false; AppUtil.hideModal('#itemModal'); toastr.success(cluster.env + " , " + scope.item.key, $translate.instant('ItemModal.AddedTips')); if (cluster.env == scope.env && cluster.name == scope.cluster) { EventManager.emit(EventManager.EventType.REFRESH_NAMESPACE, { namespace: scope.toOperationNamespace }); } }, function (result) { toastr.error(AppUtil.errorMsg(result), $translate.instant('ItemModal.AddFailed')); scope.item.addItemBtnDisabled = false; }); }); } } else { if (!scope.item.comment) { scope.item.comment = ""; } ConfigService.update_item(scope.appId, scope.env, scope.toOperationNamespace.baseInfo.clusterName, scope.toOperationNamespace.baseInfo.namespaceName, scope.item).then( function (result) { EventManager.emit(EventManager.EventType.REFRESH_NAMESPACE, { namespace: scope.toOperationNamespace }); AppUtil.hideModal('#itemModal'); toastr.success($translate.instant('ItemModal.ModifiedTips')); }, function (result) { toastr.error(AppUtil.errorMsg(result), $translate.instant('ItemModal.ModifyFailed')); }); } } var selectedClusters = []; function collectSelectedClusters(data) { selectedClusters = data; } function changeType() { scope.showNumberError = false; scope.showJsonError = false; if (scope.item.type === '2') { scope.item.lastValue = scope.item.value; scope.item.value = 'false'; } else { if (scope.item.lastType === '2') { scope.item.value = scope.item.lastValue; } else { // switch between 'String' 'Number' 'Json', the value is not changed. } } scope.item.lastType = scope.item.type; validateItemValue(); } function detectJSON() { var value = scope.item.value; if (!value) { scope.showJsonError = true; return; } try { JSON.parse(value); scope.showJsonError = false; } catch(e) { scope.showJsonError = true; } } function showHiddenChars() { var value = scope.item.value; if (!value) { return; } var hiddenCharCounter = 0, valueWithHiddenChars = _.escape(value); for (var i = 0; i < value.length; i++) { var c = value[i]; if (isHiddenChar(c)) { valueWithHiddenChars = valueWithHiddenChars.replace(c, viewHiddenChar); hiddenCharCounter++; } } scope.showHiddenCharsContext = true; scope.hiddenCharCounter = hiddenCharCounter; scope.valueWithHiddenChars = $sce.trustAsHtml(valueWithHiddenChars); } function isHiddenChar(c) { return c == '\t' || c == '\n' || c == ' ' || c == ','; } function viewHiddenChar(c) { if (c == '\t') { return '#' + $translate.instant('ItemModal.Tabs') + '#'; } else if (c == '\n') { return '#' + $translate.instant('ItemModal.NewLine') + '#'; } else if (c == ' ') { return '#' + $translate.instant('ItemModal.Space') + '#'; } else if (c == ',') { return '#' + $translate.instant('ItemModal.ChineseComma') + '#'; } } // 格式化 function formatContent() { if (scope.showJsonError) { return; } var raw = scope.item.value; if (scope.item.type === '3') { if (AppUtil.hasDuplicateKeys(raw)) { toastr.warning($translate.instant('ItemModal.JsonDuplicateKeyWarning')); return; } scope.item.value = JSON.stringify(JSON.parse(raw), null, 4); } } } } } ================================================ FILE: apollo-portal/src/main/resources/static/scripts/directive/merge-and-publish-modal-directive.js ================================================ /* * Copyright 2024 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ directive_module.directive('mergeandpublishmodal', mergeAndPublishDirective); function mergeAndPublishDirective(AppUtil, EventManager) { return { restrict: 'E', templateUrl: AppUtil.prefixPath() + '/views/component/merge-and-publish-modal.html', transclude: true, replace: true, scope: { appId: '=', env: '=', cluster: '=' }, link: function (scope) { scope.showReleaseModal = showReleaseModal; EventManager.subscribe(EventManager.EventType.MERGE_AND_PUBLISH_NAMESPACE, function (context) { var branch = context.branch; scope.toReleaseNamespace = branch; scope.toDeleteBranch = branch; scope.isEmergencyPublish = context.isEmergencyPublish ? context.isEmergencyPublish : false; var branchStatusMerge = 2; branch.branchStatus = branchStatusMerge; branch.mergeAndPublish = true; AppUtil.showModal('#mergeAndPublishModal'); }); function showReleaseModal() { EventManager.emit(EventManager.EventType.PUBLISH_NAMESPACE, { namespace: scope.toReleaseNamespace, isEmergencyPublish: scope.isEmergencyPublish }); } } } } ================================================ FILE: apollo-portal/src/main/resources/static/scripts/directive/namespace-panel-directive.js ================================================ /* * Copyright 2024 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ directive_module.directive('apollonspanel', directive); function directive($window, $translate, toastr, AppUtil, EventManager, PermissionService, NamespaceLockService, UserService, CommitService, ReleaseService, InstanceService, NamespaceBranchService, ConfigService) { return { restrict: 'E', templateUrl: AppUtil.prefixPath() + '/views/component/namespace-panel.html', transclude: true, replace: true, scope: { namespace: '=', appId: '=', env: '=', cluster: '=', user: '=', lockCheck: '=', createItem: '=', editItem: '=', preDeleteItem: '=', preRevokeItem: '=', showText: '=', showNoModifyPermissionDialog: '=', preCreateBranch: '=', preDeleteBranch: '=', showMergeAndPublishGrayTips: '=', showBody: "=?", lazyLoad: "=?" }, link: function (scope) { //constants var namespace_view_type = { TEXT: 'text', TABLE: 'table', HISTORY: 'history', INSTANCE: 'instance', RULE: 'rule' }; var namespace_instance_view_type = { LATEST_RELEASE: 'latest_release', NOT_LATEST_RELEASE: 'not_latest_release', ALL: 'all' }; var operate_branch_storage_key = 'OperateBranch'; var default_text_editor_min_lines = 10; var default_text_editor_max_lines = 20; var fullscreen_text_editor_min_lines = 25; var text_editors = []; var on_window_resize = function () { refreshTextEditorLayout(scope.namespace.isTextFullscreen); }; var fullscreen_change_events = ['fullscreenchange', 'webkitfullscreenchange', 'mozfullscreenchange', 'MSFullscreenChange']; scope.refreshNamespace = refreshNamespace; scope.switchView = switchView; scope.toggleItemSearchInput = toggleItemSearchInput; scope.toggleHistorySearchInput = toggleHistorySearchInput; scope.searchItems = searchItems; scope.resetSearchItems = resetSearchItems; scope.searchHistory = searchHistory; scope.loadCommitHistory = loadCommitHistory; scope.toggleTextEditStatus = toggleTextEditStatus; scope.toggleTextFullscreen = toggleTextFullscreen; scope.goToSyncPage = goToSyncPage; scope.goToDiffPage = goToDiffPage; scope.modifyByText = modifyByText; scope.syntaxCheck = syntaxCheck; scope.goToParentAppConfigPage = goToParentAppConfigPage; scope.switchInstanceViewType = switchInstanceViewType; scope.switchBranch = switchBranch; scope.loadInstanceInfo = loadInstanceInfo; scope.refreshInstancesInfo = refreshInstancesInfo; scope.deleteRuleItem = deleteRuleItem; scope.rollback = rollback; scope.publish = publish; scope.mergeAndPublish = mergeAndPublish; scope.addRuleItem = addRuleItem; scope.editRuleItem = editRuleItem; scope.formatContent = formatContent; scope.deleteNamespace = deleteNamespace; scope.exportNamespace = exportNamespace; scope.importNamespace = importNamespace; var subscriberId = EventManager.subscribe(EventManager.EventType.UPDATE_GRAY_RELEASE_RULES, function (context) { useRules(context.branch); }, scope.namespace.baseInfo.namespaceName); fullscreen_change_events.forEach(function (event_name) { $window.document.addEventListener(event_name, syncFullscreenStatus); }); $window.addEventListener('resize', on_window_resize); scope.$on('$destroy', function () { EventManager.unsubscribe(EventManager.EventType.UPDATE_GRAY_RELEASE_RULES, subscriberId, scope.namespace.baseInfo.namespaceName); fullscreen_change_events.forEach(function (event_name) { $window.document.removeEventListener(event_name, syncFullscreenStatus); }); $window.removeEventListener('resize', on_window_resize); if (getFullscreenElement() == getTextEditorContainer(scope.namespace)) { exitFullscreen(); } text_editors = []; }); function getTextEditorContainerId(namespace) { if (!namespace || !namespace.id) { return ''; } return 'namespaceTextEditor' + namespace.id; } function getTextEditorContainer(namespace) { return $window.document.getElementById(getTextEditorContainerId(namespace)); } function getFullscreenElement() { return $window.document.fullscreenElement || $window.document.webkitFullscreenElement || $window.document.mozFullScreenElement || $window.document.msFullscreenElement; } function requestFullscreen(element) { var request_method = element.requestFullscreen || element.webkitRequestFullscreen || element.mozRequestFullScreen || element.msRequestFullscreen; if (!request_method) { return null; } return request_method.call(element); } function exitFullscreen() { var exit_method = $window.document.exitFullscreen || $window.document.webkitExitFullscreen || $window.document.mozCancelFullScreen || $window.document.msExitFullscreen; if (!exit_method) { return null; } return exit_method.call($window.document); } function isElementVisible(element) { return !!(element && (element.offsetWidth || element.offsetHeight || element.getClientRects().length)); } function getVisibleTextEditorCount(namespace) { var text_editor_container = getTextEditorContainer(namespace); if (!text_editor_container || !text_editor_container.querySelectorAll) { return 1; } var visible_editor_count = 0; var ace_editors = text_editor_container.querySelectorAll('.ace_editor'); Array.prototype.forEach.call(ace_editors, function (ace_editor) { if (isElementVisible(ace_editor)) { visible_editor_count += 1; } }); return visible_editor_count > 0 ? visible_editor_count : 1; } function getFullscreenTextEditorLines(editor) { var line_height = editor && editor.renderer && editor.renderer.lineHeight ? editor.renderer.lineHeight : 16; var visible_editor_count = getVisibleTextEditorCount(scope.namespace); var text_editor_container = getTextEditorContainer(scope.namespace); var available_height = $window.innerHeight; if (text_editor_container && text_editor_container.clientHeight) { available_height = text_editor_container.clientHeight; } available_height = Math.max(400, available_height - 24); var lines_per_editor = Math.floor(available_height / visible_editor_count / line_height); return Math.max(fullscreen_text_editor_min_lines, lines_per_editor); } function applyTextEditorLayout(editor, is_text_fullscreen) { if (!editor || !editor.setOptions) { return; } var current_fullscreen_status = scope.namespace.isTextFullscreen; if (typeof is_text_fullscreen == 'boolean') { current_fullscreen_status = is_text_fullscreen; } var min_lines = default_text_editor_min_lines; var max_lines = default_text_editor_max_lines; if (current_fullscreen_status) { min_lines = getFullscreenTextEditorLines(editor); max_lines = min_lines; } editor.setOptions({ fontSize: 13, minLines: min_lines, maxLines: max_lines }); editor.resize(true); } function refreshTextEditorLayout(is_text_fullscreen) { text_editors.forEach(function (editor) { applyTextEditorLayout(editor, is_text_fullscreen); }); } function syncFullscreenStatus() { if (scope.$$destroyed) { return; } var is_current_namespace_fullscreen = getFullscreenElement() == getTextEditorContainer(scope.namespace); if (scope.namespace.isTextFullscreen !== is_current_namespace_fullscreen) { if (scope.$$phase) { scope.namespace.isTextFullscreen = is_current_namespace_fullscreen; } else { scope.$applyAsync(function () { scope.namespace.isTextFullscreen = is_current_namespace_fullscreen; }); } } refreshTextEditorLayout(is_current_namespace_fullscreen); if (is_current_namespace_fullscreen) { $window.setTimeout(function () { refreshTextEditorLayout(true); }, 50); } } preInit(scope.namespace); if (!scope.lazyLoad || scope.namespace.initialized) { init(); } function preInit(namespace) { scope.showNamespaceBody = false; namespace.isLinkedNamespace = namespace.isPublic ? namespace.parentAppId != namespace.baseInfo.appId : false; //namespace view name hide suffix namespace.viewName = namespace.baseInfo.namespaceName.replace(".xml", "").replace( ".properties", "").replace(".json", "").replace(".yml", "") .replace(".yaml", "").replace(".txt", ""); } function init() { initNamespace(scope.namespace); initOther(); scope.namespace.initialized = true; } function refreshNamespace() { EventManager.emit(EventManager.EventType.REFRESH_NAMESPACE, { namespace: scope.namespace }); } function initNamespace(namespace, viewType) { namespace.hasBranch = false; namespace.isBranch = false; namespace.displayControl = { currentOperateBranch: 'master', showSearchInput: namespace.showSearchItemInput, searchItemKey: namespace.searchItemKey, showHistorySearchInput: false, show: scope.showBody }; scope.showNamespaceBody = namespace.showNamespaceBody ? true : scope.showBody; namespace.viewItems = namespace.items; namespace.isPropertiesFormat = namespace.format == 'properties'; namespace.isSyntaxCheckable = namespace.format == 'yaml' || namespace.format == 'yml'; namespace.isTextEditing = false; namespace.isTextFullscreen = false; namespace.instanceViewType = namespace_instance_view_type.LATEST_RELEASE; namespace.latestReleaseInstancesPage = 0; namespace.allInstances = []; namespace.allInstancesPage = 0; namespace.commitChangeBtnDisabled = false; generateNamespaceId(namespace); initNamespaceBranch(namespace); initNamespaceViewName(namespace); initNamespaceLock(namespace); initNamespaceInstancesCount(namespace); initPermission(namespace); initLinkedNamespace(namespace); loadInstanceInfo(namespace); initSearchItemInput(namespace); function initNamespaceBranch(namespace) { NamespaceBranchService.findNamespaceBranch(scope.appId, scope.env, namespace.baseInfo.clusterName, namespace.baseInfo.namespaceName) .then(function (result) { if (!result.baseInfo) { return; } //namespace has branch namespace.hasBranch = true; namespace.branchName = result.baseInfo.clusterName; //init branch namespace.branch = result; namespace.branch.isBranch = true; namespace.branch.parentNamespace = namespace; namespace.branch.viewType = namespace_view_type.TABLE; namespace.branch.isPropertiesFormat = namespace.format == 'properties'; namespace.branch.allInstances = [];//master namespace all instances namespace.branch.latestReleaseInstances = []; namespace.branch.latestReleaseInstancesPage = 0; namespace.branch.instanceViewType = namespace_instance_view_type.LATEST_RELEASE; namespace.branch.hasLoadInstances = false; namespace.branch.displayControl = { show: true }; generateNamespaceId(namespace.branch); initBranchItems(namespace.branch); initRules(namespace.branch); loadInstanceInfo(namespace.branch); initNamespaceLock(namespace.branch); initPermission(namespace); initUserOperateBranchScene(namespace); }); function initBranchItems(branch) { branch.masterItems = []; branch.branchItems = []; var masterItemsMap = {}; branch.parentNamespace.items.forEach(function (item) { if (item.item.key) { masterItemsMap[item.item.key] = item; } }); var branchItemsMap = {}; var itemModifiedCnt = 0; branch.items.forEach(function (item) { var key = item.item.key; var masterItem = masterItemsMap[key]; //modify master item and set item's masterReleaseValue if (masterItem) { item.masterItemExists = true; if (masterItem.isModified) { item.masterReleaseValue = masterItem.oldValue; } else { item.masterReleaseValue = masterItem.item.value; } } else {//delete branch item item.masterItemExists = false; } //delete master item. ignore if (item.isDeleted && masterItem) { if (item.masterReleaseValue != item.oldValue) { itemModifiedCnt++; branch.branchItems.push(item); } } else {//branch's item branchItemsMap[key] = item; if (item.isModified) { itemModifiedCnt++; } branch.branchItems.push(item); } }); branch.itemModifiedCnt = itemModifiedCnt; branch.parentNamespace.items.forEach(function (item) { if (item.item.key) { if (!branchItemsMap[item.item.key]) { branch.masterItems.push(item); } else { item.hasBranchValue = true; } } }) } } function generateNamespaceId(namespace) { namespace.id = Math.random().toString(36).substr(2); } function initPermission(namespace) { PermissionService.has_modify_namespace_permission( scope.appId, namespace.baseInfo.namespaceName) .then(function (result) { if (!result.hasPermission) { PermissionService.has_modify_namespace_env_permission( scope.appId, scope.env, namespace.baseInfo.namespaceName ) .then(function (result) { //branch has same permission namespace.hasModifyPermission = namespace.hasModifyPermission || result.hasPermission; if (namespace.branch) { namespace.branch.hasModifyPermission = namespace.branch.hasModifyPermission || result.hasPermission; } }); } else { //branch has same permission namespace.hasModifyPermission = namespace.hasModifyPermission || result.hasPermission; if (namespace.branch) { namespace.branch.hasModifyPermission = namespace.branch.hasModifyPermission || result.hasPermission; } } }); PermissionService.has_modify_cluster_ns_permission( scope.appId, scope.env, scope.cluster ).then(function (result) { if (result.hasPermission) { namespace.hasModifyPermission = namespace.hasModifyPermission || result.hasPermission; if (namespace.branch) { namespace.branch.hasModifyPermission = namespace.branch.hasModifyPermission || result.hasPermission; } } }); PermissionService.has_release_namespace_permission( scope.appId, namespace.baseInfo.namespaceName) .then(function (result) { if (!result.hasPermission) { PermissionService.has_release_namespace_env_permission( scope.appId, scope.env, namespace.baseInfo.namespaceName ) .then(function (result) { //branch has same permission namespace.hasReleasePermission ||= result.hasPermission; if (namespace.branch) { namespace.branch.hasReleasePermission ||= result.hasPermission; } }); } else { //branch has same permission namespace.hasReleasePermission ||= result.hasPermission; if (namespace.branch) { namespace.branch.hasReleasePermission ||= result.hasPermission; } } }); PermissionService.has_release_cluster_ns_permission( scope.appId, scope.env, scope.cluster ).then(function (result) { if (result.hasPermission) { namespace.hasReleasePermission ||= result.hasPermission; if (namespace.branch) { namespace.branch.hasReleasePermission ||= result.hasPermission; } } }); } function initLinkedNamespace(namespace) { if (!namespace.isPublic || !namespace.isLinkedNamespace) { return; } //load public namespace ConfigService.load_public_namespace_for_associated_namespace(scope.env, scope.appId, scope.cluster, namespace.baseInfo.namespaceName) .then(function (result) { var publicNamespace = result; namespace.publicNamespace = publicNamespace; var linkNamespaceItemKeys = []; namespace.items.forEach(function (item) { var key = item.item.key; linkNamespaceItemKeys.push(key); }); publicNamespace.viewItems = []; publicNamespace.items.forEach(function (item) { var key = item.item.key; if (key) { publicNamespace.viewItems.push(item); } item.covered = linkNamespaceItemKeys.indexOf(key) >= 0; if (item.isModified || item.isDeleted) { publicNamespace.isModified = true; } else if (key) { publicNamespace.hasPublishedItem = true; } }); publicNamespace.isPropertiesFormat = publicNamespace.format == 'properties'; loadParentNamespaceText(namespace); }); } function loadParentNamespaceText(namespace){ namespace.publicNamespaceText = ""; if(namespace.isLinkedNamespace) { namespace.publicNamespaceText = parseModel2Text(namespace.publicNamespace) } } function initNamespaceViewName(namespace) { if (!viewType) { if (namespace.isPropertiesFormat) { switchView(namespace, namespace_view_type.TABLE); } else { switchView(namespace, namespace_view_type.TEXT); } } else if (viewType == namespace_view_type.TABLE) { namespace.viewType = namespace_view_type.TABLE; } } function initNamespaceLock(namespace) { NamespaceLockService.get_namespace_lock(scope.appId, scope.env, namespace.baseInfo.clusterName, namespace.baseInfo.namespaceName) .then(function (result) { namespace.lockOwner = result.lockOwner; namespace.isEmergencyPublishAllowed = result.isEmergencyPublishAllowed; }); } function initUserOperateBranchScene(namespace) { var operateBranchStorage = JSON.parse(localStorage.getItem(operate_branch_storage_key)); var namespaceId = [scope.appId, scope.env, scope.cluster, namespace.baseInfo.namespaceName].join( "+"); if (!operateBranchStorage) { operateBranchStorage = {}; } if (!operateBranchStorage[namespaceId]) { operateBranchStorage[namespaceId] = namespace.branchName; } localStorage.setItem(operate_branch_storage_key, JSON.stringify(operateBranchStorage)); switchBranch(operateBranchStorage[namespaceId], false); } function initSearchItemInput(namespace) { if (namespace.displayControl.searchItemKey) { namespace.searchKey = namespace.displayControl.searchItemKey; searchItems(namespace); } } } function initNamespaceInstancesCount(namespace) { InstanceService.getInstanceCountByNamespace(scope.appId, scope.env, scope.cluster, namespace.baseInfo.namespaceName) .then(function (result) { namespace.instancesCount = result.num; }) } function initOther() { UserService.load_user().then(function (result) { scope.currentUser = result.userId; }); PermissionService.has_assign_user_permission(scope.appId) .then(function (result) { scope.hasAssignUserPermission = result.hasPermission; }, function (result) { }); } function switchBranch(branchName, forceShowBody) { if (branchName != 'master') { initRules(scope.namespace.branch); } if (forceShowBody) { scope.showNamespaceBody = true; } scope.namespace.displayControl.currentOperateBranch = branchName; //save to local storage var operateBranchStorage = JSON.parse(localStorage.getItem(operate_branch_storage_key)); if (!operateBranchStorage) { return; } var namespaceId = [scope.appId, scope.env, scope.cluster, scope.namespace.baseInfo.namespaceName].join( "+"); operateBranchStorage[namespaceId] = branchName; localStorage.setItem(operate_branch_storage_key, JSON.stringify(operateBranchStorage)); } function switchView(namespace, viewType) { if (viewType != namespace_view_type.TEXT && getFullscreenElement() == getTextEditorContainer(namespace)) { exitFullscreen(); } namespace.viewType = viewType; if (namespace_view_type.TEXT == viewType) { namespace.text = parseModel2Text(namespace); } else if (namespace_view_type.TABLE == viewType) { } else if (namespace_view_type.HISTORY == viewType) { loadCommitHistory(namespace); } else if (namespace_view_type.INSTANCE == viewType) { refreshInstancesInfo(namespace); } } function switchInstanceViewType(namespace, type) { namespace.instanceViewType = type; loadInstanceInfo(namespace); } function loadCommitHistory(namespace) { if (!namespace.commits) { namespace.commits = []; namespace.commitPage = 0; } var size = 10; CommitService.find_commits(scope.appId, scope.env, namespace.baseInfo.clusterName, namespace.baseInfo.namespaceName, namespace.HistorySearchKey, namespace.commitPage, size) .then(function (result) { if (result.length < size) { namespace.hasLoadAllCommit = true; } for (var i = 0; i < result.length; i++) { //to json result[i].changeSets = JSON.parse(result[i].changeSets); namespace.commits.push(result[i]); } namespace.commitPage += 1; }, function (result) { toastr.error(AppUtil.errorMsg(result), $translate.instant('ApolloNsPanel.LoadingHistoryError')); }); } function loadInstanceInfo(namespace) { var size = 20; if (namespace.isBranch) { size = 2000; } var type = namespace.instanceViewType; if (namespace_instance_view_type.LATEST_RELEASE == type) { if (!namespace.latestRelease) { ReleaseService.findLatestActiveRelease(scope.appId, scope.env, namespace.baseInfo.clusterName, namespace.baseInfo.namespaceName) .then(function (result) { namespace.isLatestReleaseLoaded = true; if (!result) { namespace.latestReleaseInstances = {}; namespace.latestReleaseInstances.total = 0; return; } namespace.latestRelease = result; InstanceService.findInstancesByRelease(scope.env, namespace.latestRelease.id, namespace.latestReleaseInstancesPage, size) .then(function (result) { namespace.latestReleaseInstances = result; namespace.latestReleaseInstancesPage++; }) }); } else { InstanceService.findInstancesByRelease(scope.env, namespace.latestRelease.id, namespace.latestReleaseInstancesPage, size) .then(function (result) { if (result && result.content.length) { namespace.latestReleaseInstancesPage++; result.content.forEach(function (instance) { namespace.latestReleaseInstances.content.push( instance); }) } }) } } else if (namespace_instance_view_type.NOT_LATEST_RELEASE == type) { if (!namespace.latestRelease) { return; } InstanceService.findByReleasesNotIn(scope.appId, scope.env, scope.cluster, namespace.baseInfo.namespaceName, namespace.latestRelease.id) .then(function (result) { if (!result || result.length == 0) { return } var groupedInstances = {}, notLatestReleases = []; result.forEach(function (instance) { var configs = instance.configs; if (configs && configs.length > 0) { configs.forEach(function (instanceConfig) { var release = instanceConfig.release; //filter dirty data if (!release) { return; } if (!groupedInstances[release.id]) { groupedInstances[release.id] = []; notLatestReleases.push(release); } groupedInstances[release.id].push(instance); }) } }); namespace.notLatestReleases = notLatestReleases; namespace.notLatestReleaseInstances = groupedInstances; }) } else { InstanceService.findInstancesByNamespace(scope.appId, scope.env, scope.cluster, namespace.baseInfo.namespaceName, '', namespace.allInstancesPage) .then(function (result) { if (result && result.content.length) { namespace.allInstancesPage++; result.content.forEach(function (instance) { namespace.allInstances.push(instance); }) } }); } } function refreshInstancesInfo(namespace) { namespace.instanceViewType = namespace_instance_view_type.LATEST_RELEASE; namespace.latestReleaseInstancesPage = 0; namespace.latestReleaseInstances = []; namespace.latestRelease = undefined; if (!namespace.isBranch) { namespace.notLatestReleaseNames = []; namespace.notLatestReleaseInstances = {}; namespace.allInstancesPage = 0; namespace.allInstances = []; } initNamespaceInstancesCount(namespace); loadInstanceInfo(namespace); } function initRules(branch) { NamespaceBranchService.findBranchGrayRules(scope.appId, scope.env, scope.cluster, scope.namespace.baseInfo.namespaceName, branch.baseInfo.clusterName) .then(function (result) { if (result.appId) { branch.rules = result; } }, function (result) { toastr.error(AppUtil.errorMsg(result), $translate.instant('ApolloNsPanel.LoadingGrayscaleError')); }); } function addRuleItem(branch) { var newRuleItem = { clientAppId: !branch.parentNamespace.isPublic ? branch.baseInfo.appId : '', clientIpList: [], draftIpList: [], clientLabelList: [], draftLabelList: [], isNew: true }; branch.editingRuleItem = newRuleItem; EventManager.emit(EventManager.EventType.EDIT_GRAY_RELEASE_RULES, { branch: branch }); } function editRuleItem(branch, ruleItem) { ruleItem.isNew = false; ruleItem.draftIpList = _.clone(ruleItem.clientIpList) || []; ruleItem.draftLabelList = _.clone(ruleItem.clientLabelList) || []; branch.editingRuleItem = ruleItem; EventManager.emit(EventManager.EventType.EDIT_GRAY_RELEASE_RULES, { branch: branch }); } function deleteRuleItem(branch, ruleItem) { branch.rules.ruleItems.forEach(function (item, index) { if (item.clientAppId == ruleItem.clientAppId) { branch.rules.ruleItems.splice(index, 1); toastr.success($translate.instant('ApolloNsPanel.Deleted')); } }); useRules(branch); } function useRules(branch) { NamespaceBranchService.updateBranchGrayRules(scope.appId, scope.env, scope.cluster, scope.namespace.baseInfo.namespaceName, branch.baseInfo.clusterName, branch.rules ) .then(function (result) { toastr.success($translate.instant('ApolloNsPanel.GrayscaleModified')); //show tips if branch has not release configs if (branch.itemModifiedCnt) { AppUtil.showModal("#updateRuleTips"); } setTimeout(function () { refreshInstancesInfo(branch); }, 1500); }, function (result) { AppUtil.showErrorMsg(result, $translate.instant('ApolloNsPanel.GrayscaleModifyFailed')); }) } function toggleTextEditStatus(namespace) { if (!scope.lockCheck(namespace)) { return; } namespace.isTextEditing = !namespace.isTextEditing; if (namespace.isTextEditing) {//切换为编辑状态 namespace.committed = false; namespace.backupText = namespace.text; namespace.editText = parseModel2Text(namespace); } else { if (!namespace.committed) {//取消编辑,则复原 namespace.text = namespace.backupText; } } } function toggleTextFullscreen(namespace) { var text_editor_container = getTextEditorContainer(namespace); if (!text_editor_container) { return; } var fullscreen_request; if (getFullscreenElement() == text_editor_container) { fullscreen_request = exitFullscreen(); } else { fullscreen_request = requestFullscreen(text_editor_container); } if (fullscreen_request && typeof fullscreen_request.catch == 'function') { fullscreen_request.catch(function () { syncFullscreenStatus(); }); } setTimeout(syncFullscreenStatus, 0); } // 格式化 function formatContent(namespace) { try { if (namespace.format === 'json') { if (AppUtil.hasDuplicateKeys(namespace.editText)) { toastr.warning($translate.instant('ApolloNsPanel.JsonDuplicateKeyWarning')); return; } namespace.editText = JSON.stringify(JSON.parse(namespace.editText), null, 4); } } catch (e) { toastr.error('format content failed: ' + e.message); } } function goToSyncPage(namespace) { if (!scope.lockCheck(namespace)) { return false; } $window.location.href = AppUtil.prefixPath() + "/config/sync.html?#/appid=" + scope.appId + "&env=" + scope.env + "&clusterName=" + scope.cluster + "&namespaceName=" + namespace.baseInfo.namespaceName; } function goToDiffPage(namespace) { $window.location.href = AppUtil.prefixPath() + "/config/diff.html?#/appid=" + scope.appId + "&env=" + scope.env + "&clusterName=" + scope.cluster + "&namespaceName=" + namespace.baseInfo.namespaceName; } function modifyByText(namespace) { var model = { configText: namespace.editText, namespaceId: namespace.baseInfo.id, format: namespace.format }; //prevent repeat submit if (namespace.commitChangeBtnDisabled) { return; } namespace.commitChangeBtnDisabled = true; ConfigService.modify_items(scope.appId, scope.env, scope.cluster, namespace.baseInfo.namespaceName, model).then( function (result) { toastr.success($translate.instant('ApolloNsPanel.ModifiedTips')); //refresh all namespace items EventManager.emit(EventManager.EventType.REFRESH_NAMESPACE, { namespace: namespace }); return true; }, function (result) { toastr.error(AppUtil.errorMsg(result), $translate.instant('ApolloNsPanel.ModifyFailed')); namespace.commitChangeBtnDisabled = false; return false; } ); namespace.committed = true; } function syntaxCheck(namespace) { var model = { configText: namespace.editText, namespaceId: namespace.baseInfo.id, format: namespace.format }; ConfigService.syntax_check_text(scope.appId, scope.env, scope.cluster, namespace.baseInfo.namespaceName, model).then( function (result) { toastr.success($translate.instant('ApolloNsPanel.GrammarIsRight')); }, function (result) { EventManager.emit(EventManager.EventType.SYNTAX_CHECK_TEXT_FAILED, { syntaxCheckMessage: AppUtil.pureErrorMsg(result) }); } ); } function goToParentAppConfigPage(namespace) { $window.location.href = AppUtil.prefixPath() + "/config.html?#/appid=" + namespace.parentAppId; $window.location.reload(); } function parseModel2Text(namespace) { if (namespace.items.length == 0) { namespace.itemCnt = 0; return ""; } //文件模式 if (!namespace.isPropertiesFormat) { return parseNotPropertiesText(namespace); } else { return parsePropertiesText(namespace); } } function parseNotPropertiesText(namespace) { var text = namespace.items[0].item.value; var lineNum = text.split("\n").length; namespace.itemCnt = lineNum; return text; } function parsePropertiesText(namespace) { var result = ""; var itemCnt = 0; namespace.items.forEach(function (item) { //deleted key if (item.isDeleted) { return; } if (item.item.key) { //use string \n to display as new line var itemValue = item.item.value.replace(/\n/g, "\\n"); result += item.item.key + " = " + itemValue + "\n"; } else { result += item.item.comment + "\n"; } itemCnt++; }); namespace.itemCnt = itemCnt; return result; } function toggleItemSearchInput(namespace) { namespace.displayControl.showSearchInput = !namespace.displayControl.showSearchInput; } function searchItems(namespace) { var searchKey = namespace.searchKey.toLowerCase(); var items = []; namespace.items.forEach(function (item) { var key = item.item.key; if (key && key.toLowerCase().indexOf(searchKey) >= 0) { items.push(item); } }); namespace.viewItems = items; } function resetSearchItems(namespace) { namespace.searchKey = ''; searchItems(namespace); } function toggleHistorySearchInput(namespace) { namespace.displayControl.showHistorySearchInput = !namespace.displayControl.showHistorySearchInput; } function searchHistory(namespace) { namespace.commits = []; namespace.commitPage = 0; namespace.hasLoadAllCommit = false; var size = 10; CommitService.find_commits(scope.appId, scope.env, namespace.baseInfo.clusterName, namespace.baseInfo.namespaceName, namespace.HistorySearchKey, namespace.commitPage, size) .then(function (result) { if (result.length < size) { namespace.hasLoadAllCommit = true; } for (var i = 0; i < result.length; i++) { //to json result[i].changeSets = JSON.parse(result[i].changeSets); namespace.commits.push(result[i]); } namespace.commitPage++ }, function (result) { toastr.error(AppUtil.errorMsg(result), $translate.instant('ApolloNsPanel.LoadingHistoryError')); }); } //normal release and gray release function publish(namespace) { if (!namespace.hasReleasePermission) { AppUtil.showModal('#releaseNoPermissionDialog'); return; } else if (namespace.lockOwner && scope.user == namespace.lockOwner) { //can not publish if config modified by himself EventManager.emit(EventManager.EventType.PUBLISH_DENY, { namespace: namespace, mergeAndPublish: false }); return; } if (namespace.isBranch) { namespace.mergeAndPublish = false; } EventManager.emit(EventManager.EventType.PUBLISH_NAMESPACE, { namespace: namespace }); } function mergeAndPublish(branch) { var parentNamespace = branch.parentNamespace; if (!parentNamespace.hasReleasePermission) { AppUtil.showModal('#releaseNoPermissionDialog'); } else if (parentNamespace.itemModifiedCnt > 0) { AppUtil.showModal('#mergeAndReleaseDenyDialog'); } else if (branch.lockOwner && scope.user == branch.lockOwner) { EventManager.emit(EventManager.EventType.PUBLISH_DENY, { namespace: branch, mergeAndPublish: true }); } else { EventManager.emit(EventManager.EventType.MERGE_AND_PUBLISH_NAMESPACE, { branch: branch }); } } function rollback(namespace) { EventManager.emit(EventManager.EventType.PRE_ROLLBACK_NAMESPACE, { namespace: namespace }); } function deleteNamespace(namespace) { EventManager.emit(EventManager.EventType.PRE_DELETE_NAMESPACE, { namespace: namespace }); } function exportNamespace(namespace) { $window.location.href = AppUtil.prefixPath() + '/apps/' + scope.appId + "/envs/" + scope.env + "/clusters/" + scope.cluster + "/namespaces/" + namespace.baseInfo.namespaceName + "/items/export" } function importNamespace(namespace) { EventManager.emit(EventManager.EventType.PRE_IMPORT_NAMESPACE, { namespace: namespace }); } //theme: https://github.com/ajaxorg/ace-builds/tree/ba3b91e04a5aa559d56ac70964f9054baa0f4caf/src-min scope.aceConfig = { $blockScrolling: Infinity, showPrintMargin: false, theme: 'eclipse', mode: scope.namespace.format === 'yml' ? 'yaml' : (scope.namespace.format === 'txt' ? undefined : scope.namespace.format), require: ['ace/ext/searchbox'], onLoad: function (_editor) { _editor.$blockScrolling = Infinity; if (text_editors.indexOf(_editor) < 0) { text_editors.push(_editor); } applyTextEditorLayout(_editor); }, onChange: function (e) { if ((e[0].action === 'insert') && (scope.namespace.hasOwnProperty("editText"))) { scope.namespace.editText = e[1].session.getValue(); } } }; setTimeout(function () { scope.namespace.show = true; }, 70); } } } ================================================ FILE: apollo-portal/src/main/resources/static/scripts/directive/open-manage-grant-permission-modal-directive.js ================================================ /* * Copyright 2024 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ directive_module.directive('grantpermissionmodal', grantPermissionModalDirective); function grantPermissionModalDirective($translate, toastr, $sce, AppUtil, EnvService, ConsumerService) { return { restrict: 'E', templateUrl: AppUtil.prefixPath() + '/open/grant-permission-modal.html', transclude: true, replace: true, scope: { consumerRole: '=', assignRoleToConsumer: '=', }, link: function (scope) { scope.initialized = false; scope.envs = []; scope.envsChecked = []; if(!scope.initialized){ initEnv(); } scope.doAssignRoleToConsumer = function () { ConsumerService.assignRoleToConsumer(scope.consumerRole.token, scope.consumerRole.type, scope.consumerRole.appId, scope.consumerRole.namespaceName, scope.envsChecked) .then(function (consumerRoles) { toastr.success($translate.instant('Open.Manage.GrantSuccessfully')); AppUtil.hideModal('#grantPermissionModal'); scope.consumerRole = {} }, function (response) { AppUtil.showErrorMsg(response, $translate.instant('Open.Manage.GrantFailed')); }) } function initEnv() { EnvService.find_all_envs() .then(function (result) { for (let iLoop = 0; iLoop < result.length; iLoop++) { scope.envs.push({ checked: false, env: result[iLoop] }); scope.envsChecked = []; } scope.envsChecked.switchSelect = function (item) { item.checked = !item.checked; scope.envsChecked = []; for (let iLoop = 0; iLoop < scope.envs.length; iLoop++) { const env = scope.envs[iLoop]; if (env.checked) { scope.envsChecked.push(env.env); } } }; }); } } } } ================================================ FILE: apollo-portal/src/main/resources/static/scripts/directive/publish-deny-modal-directive.js ================================================ /* * Copyright 2024 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ directive_module.directive('publishdenymodal', publishDenyDirective); function publishDenyDirective(AppUtil, EventManager) { return { restrict: 'E', templateUrl: AppUtil.prefixPath() + '/views/component/publish-deny-modal.html', transclude: true, replace: true, scope: { env: "=" }, link: function (scope) { var MODAL_ID = "#publishDenyModal"; EventManager.subscribe(EventManager.EventType.PUBLISH_DENY, function (context) { scope.toReleaseNamespace = context.namespace; scope.mergeAndPublish = !!context.mergeAndPublish; AppUtil.showModal(MODAL_ID); }); scope.emergencyPublish = emergencyPublish; function emergencyPublish() { AppUtil.hideModal(MODAL_ID); EventManager.emit(EventManager.EventType.EMERGENCY_PUBLISH, { mergeAndPublish: scope.mergeAndPublish, namespace: scope.toReleaseNamespace }); } } } } ================================================ FILE: apollo-portal/src/main/resources/static/scripts/directive/release-modal-directive.js ================================================ /* * Copyright 2024 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ directive_module.directive('releasemodal', releaseModalDirective); function releaseModalDirective($translate, toastr, AppUtil, EventManager, ReleaseService, NamespaceBranchService) { return { restrict: 'E', templateUrl: AppUtil.prefixPath() + '/views/component/release-modal.html', transclude: true, replace: true, scope: { appId: '=', env: '=', cluster: '=' }, link: function (scope) { scope.switchReleaseChangeViewType = switchReleaseChangeViewType; scope.release = release; scope.releaseBtnDisabled = false; scope.releaseChangeViewType = 'compareWithPublishedValue'; scope.isComparePublished = true; scope.releaseComment = ''; scope.isEmergencyPublish = false; EventManager.subscribe(EventManager.EventType.PUBLISH_NAMESPACE, function (context) { var namespace = context.namespace; scope.toReleaseNamespace = context.namespace; scope.isEmergencyPublish = !!context.isEmergencyPublish; var date = new Date().Format("yyyyMMddhhmmss"); if (namespace.mergeAndPublish) { namespace.releaseTitle = date + "-gray-release-merge-to-master"; } else if (namespace.isBranch) { namespace.releaseTitle = date + "-gray"; } else { namespace.releaseTitle = date + "-release"; } AppUtil.showModal('#releaseModal'); }); function release() { if (scope.toReleaseNamespace.mergeAndPublish) { mergeAndPublish(); } else if (scope.toReleaseNamespace.isBranch) { grayPublish(); } else { publish(); } } function publish() { scope.releaseBtnDisabled = true; ReleaseService.publish(scope.appId, scope.env, scope.toReleaseNamespace.baseInfo.clusterName, scope.toReleaseNamespace.baseInfo.namespaceName, scope.toReleaseNamespace.releaseTitle, scope.releaseComment, scope.isEmergencyPublish).then( function (result) { AppUtil.hideModal('#releaseModal'); toastr.success($translate.instant('ReleaseModal.Published')); scope.releaseBtnDisabled = false; EventManager.emit(EventManager.EventType.REFRESH_NAMESPACE, { namespace: scope.toReleaseNamespace }) }, function (result) { scope.releaseBtnDisabled = false; toastr.error(AppUtil.errorMsg(result), $translate.instant('ReleaseModal.PublishFailed')); } ); } function grayPublish() { scope.releaseBtnDisabled = true; ReleaseService.grayPublish(scope.appId, scope.env, scope.toReleaseNamespace.parentNamespace.baseInfo.clusterName, scope.toReleaseNamespace.baseInfo.namespaceName, scope.toReleaseNamespace.baseInfo.clusterName, scope.toReleaseNamespace.releaseTitle, scope.releaseComment, scope.isEmergencyPublish).then( function (result) { AppUtil.hideModal('#releaseModal'); toastr.success($translate.instant('ReleaseModal.GrayscalePublished')); scope.releaseBtnDisabled = false; //refresh item status for (let index = 0; index < scope.toReleaseNamespace.branchItems.length; index++) { const item = scope.toReleaseNamespace.branchItems[index]; if (item.isDeleted) { scope.toReleaseNamespace.branchItems.splice(index, 1); index--; } else { item.isModified = false; } } //reset namespace status scope.toReleaseNamespace.itemModifiedCnt = 0; scope.toReleaseNamespace.lockOwner = undefined; //check rules if (!scope.toReleaseNamespace.rules || !scope.toReleaseNamespace.rules.ruleItems || !scope.toReleaseNamespace.rules.ruleItems.length) { scope.toReleaseNamespace.viewType = 'rule'; AppUtil.showModal('#grayReleaseWithoutRulesTips'); } }, function (result) { scope.releaseBtnDisabled = false; toastr.error(AppUtil.errorMsg(result), $translate.instant('ReleaseModal.GrayscalePublishFailed')); }); } function mergeAndPublish() { NamespaceBranchService.mergeAndReleaseBranch(scope.appId, scope.env, scope.cluster, scope.toReleaseNamespace.baseInfo.namespaceName, scope.toReleaseNamespace.baseInfo.clusterName, scope.toReleaseNamespace.releaseTitle, scope.releaseComment, scope.isEmergencyPublish, scope.toReleaseNamespace.mergeAfterDeleteBranch) .then(function (result) { toastr.success($translate.instant('ReleaseModal.AllPublished')); EventManager.emit(EventManager.EventType.REFRESH_NAMESPACE, { namespace: scope.toReleaseNamespace }) }, function (result) { toastr.error(AppUtil.errorMsg(result), $translate.instant('ReleaseModal.AllPublishFailed')); }); AppUtil.hideModal('#releaseModal'); } function switchReleaseChangeViewType(type) { scope.releaseChangeViewType = type; scope.isCompareMaster = type === 'compareWithMasterValue'; scope.isComparePublished = type === 'compareWithPublishedValue'; scope.isNoCompare = type === 'release'; } } } } ================================================ FILE: apollo-portal/src/main/resources/static/scripts/directive/rollback-modal-directive.js ================================================ /* * Copyright 2024 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ directive_module.directive('rollbackmodal', rollbackModalDirective); function rollbackModalDirective($translate, AppUtil, EventManager, ReleaseService, toastr) { return { restrict: 'E', templateUrl: AppUtil.prefixPath() + '/views/component/rollback-modal.html', transclude: true, replace: true, scope: { appId: '=', env: '=', cluster: '=' }, link: function (scope) { scope.isRollbackTo = false; scope.showRollbackAlertDialog = showRollbackAlertDialog; EventManager.subscribe(EventManager.EventType.PRE_ROLLBACK_NAMESPACE, function (context) { if (context.toReleaseId) { preRollbackTo(context.namespace, context.toReleaseId); } else { preRollback(context.namespace); } }); EventManager.subscribe(EventManager.EventType.ROLLBACK_NAMESPACE, function (context) { if (context.toReleaseId) { rollbackTo(context.toReleaseId); } else { rollback(); } }); function preRollback(namespace) { scope.toRollbackNamespace = namespace; //load latest two active releases ReleaseService.findActiveReleases(scope.appId, scope.env, scope.cluster, scope.toRollbackNamespace.baseInfo.namespaceName, 0, 2) .then(function (result) { if (result.length <= 1) { toastr.error($translate.instant('Rollback.NoRollbackList')); return; } scope.toRollbackNamespace.firstRelease = result[0]; scope.toRollbackNamespace.secondRelease = result[1]; ReleaseService.compare(scope.env, scope.toRollbackNamespace.firstRelease.id, scope.toRollbackNamespace.secondRelease.id) .then(function (result) { scope.toRollbackNamespace.releaseCompareResult = result.changes; AppUtil.showModal('#rollbackModal'); }) }); } function preRollbackTo(namespace, toReleaseId) { scope.isRollbackTo = true; scope.toRollbackNamespace = namespace; scope.toRollbackNamespace.isPropertiesFormat = namespace.format == 'properties'; ReleaseService.findLatestActiveRelease(scope.appId, scope.env, namespace.baseInfo.clusterName, namespace.baseInfo.namespaceName) .then(function (result) { if (!result) { toastr.error($translate.instant('Rollback.NoRollbackList')); return; } scope.toRollbackNamespace.firstRelease = result; ReleaseService.get(scope.env, toReleaseId) .then(function (result) { scope.toRollbackNamespace.secondRelease = result; if (scope.toRollbackNamespace.firstRelease.id == scope.toRollbackNamespace.secondRelease.id) { toastr.error($translate.instant('Rollback.SameAsCurrentRelease')); return; } ReleaseService.compare(scope.env, scope.toRollbackNamespace.firstRelease.id, scope.toRollbackNamespace.secondRelease.id) .then(function (result) { scope.toRollbackNamespace.releaseCompareResult = result.changes; AppUtil.showModal('#rollbackModal'); }) }) }) } function rollback() { scope.toRollbackNamespace.rollbackBtnDisabled = true; ReleaseService.rollback(scope.env, scope.toRollbackNamespace.firstRelease.id) .then(function (result) { toastr.success($translate.instant('Rollback.RollbackSuccessfully')); scope.toRollbackNamespace.rollbackBtnDisabled = false; AppUtil.hideModal('#rollbackModal'); EventManager.emit(EventManager.EventType.REFRESH_NAMESPACE, { namespace: scope.toRollbackNamespace }); }, function (result) { scope.toRollbackNamespace.rollbackBtnDisabled = false; AppUtil.showErrorMsg(result, $translate.instant('Rollback.RollbackFailed')); }) } function rollbackTo(toReleaseId) { scope.toRollbackNamespace.rollbackBtnDisabled = true; ReleaseService.rollbackTo(scope.env, scope.toRollbackNamespace.firstRelease.id, toReleaseId ) .then(function (result) { toastr.success($translate.instant('Rollback.RollbackSuccessfully')); scope.toRollbackNamespace.rollbackBtnDisabled = false; AppUtil.hideModal('#rollbackModal'); EventManager.emit(EventManager.EventType.REFRESH_RELEASE_HISTORY, {releaseId: toReleaseId}); }, function (result) { scope.toRollbackNamespace.rollbackBtnDisabled = false; AppUtil.showErrorMsg(result, $translate.instant('Rollback.RollbackFailed')); }) } function showRollbackAlertDialog() { AppUtil.hideModal("#rollbackModal"); AppUtil.showModal("#rollbackAlertDialog"); } } } } ================================================ FILE: apollo-portal/src/main/resources/static/scripts/directive/show-text-modal-directive.js ================================================ /* * Copyright 2024 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ directive_module.directive('showtextmodal', showTextModalDirective) .filter('jsonBigIntFilter', function () { return function (text) { if (typeof(text) === "undefined" || typeof JSON.parse(text) !== "object" || !text) { return; } const numberRegex = /"\|+\d+\|+"/g; const splitRegex = /"\|+\d+\|+"/; const splitArray = text.split(splitRegex); const matchResult = text.match(numberRegex); const borderRegex = /"\|\d+\|"/; if (!matchResult || 0 === splitArray.length) { return text; } else { Object.keys(matchResult).forEach(function (key) { if (matchResult[key].includes('"\|')) { if (borderRegex.test(matchResult[key])) { matchResult[key] = matchResult[key].replaceAll('"\|', ''); matchResult[key] = matchResult[key].replaceAll('|\"', ''); } else { matchResult[key] = matchResult[key].replaceAll('"\|', '"'); matchResult[key] = matchResult[key].replaceAll('|\"', '"'); } } }); } let resultStr = ''; let index = 0; const isJsonText = resultStr => { try { return typeof JSON.parse(resultStr) === "object"; } catch (e) { return false; } }; Object.keys(splitArray).forEach(function (key) { resultStr = resultStr.concat(splitArray[key]); if (typeof(matchResult[index]) !== "undefined" && !isJsonText(resultStr)) { resultStr = resultStr.concat(matchResult[index++]) } }) return resultStr; } }); function showTextModalDirective(AppUtil) { return { restrict: 'E', templateUrl: AppUtil.prefixPath() + '/views/component/show-text-modal.html', transclude: true, replace: true, scope: { text: '=', oldStr: '=', newStr: '=', enableTextDiff: '=' }, link: function (scope) { scope.$watch('text', init); function init() { scope.jsonObject = undefined; if (isJsonText(scope.text) && !AppUtil.hasDuplicateKeys(scope.text)) { scope.jsonObject = parseBigInt(scope.text); } } function isJsonText(text) { try { return typeof JSON.parse(text) === "object"; } catch (e) { return false; } } function parseBigInt(str) { if (/\d+/.test(str)) { let replaceMap = []; let n = 0; str = str.replace(/"\|+\d+\|+"/g, function (match) { return match.replace('"\|', '"\||').replace('|"', '||"'); }) .replace(/"(\\?[\s\S])*?"/g, function (match) { if (/\d+/.test(match)) { replaceMap.push(match); return '"""'; } return match; }).replace(/[+\-\d.eE]+/g, function (match) { if (/^\d+$/.test(match)) { return '"|' + match + '|"'; } return match; }).replace(/"""/g, function () { return replaceMap[n++]; }) } return JSON.parse(str); } } } } ================================================ FILE: apollo-portal/src/main/resources/static/scripts/services/AccessKeyService.js ================================================ /* * Copyright 2024 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ appService.service('AccessKeyService', ['$resource', '$q', 'AppUtil', function ($resource, $q, AppUtil) { var access_key_resource = $resource('', {}, { load_access_keys: { method: 'GET', isArray: true, url: AppUtil.prefixPath() + '/apps/:appId/envs/:env/accesskeys' }, create_access_key: { method: 'POST', url: AppUtil.prefixPath() + '/apps/:appId/envs/:env/accesskeys' }, remove_access_key: { method: 'DELETE', url: AppUtil.prefixPath() + '/apps/:appId/envs/:env/accesskeys/:id' }, enable_access_key: { method: 'PUT', url: AppUtil.prefixPath() + '/apps/:appId/envs/:env/accesskeys/:id/enable?mode=:mode' }, disable_access_key: { method: 'PUT', url: AppUtil.prefixPath() + '/apps/:appId/envs/:env/accesskeys/:id/disable' } }); return { load_access_keys: function (appId, env) { var d = $q.defer(); access_key_resource.load_access_keys({ appId: appId, env: env }, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; }, create_access_key: function (appId, env, user) { var d = $q.defer(); access_key_resource.create_access_key({ appId: appId, env: env }, { dataChangeCreatedBy: user }, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; }, remove_access_key: function (appId, env, id) { var d = $q.defer(); access_key_resource.remove_access_key({ appId: appId, env: env, id: id }, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; }, enable_access_key: function (appId, env, id, mode) { var d = $q.defer(); access_key_resource.enable_access_key({ appId: appId, env: env, id: id, mode: mode }, {}, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; }, disable_access_key: function (appId, env, id) { var d = $q.defer(); access_key_resource.disable_access_key({ appId: appId, env: env, id: id }, {}, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; } } }]); ================================================ FILE: apollo-portal/src/main/resources/static/scripts/services/AppService.js ================================================ /* * Copyright 2024 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ appService.service('AppService', ['$resource', '$q', 'AppUtil', function ($resource, $q, AppUtil) { var app_resource = $resource(AppUtil.prefixPath() + '/apps/:appId', {}, { find_apps: { method: 'GET', isArray: true, url: AppUtil.prefixPath() + '/apps' }, find_app_by_self: { method: 'GET', isArray: true, url: AppUtil.prefixPath() + '/apps/by-self' }, load_navtree: { method: 'GET', isArray: false, url: AppUtil.prefixPath() + '/apps/:appId/navtree' }, load_app: { method: 'GET', isArray: false }, create_app: { method: 'POST', url: AppUtil.prefixPath() + '/apps' }, update_app: { method: 'PUT', url: AppUtil.prefixPath() + '/apps/:appId' }, create_app_remote: { method: 'POST', url: AppUtil.prefixPath() + '/apps/envs/:env' }, find_miss_envs: { method: 'GET', url: AppUtil.prefixPath() + '/apps/:appId/miss_envs' }, create_missing_namespaces: { method: 'POST', url: AppUtil.prefixPath() + '/apps/:appId/envs/:env/clusters/:clusterName/missing-namespaces' }, find_missing_namespaces: { method: 'GET', url: AppUtil.prefixPath() + '/apps/:appId/envs/:env/clusters/:clusterName/missing-namespaces' }, delete_app: { method: 'DELETE', isArray: false }, allow_app_master_assign_role: { method: 'POST', url: AppUtil.prefixPath() + '/apps/:appId/system/master/:userId' }, delete_app_master_assign_role: { method: 'DELETE', url: AppUtil.prefixPath() + '/apps/:appId/system/master/:userId' }, has_create_application_role: { method: 'GET', url: AppUtil.prefixPath() + '/system/role/createApplication/:userId' } }); return { find_apps: function (appIds) { if (!appIds) { appIds = ''; } var d = $q.defer(); app_resource.find_apps({appIds: appIds}, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; }, find_app_by_self: function (page, size) { var d = $q.defer(); app_resource.find_app_by_self({ page: page, size: size }, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; }, load_nav_tree: function (appId) { var d = $q.defer(); app_resource.load_navtree({ appId: appId }, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; }, create: function (app) { var d = $q.defer(); app_resource.create_app({}, app, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; }, update: function (app) { var d = $q.defer(); app_resource.update_app({ appId: app.appId }, app, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; }, create_remote: function (env, app) { var d = $q.defer(); app_resource.create_app_remote({env: env}, app, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; }, load: function (appId) { var d = $q.defer(); app_resource.load_app({ appId: appId }, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; }, find_miss_envs: function (appId) { var d = $q.defer(); app_resource.find_miss_envs({ appId: appId }, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; }, create_missing_namespaces: function (appId, env, clusterName) { var d = $q.defer(); app_resource.create_missing_namespaces({ appId: appId, env: env, clusterName: clusterName }, null, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; }, find_missing_namespaces: function (appId, env, clusterName) { var d = $q.defer(); app_resource.find_missing_namespaces({ appId: appId, env: env, clusterName: clusterName }, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; }, delete_app: function (appId) { var d = $q.defer(); app_resource.delete_app({ appId: appId }, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; }, allow_app_master_assign_role: function (appId, userId) { var d = $q.defer(); app_resource.allow_app_master_assign_role({ appId: appId, userId: userId }, null, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; }, delete_app_master_assign_role: function (appId, userId) { var d = $q.defer(); app_resource.delete_app_master_assign_role({ appId: appId, userId: userId }, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; }, has_create_application_role: function (userId) { var d = $q.defer(); app_resource.has_create_application_role({ userId: userId }, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; } } }]); ================================================ FILE: apollo-portal/src/main/resources/static/scripts/services/AuditLogService.js ================================================ /* * Copyright 2024 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ appService.service('AuditLogService', ['$resource', '$q', 'AppUtil', function ($resource, $q, AppUtil) { var audit_resource = $resource('', {}, { get_properties: { method: 'GET', url: AppUtil.prefixPath() + '/apollo/audit/properties', isArray: false }, find_all_logs: { method: 'GET', url: AppUtil.prefixPath() + '/apollo/audit/logs?page=:page&size=:size', isArray: true }, find_logs_by_opName: { method: 'GET', url: AppUtil.prefixPath() + '/apollo/audit/logs/opName?opName=:opName&page=:page&size=:size&startDate=:startDate&endDate=:endDate', isArray: true }, find_trace_details: { method: 'GET', url: AppUtil.prefixPath() + '/apollo/audit/trace?traceId=:traceId', isArray: true }, find_dataInfluences_by_field: { method: 'GET', url: AppUtil.prefixPath() + '/apollo/audit/logs/dataInfluences/field?entityName=:entityName&entityId=:entityId&fieldName=:fieldName&page=:page&size=:size', isArray: true }, search_by_name_or_type_or_operator: { method: 'GET', url: AppUtil.prefixPath() + '/apollo/audit/logs/by-name-or-type-or-operator?query=:query&page=:page&size=:size', isArray: true } }); return { get_properties: function () { var d = $q.defer(); audit_resource.get_properties({ }, function (result) { d.resolve(result); }, function (result) { d.reject(result); } ); return d.promise; }, find_all_logs: function (page, size) { var d = $q.defer(); audit_resource.find_all_logs({ page: page, size: size }, function (result) { d.resolve(result); }, function (result) { d.reject(result); } ); return d.promise; }, find_logs_by_opName: function (opName, startDate, endDate, page, size) { var d = $q.defer(); audit_resource.find_logs_by_opName({ opName: opName, startDate: startDate, endDate: endDate, page: page, size: size }, function (result) { d.resolve(result); }, function (result) { d.reject(result); } ); return d.promise; }, find_trace_details: function (traceId) { var d = $q.defer(); audit_resource.find_trace_details({ traceId: traceId }, function (result) { d.resolve(result); }, function (result) { d.reject(result); } ); return d.promise; }, find_dataInfluences_by_field: function (entityName, entityId, fieldName, page, size) { var d = $q.defer(); audit_resource.find_dataInfluences_by_field({ entityName: entityName, entityId: entityId, fieldName: fieldName, page: page, size: size }, function (result) { d.resolve(result); }, function (result) { d.reject(result); } ); return d.promise; }, search_by_name_or_type_or_operator: function (query, page, size) { var d = $q.defer(); audit_resource.search_by_name_or_type_or_operator({ query: query, page: page, size: size }, function (result) { d.resolve(result); }, function (result) { d.reject(result); } ); return d.promise; } }; }]); ================================================ FILE: apollo-portal/src/main/resources/static/scripts/services/ClusterService.js ================================================ /* * Copyright 2024 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ appService.service('ClusterService', ['$resource', '$q', function ($resource, $q) { var cluster_resource = $resource('', {}, { create_cluster: { method: 'POST', url: '/openapi/v1/envs/:env/apps/:appId/clusters' }, load_cluster: { method: 'GET', url: '/openapi/v1/envs/:env/apps/:appId/clusters/:clusterName' }, delete_cluster: { method: 'DELETE', url: '/openapi/v1/envs/:env/apps/:appId/clusters/:clusterName' } }); return { create_cluster: function (appId, env, cluster) { var d = $q.defer(); cluster_resource.create_cluster({ appId: appId, env: env }, cluster, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; }, load_cluster: function (appId, env, clusterName) { var d = $q.defer(); cluster_resource.load_cluster({ appId: appId, env: env, clusterName: clusterName }, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; }, delete_cluster: function (appId, env, clusterName) { var d = $q.defer(); cluster_resource.delete_cluster({ appId: appId, env: env, clusterName: clusterName }, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; } } }]); ================================================ FILE: apollo-portal/src/main/resources/static/scripts/services/CommitService.js ================================================ /* * Copyright 2024 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ appService.service('CommitService', ['$resource', '$q','AppUtil', function ($resource, $q, AppUtil) { var commit_resource = $resource('', {}, { find_commits: { method: 'GET', isArray: true, url: AppUtil.prefixPath() + '/apps/:appId/envs/:env/clusters/:clusterName/namespaces/:namespaceName/commits?page=:page' } }); return { find_commits: function (appId, env, clusterName, namespaceName, key, page, size) { var d = $q.defer(); commit_resource.find_commits({ appId: appId, env: env, clusterName: clusterName, namespaceName: namespaceName, key: key, page: page, size: size }, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; } } }]); ================================================ FILE: apollo-portal/src/main/resources/static/scripts/services/CommonService.js ================================================ /* * Copyright 2024 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ appService.service('CommonService', ['$resource', '$q', 'AppUtil', function ($resource, $q, AppUtil) { var resource = $resource('', {}, { page_setting: { method: 'GET', isArray: false, url: AppUtil.prefixPath() + '/page-settings' } }); return { getPageSetting: function () { return AppUtil.ajax(resource.page_setting, {}); } } }]); ================================================ FILE: apollo-portal/src/main/resources/static/scripts/services/ConfigService.js ================================================ /* * Copyright 2024 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ appService.service("ConfigService", ['$resource', '$q', 'AppUtil', function ($resource, $q, AppUtil) { var config_source = $resource("", {}, { load_namespace: { method: 'GET', isArray: false, url: AppUtil.prefixPath() + '/apps/:appId/envs/:env/clusters/:clusterName/namespaces/:namespaceName' }, load_public_namespace_for_associated_namespace: { method: 'GET', isArray: false, url: AppUtil.prefixPath() + '/envs/:env/apps/:appId/clusters/:clusterName/namespaces/:namespaceName/associated-public-namespace' }, load_all_namespaces: { method: 'GET', isArray: true, url: AppUtil.prefixPath() + '/apps/:appId/envs/:env/clusters/:clusterName/namespaces' }, find_items: { method: 'GET', isArray: true, url: AppUtil.prefixPath() + '/apps/:appId/envs/:env/clusters/:clusterName/namespaces/:namespaceName/items' }, modify_items: { method: 'PUT', url: AppUtil.prefixPath() + '/apps/:appId/envs/:env/clusters/:clusterName/namespaces/:namespaceName/items' }, diff: { method: 'POST', url: AppUtil.prefixPath() + '/namespaces/:namespaceName/diff', isArray: true }, sync_item: { method: 'PUT', url: AppUtil.prefixPath() + '/apps/:appId/namespaces/:namespaceName/items', isArray: false }, create_item: { method: 'POST', url: AppUtil.prefixPath() + '/apps/:appId/envs/:env/clusters/:clusterName/namespaces/:namespaceName/item' }, update_item: { method: 'PUT', url: AppUtil.prefixPath() + '/apps/:appId/envs/:env/clusters/:clusterName/namespaces/:namespaceName/item' }, delete_item: { method: 'DELETE', url: AppUtil.prefixPath() + '/apps/:appId/envs/:env/clusters/:clusterName/namespaces/:namespaceName/items/:itemId' }, syntax_check_text: { method: 'POST', url: AppUtil.prefixPath() + '/apps/:appId/envs/:env/clusters/:clusterName/namespaces/:namespaceName/syntax-check' }, revoke_item: { method: 'PUT', url: AppUtil.prefixPath() + '/apps/:appId/envs/:env/clusters/:clusterName/namespaces/:namespaceName/revoke-items' }, }); return { load_namespace: function (appId, env, clusterName, namespaceName) { var d = $q.defer(); config_source.load_namespace({ appId: appId, env: env, clusterName: clusterName, namespaceName: namespaceName }, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; }, load_public_namespace_for_associated_namespace: function (env, appId, clusterName, namespaceName) { var d = $q.defer(); config_source.load_public_namespace_for_associated_namespace({ env: env, appId: appId, clusterName: clusterName, namespaceName: namespaceName }, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; }, load_all_namespaces: function (appId, env, clusterName) { var d = $q.defer(); config_source.load_all_namespaces({ appId: appId, env: env, clusterName: clusterName }, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; }, find_items: function (appId, env, clusterName, namespaceName, orderBy) { var d = $q.defer(); config_source.find_items({ appId: appId, env: env, clusterName: clusterName, namespaceName: namespaceName, orderBy: orderBy }, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; }, modify_items: function (appId, env, clusterName, namespaceName, model) { var d = $q.defer(); config_source.modify_items({ appId: appId, env: env, clusterName: clusterName, namespaceName: namespaceName }, model, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; }, diff: function (namespaceName, sourceData) { var d = $q.defer(); config_source.diff({ namespaceName: namespaceName }, sourceData, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; }, sync_items: function (appId, namespaceName, sourceData) { var d = $q.defer(); config_source.sync_item({ appId: appId, namespaceName: namespaceName }, sourceData, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; }, create_item: function (appId, env, clusterName, namespaceName, item) { var d = $q.defer(); config_source.create_item({ appId: appId, env: env, clusterName: clusterName, namespaceName: namespaceName }, item, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; }, update_item: function (appId, env, clusterName, namespaceName, item) { var d = $q.defer(); config_source.update_item({ appId: appId, env: env, clusterName: clusterName, namespaceName: namespaceName }, item, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; }, delete_item: function (appId, env, clusterName, namespaceName, itemId) { var d = $q.defer(); config_source.delete_item({ appId: appId, env: env, clusterName: clusterName, namespaceName: namespaceName, itemId: itemId }, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; }, syntax_check_text: function (appId, env, clusterName, namespaceName, model) { var d = $q.defer(); config_source.syntax_check_text({ appId: appId, env: env, clusterName: clusterName, namespaceName: namespaceName }, model, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; }, revoke_item: function (appId, env, clusterName, namespaceName) { var d = $q.defer(); config_source.revoke_item({ appId: appId, env: env, clusterName: clusterName, namespaceName: namespaceName },{}, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; } } }]); ================================================ FILE: apollo-portal/src/main/resources/static/scripts/services/ConsumerService.js ================================================ /* * Copyright 2024 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ appService.service('ConsumerService', ['$resource', '$q', 'AppUtil', function ($resource, $q, AppUtil) { var resource = $resource('', {}, { create_consumer: { method: 'POST', isArray: false, url: AppUtil.prefixPath() + '/consumers' }, get_consumer_token_by_appId: { method: 'GET', isArray: false, url: AppUtil.prefixPath() + '/consumer-tokens/by-appId' }, assign_role_to_consumer: { method: 'POST', isArray: true, url: AppUtil.prefixPath() + '/consumers/:token/assign-role' }, get_consumer_list: { method: 'GET', isArray: true, url: AppUtil.prefixPath() + '/consumers' }, delete_consumer: { method: 'DELETE', isArray: false, url: AppUtil.prefixPath() + '/consumers/by-appId' } }); return { createConsumer: function (consumer) { return AppUtil.ajax(resource.create_consumer, {}, consumer); }, getConsumerTokenByAppId: function (appId) { return AppUtil.ajax(resource.get_consumer_token_by_appId, { appId: appId }); }, assignRoleToConsumer: function (token, type, appId, namespaceName, envs) { return AppUtil.ajax(resource.assign_role_to_consumer, { token: token, type: type, envs: envs }, { appId: appId, namespaceName: namespaceName } ) }, getConsumerList: function (page, size){ return AppUtil.ajax(resource.get_consumer_list, { page: page, size: size } ) }, deleteConsumer: function (appId){ return AppUtil.ajax(resource.delete_consumer, { appId: appId } ) } } }]); ================================================ FILE: apollo-portal/src/main/resources/static/scripts/services/EnvService.js ================================================ /* * Copyright 2024 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ appService.service('EnvService', ['$resource', '$q', 'AppUtil', function ($resource, $q, AppUtil) { var env_resource = $resource(AppUtil.prefixPath() + '/openapi/v1/envs', {}, { find_all_envs:{ method: 'GET', isArray: true, url: AppUtil.prefixPath() + '/openapi/v1/envs' } }); return { find_all_envs: function () { var d = $q.defer(); env_resource.find_all_envs({ }, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; } } }]); ================================================ FILE: apollo-portal/src/main/resources/static/scripts/services/EventManager.js ================================================ /* * Copyright 2024 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ appService.service('EventManager', [function () { /** * subscribe EventType with any object * @type {string} */ var ALL_OBJECT = '*'; var eventRegistry = {}; /** * * @param eventType acquired. event type * @param context optional. event execute context * @param objectId optional. subscribe object id and empty value means subscribe event type with all object */ function emit(eventType, context, objectId) { if (!eventType) { return; } if (!eventRegistry[eventType]) { return; } context = context || {}; if (objectId != null && objectId != ALL_OBJECT) { emitEventToSubscribers(eventRegistry[eventType][objectId], context); emitEventToSubscribers(eventRegistry[eventType][ALL_OBJECT]); } else { //emit event to subscriber which subscribed all object emitEventToSubscribers(eventRegistry[eventType][ALL_OBJECT], context); } } function emitEventToSubscribers(subscribers, context) { if (subscribers) { subscribers.forEach(function (subscriber) { subscriber.callback(context); }) } } /** * * @param eventType acquired. event type * @param callback acquired. callback function when event emitted * @param objectId optional. subscribe object id and empty value means subscribe event type with all object */ function subscribe(eventType, callback, objectId) { if (!eventType || !callback) { return; } objectId = objectId || ALL_OBJECT; eventRegistry[eventType] = eventRegistry[eventType] || {}; eventRegistry[eventType][objectId] = eventRegistry[eventType][objectId] || []; var subscriber = { id: Math.random() * Math.random(), callback: callback }; eventRegistry[eventType][objectId].push(subscriber); return subscriber.id; } /** * * @param eventType acquired. event type * @param subscriberId acquired. subscriber id which get from event manager when subscribe * @param objectId optional. subscribe object id and empty value means subscribe event type with all object */ function unsubscribe(eventType, subscriberId, objectId) { if (!eventType || !subscriberId) { return; } objectId = objectId || ALL_OBJECT; if (eventRegistry[eventType] && eventRegistry[eventType][objectId]) { var subscribers = eventRegistry[eventType][objectId]; subscribers.forEach(function (subscriber, index) { if (subscriber.id == subscriberId) { subscribers.splice(index, 1); } }) } } return { ALL_OBJECT: ALL_OBJECT, emit: emit, subscribe: subscribe, unsubscribe: unsubscribe, EventType: { REFRESH_NAMESPACE: 'refresh_namespace', REFRESH_RELEASE_HISTORY: 'refresh_release_history', PUBLISH_NAMESPACE: 'pre_public_namespace', MERGE_AND_PUBLISH_NAMESPACE: 'merge_and_publish_namespace', PRE_ROLLBACK_NAMESPACE: 'pre_rollback_namespace', ROLLBACK_NAMESPACE: 'rollback_namespace', EDIT_GRAY_RELEASE_RULES: 'edit_gray_release_rules', UPDATE_GRAY_RELEASE_RULES: 'update_gray_release_rules', PUBLISH_DENY: 'publish_deny', EMERGENCY_PUBLISH: 'emergency_publish', PRE_DELETE_NAMESPACE: 'pre_delete_namespace', PRE_IMPORT_NAMESPACE: 'pre_import_namespace', DELETE_NAMESPACE: 'delete_namespace', CHANGE_ENV_CLUSTER: "change_env_cluster", SYNTAX_CHECK_TEXT_FAILED: "syntax_check_text_failed" } } }]); ================================================ FILE: apollo-portal/src/main/resources/static/scripts/services/ExportService.js ================================================ /* * Copyright 2024 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ appService.service('ExportService', ['$resource', '$q', function ($resource, $q) { var resource = $resource('', {}, { importConfig: { method: 'POST', url: '/import', headers: {'Content-Type': undefined}, } }); return { importConfig: function (envs, file) { var form = new FormData(); form.append('file', file); var d = $q.defer(); resource.importConfig({ data: form, envs: envs, }, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; } } }]); ================================================ FILE: apollo-portal/src/main/resources/static/scripts/services/FavoriteService.js ================================================ /* * Copyright 2024 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ appService.service('FavoriteService', ['$resource', '$q', 'AppUtil', function ($resource, $q, AppUtil) { var resource = $resource('', {}, { find_favorites: { method: 'GET', url: AppUtil.prefixPath() + '/favorites', isArray: true }, add_favorite: { method: 'POST', url: AppUtil.prefixPath() + '/favorites' }, delete_favorite: { method: 'DELETE', url: AppUtil.prefixPath() + '/favorites/:favoriteId' }, to_top: { method: 'PUT', url: AppUtil.prefixPath() + '/favorites/:favoriteId' } }); return { findFavorites: function (userId, appId, page, size) { var d = $q.defer(); resource.find_favorites({ userId: userId, appId: appId, page: page, size: size }, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; }, addFavorite: function (favorite) { var d = $q.defer(); resource.add_favorite({}, favorite, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; }, deleteFavorite: function (favoriteId) { var d = $q.defer(); resource.delete_favorite({ favoriteId: favoriteId }, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; }, toTop: function (favoriteId) { var d = $q.defer(); resource.to_top({ favoriteId: favoriteId }, {}, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; } } }]); ================================================ FILE: apollo-portal/src/main/resources/static/scripts/services/GlobalSearchValueService.js ================================================ /* * Copyright 2024 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ appService.service('GlobalSearchValueService', ['$resource', '$q', 'AppUtil', function ($resource, $q, AppUtil) { let global_search_resource = $resource('', {}, { get_item_Info_by_key_and_Value: { isArray: false, method: 'GET', url: AppUtil.prefixPath() + '/global-search/item-info/by-key-or-value', params: { key: 'key', value: 'value' } } }); return { findItemInfoByKeyAndValue:function (key,value){ let d = $q.defer(); global_search_resource.get_item_Info_by_key_and_Value({key: key,value: value},function (result) { d.resolve(result); }, function (error) { d.reject(error); }); return d.promise; } } }]); ================================================ FILE: apollo-portal/src/main/resources/static/scripts/services/InstanceService.js ================================================ /* * Copyright 2024 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ appService.service('InstanceService', ['$resource', '$q', 'AppUtil', function ($resource, $q, AppUtil) { var resource = $resource('', {}, { find_instances_by_release: { method: 'GET', url: AppUtil.prefixPath() + '/envs/:env/instances/by-release' }, find_instances_by_namespace: { method: 'GET', isArray: false, url: AppUtil.prefixPath() + '/envs/:env/instances/by-namespace' }, find_by_releases_not_in: { method: 'GET', isArray: true, url: AppUtil.prefixPath() + '/envs/:env/instances/by-namespace-and-releases-not-in' }, get_instance_count_by_namespace: { method: 'GET', isArray: false, url: AppUtil.prefixPath() + "/envs/:env/instances/by-namespace/count" } }); var instanceService = { findInstancesByRelease: function (env, releaseId, page, size) { if (!size) { size = 20; } var d = $q.defer(); resource.find_instances_by_release({ env: env, releaseId: releaseId, page: page, size: size }, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; }, findInstancesByNamespace: function (appId, env, clusterName, namespaceName, instanceAppId, page, size) { if (!size) { size = 20; } var d = $q.defer(); var instanceAppIdRequest = instanceAppId; instanceService.lastInstanceAppIdRequest = instanceAppIdRequest; resource.find_instances_by_namespace({ env: env, appId: appId, clusterName: clusterName, namespaceName: namespaceName, instanceAppId: instanceAppId, page: page, size: size }, function (result) { if (instanceAppIdRequest != instanceService.lastInstanceAppIdRequest) { return; } d.resolve(result); }, function (result) { if (instanceAppIdRequest != instanceService.lastInstanceAppIdRequest) { return; } d.reject(result); }); return d.promise; }, findByReleasesNotIn: function (appId, env, clusterName, namespaceName, releaseIds) { var d = $q.defer(); resource.find_by_releases_not_in({ env: env, appId: appId, clusterName: clusterName, namespaceName: namespaceName, releaseIds: releaseIds }, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; }, getInstanceCountByNamespace: function (appId, env, clusterName, namespaceName) { var d = $q.defer(); resource.get_instance_count_by_namespace({ env: env, appId: appId, clusterName: clusterName, namespaceName: namespaceName }, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; } }; return instanceService; }]); ================================================ FILE: apollo-portal/src/main/resources/static/scripts/services/NamespaceBranchService.js ================================================ /* * Copyright 2024 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ appService.service('NamespaceBranchService', ['$resource', '$q', 'AppUtil', function ($resource, $q, AppUtil) { var resource = $resource('', {}, { find_namespace_branch: { method: 'GET', isArray: false, url: AppUtil.prefixPath() + '/apps/:appId/envs/:env/clusters/:clusterName/namespaces/:namespaceName/branches' }, create_branch: { method: 'POST', isArray: false, url: AppUtil.prefixPath() + '/apps/:appId/envs/:env/clusters/:clusterName/namespaces/:namespaceName/branches' }, delete_branch: { method: 'DELETE', isArray: false, url: AppUtil.prefixPath() + '/apps/:appId/envs/:env/clusters/:clusterName/namespaces/:namespaceName/branches/:branchName' }, merge_and_release_branch: { method: 'POST', isArray: false, url: AppUtil.prefixPath() + '/apps/:appId/envs/:env/clusters/:clusterName/namespaces/:namespaceName/branches/:branchName/merge' }, find_branch_gray_rules: { method: 'GET', isArray: false, url: AppUtil.prefixPath() + '/apps/:appId/envs/:env/clusters/:clusterName/namespaces/:namespaceName/branches/:branchName/rules' }, update_branch_gray_rules: { method: 'PUT', isArray: false, url: AppUtil.prefixPath() + '/apps/:appId/envs/:env/clusters/:clusterName/namespaces/:namespaceName/branches/:branchName/rules' } }); function find_namespace_branch(appId, env, clusterName, namespaceName) { var d = $q.defer(); resource.find_namespace_branch({ appId: appId, env: env, clusterName: clusterName, namespaceName: namespaceName }, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; } function create_branch(appId, env, clusterName, namespaceName) { var d = $q.defer(); resource.create_branch({ appId: appId, env: env, clusterName: clusterName, namespaceName: namespaceName }, {}, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; } function delete_branch(appId, env, clusterName, namespaceName, branchName) { var d = $q.defer(); resource.delete_branch({ appId: appId, env: env, clusterName: clusterName, namespaceName: namespaceName, branchName: branchName }, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; } function merge_and_release_branch(appId, env, clusterName, namespaceName, branchName, title, comment, isEmergencyPublish, deleteBranch) { var d = $q.defer(); resource.merge_and_release_branch({ appId: appId, env: env, clusterName: clusterName, namespaceName: namespaceName, branchName: branchName, deleteBranch:deleteBranch }, { releaseTitle: title, releaseComment: comment, isEmergencyPublish: isEmergencyPublish }, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; } function find_branch_gray_rules(appId, env, clusterName, namespaceName, branchName) { var d = $q.defer(); resource.find_branch_gray_rules({ appId: appId, env: env, clusterName: clusterName, namespaceName: namespaceName, branchName: branchName }, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; } function update_branch_gray_rules(appId, env, clusterName, namespaceName, branchName, newRules) { var d = $q.defer(); resource.update_branch_gray_rules({ appId: appId, env: env, clusterName: clusterName, namespaceName: namespaceName, branchName: branchName }, newRules, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; } return { findNamespaceBranch: find_namespace_branch, createBranch: create_branch, deleteBranch: delete_branch, mergeAndReleaseBranch: merge_and_release_branch, findBranchGrayRules: find_branch_gray_rules, updateBranchGrayRules: update_branch_gray_rules } }]); ================================================ FILE: apollo-portal/src/main/resources/static/scripts/services/NamespaceLockService.js ================================================ /* * Copyright 2024 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ appService.service('NamespaceLockService', ['$resource', '$q', function ($resource, $q) { var resource = $resource('', {}, { get_namespace_lock: { method: 'GET', url: 'apps/:appId/envs/:env/clusters/:clusterName/namespaces/:namespaceName/lock-info' } }); return { get_namespace_lock: function (appId, env, clusterName, namespaceName) { var d = $q.defer(); resource.get_namespace_lock({ appId: appId, env: env, clusterName: clusterName, namespaceName: namespaceName }, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; } } }]); ================================================ FILE: apollo-portal/src/main/resources/static/scripts/services/NamespaceService.js ================================================ /* * Copyright 2024 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ appService.service("NamespaceService", ['$resource', '$q', 'AppUtil', function ($resource, $q, AppUtil) { var namespace_source = $resource("", {}, { find_public_namespaces: { method: 'GET', isArray: true, url: AppUtil.prefixPath() + '/appnamespaces/public' }, createNamespace: { method: 'POST', url: AppUtil.prefixPath() + '/apps/:appId/namespaces', isArray: false }, createAppNamespace: { method: 'POST', url: AppUtil.prefixPath() + '/apps/:appId/appnamespaces?appendNamespacePrefix=:appendNamespacePrefix', isArray: false }, getNamespacePublishInfo: { method: 'GET', url: AppUtil.prefixPath() + '/apps/:appId/namespaces/publish_info' }, deleteLinkedNamespace: { method: 'DELETE', url: AppUtil.prefixPath() + '/apps/:appId/envs/:env/clusters/:clusterName/linked-namespaces/:namespaceName' }, getPublicAppNamespaceAllNamespaces: { method: 'GET', url: AppUtil.prefixPath() + '/envs/:env/appnamespaces/:publicNamespaceName/namespaces', isArray: true }, loadAppNamespace: { method: 'GET', url: AppUtil.prefixPath() + '/apps/:appId/appnamespaces/:namespaceName' }, deleteAppNamespace: { method: 'DELETE', url: AppUtil.prefixPath() + '/apps/:appId/appnamespaces/:namespaceName' }, getLinkedNamespaceUsage: { method: 'GET', url: AppUtil.prefixPath() + '/apps/:appId/envs/:env/clusters/:clusterName/linked-namespaces/:namespaceName/usage', isArray: true }, getNamespaceUsage: { method: 'GET', url: AppUtil.prefixPath() + '/apps/:appId/namespaces/:namespaceName/usage', isArray: true }, findPublicNamespaceNames: { method: 'GET', isArray: true, url: AppUtil.prefixPath() + '/appnamespaces/public/names' } }); function find_public_namespaces() { var d = $q.defer(); namespace_source.find_public_namespaces({}, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; } function createNamespace(appId, namespaceCreationModel) { var d = $q.defer(); namespace_source.createNamespace({ appId: appId }, namespaceCreationModel, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; } function createAppNamespace(appId, appnamespace, appendNamespacePrefix) { var d = $q.defer(); namespace_source.createAppNamespace({ appId: appId, appendNamespacePrefix: appendNamespacePrefix }, appnamespace, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; } function getNamespacePublishInfo(appId) { var d = $q.defer(); namespace_source.getNamespacePublishInfo({ appId: appId }, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; } function deleteLinkedNamespace(appId, env, clusterName, namespaceName) { var d = $q.defer(); namespace_source.deleteLinkedNamespace({ appId: appId, env: env, clusterName: clusterName, namespaceName: namespaceName }, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; } function getPublicAppNamespaceAllNamespaces(env, publicNamespaceName, page, size) { var d = $q.defer(); namespace_source.getPublicAppNamespaceAllNamespaces({ env: env, publicNamespaceName: publicNamespaceName, page: page, size: size }, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; } function loadAppNamespace(appId, namespaceName) { var d = $q.defer(); namespace_source.loadAppNamespace({ appId: appId, namespaceName: namespaceName }, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; } function deleteAppNamespace(appId, namespaceName) { var d = $q.defer(); namespace_source.deleteAppNamespace({ appId: appId, namespaceName: namespaceName }, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; } function getLinkedNamespaceUsage(appId, env, clusterName, namespaceName) { var d = $q.defer(); namespace_source.getLinkedNamespaceUsage({ appId: appId, env: env, clusterName: clusterName, namespaceName: namespaceName }, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; } function getNamespaceUsage(appId, namespaceName) { var d = $q.defer(); namespace_source.getNamespaceUsage({ appId: appId, namespaceName: namespaceName }, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; } function findPublicNamespaceNames() { var d = $q.defer(); namespace_source.findPublicNamespaceNames({}, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; } return { find_public_namespaces: find_public_namespaces, createNamespace: createNamespace, createAppNamespace: createAppNamespace, getNamespacePublishInfo: getNamespacePublishInfo, deleteLinkedNamespace: deleteLinkedNamespace, getPublicAppNamespaceAllNamespaces: getPublicAppNamespaceAllNamespaces, loadAppNamespace: loadAppNamespace, deleteAppNamespace: deleteAppNamespace, getLinkedNamespaceUsage: getLinkedNamespaceUsage, getNamespaceUsage: getNamespaceUsage, findPublicNamespaceNames: findPublicNamespaceNames } }]); ================================================ FILE: apollo-portal/src/main/resources/static/scripts/services/OrganizationService.js ================================================ /* * Copyright 2024 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ appService.service("OrganizationService", ['$resource', '$q', 'AppUtil', function ($resource, $q, AppUtil) { var organization_source = $resource("", {}, { find_organizations: { method: 'GET', isArray: true, url: AppUtil.prefixPath() + '/openapi/v1/organizations' } }); return { find_organizations: function () { var d = $q.defer(); organization_source.find_organizations({}, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; } } }]); ================================================ FILE: apollo-portal/src/main/resources/static/scripts/services/PermissionService.js ================================================ /* * Copyright 2024 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ appService.service('PermissionService', ['$resource', '$q', 'AppUtil', function ($resource, $q, AppUtil) { var permission_resource = $resource('', {}, { init_app_namespace_permission: { method: 'POST', url: AppUtil.prefixPath() + '/apps/:appId/initPermission', headers: { 'Content-Type': 'text/plain;charset=UTF-8' } }, init_cluster_ns_permission: { method: 'POST', url: AppUtil.prefixPath() + '/apps/:appId/envs/:env/clusters/:clusterName/initNsPermission', headers: { 'Content-Type': 'text/plain;charset=UTF-8' } }, has_app_permission: { method: 'GET', url: AppUtil.prefixPath() + '/apps/:appId/permissions/:permissionType' }, has_namespace_permission: { method: 'GET', url: AppUtil.prefixPath() + '/apps/:appId/namespaces/:namespaceName/permissions/:permissionType' }, has_namespace_env_permission: { method: 'GET', url: AppUtil.prefixPath() + '/apps/:appId/envs/:env/namespaces/:namespaceName/permissions/:permissionType' }, has_cluster_ns_permission: { method: 'GET', url: AppUtil.prefixPath() + '/apps/:appId/envs/:env/clusters/:clusterName/ns_permissions/:permissionType' }, has_root_permission:{ method: 'GET', url: AppUtil.prefixPath() + '/permissions/root' }, get_namespace_role_users: { method: 'GET', url: AppUtil.prefixPath() + '/apps/:appId/namespaces/:namespaceName/role_users' }, get_namespace_env_role_users: { method: 'GET', url: AppUtil.prefixPath() + '/apps/:appId/envs/:env/namespaces/:namespaceName/role_users' }, assign_namespace_role_to_user: { method: 'POST', url: AppUtil.prefixPath() + '/apps/:appId/namespaces/:namespaceName/roles/:roleType', headers: { 'Content-Type': 'text/plain;charset=UTF-8' } }, assign_namespace_env_role_to_user: { method: 'POST', url: AppUtil.prefixPath() + '/apps/:appId/envs/:env/namespaces/:namespaceName/roles/:roleType', headers: { 'Content-Type': 'text/plain;charset=UTF-8' } }, remove_namespace_role_from_user: { method: 'DELETE', url: AppUtil.prefixPath() + '/apps/:appId/namespaces/:namespaceName/roles/:roleType?user=:user' }, remove_namespace_env_role_from_user: { method: 'DELETE', url: AppUtil.prefixPath() + '/apps/:appId/envs/:env/namespaces/:namespaceName/roles/:roleType?user=:user' }, get_app_role_users: { method: 'GET', url: AppUtil.prefixPath() + '/apps/:appId/role_users' }, assign_app_role_to_user: { method: 'POST', url: AppUtil.prefixPath() + '/apps/:appId/roles/:roleType', headers: { 'Content-Type': 'text/plain;charset=UTF-8' } }, remove_app_role_from_user: { method: 'DELETE', url: AppUtil.prefixPath() + '/apps/:appId/roles/:roleType?user=:user' }, has_open_manage_app_master_role_limit: { method: 'GET', url: AppUtil.prefixPath() + '/system/role/manageAppMaster' }, get_cluster_ns_role_users: { method: 'GET', url: AppUtil.prefixPath() + '/apps/:appId/envs/:env/clusters/:clusterName/ns_role_users' }, assign_cluster_ns_role_to_user: { method: 'POST', url: AppUtil.prefixPath() + '/apps/:appId/envs/:env/clusters/:clusterName/ns_roles/:roleType', headers: { 'Content-Type': 'text/plain;charset=UTF-8' } }, remove_cluster_ns_role_from_user: { method: 'DELETE', url: AppUtil.prefixPath() + '/apps/:appId/envs/:env/clusters/:clusterName/ns_roles/:roleType?user=:user' } }); function initAppNamespacePermission(appId, namespace) { var d = $q.defer(); permission_resource.init_app_namespace_permission({ appId: appId }, namespace, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; } function initClusterNsPermission(appId, env, clusterName) { var d = $q.defer(); permission_resource.init_cluster_ns_permission({ appId: appId, env: env, clusterName: clusterName }, {}, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; } function hasAppPermission(appId, permissionType) { var d = $q.defer(); permission_resource.has_app_permission({ appId: appId, permissionType: permissionType }, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; } function hasNamespacePermission(appId, namespaceName, permissionType) { var d = $q.defer(); permission_resource.has_namespace_permission({ appId: appId, namespaceName: namespaceName, permissionType: permissionType }, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; } function hasNamespaceEnvPermission(appId, env, namespaceName, permissionType) { var d = $q.defer(); permission_resource.has_namespace_env_permission({ appId: appId, namespaceName: namespaceName, permissionType: permissionType, env: env }, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; } function hasClusterNsPermission(appId, env, clusterName, permissionType) { var d = $q.defer(); permission_resource.has_cluster_ns_permission({ appId: appId, env: env, clusterName: clusterName, permissionType: permissionType }, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; } function assignNamespaceRoleToUser(appId, namespaceName, roleType, user) { var d = $q.defer(); permission_resource.assign_namespace_role_to_user({ appId: appId, namespaceName: namespaceName, roleType: roleType }, user, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; } function assignNamespaceEnvRoleToUser(appId, env, namespaceName, roleType, user) { var d = $q.defer(); permission_resource.assign_namespace_env_role_to_user({ appId: appId, namespaceName: namespaceName, roleType: roleType, env: env }, user, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; } function removeNamespaceRoleFromUser(appId, namespaceName, roleType, user) { var d = $q.defer(); permission_resource.remove_namespace_role_from_user({ appId: appId, namespaceName: namespaceName, roleType: roleType, user: user }, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; } function removeNamespaceEnvRoleFromUser(appId, env, namespaceName, roleType, user) { var d = $q.defer(); permission_resource.remove_namespace_env_role_from_user({ appId: appId, namespaceName: namespaceName, roleType: roleType, user: user, env: env }, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; } function assignClusterNsRoleToUser(appId, env, clusterName, roleType, user) { var d = $q.defer(); permission_resource.assign_cluster_ns_role_to_user({ appId: appId, env: env, clusterName: clusterName, roleType: roleType }, user, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; } function removeClusterNsRoleFromUser(appId, env, clusterName, roleType, user) { var d = $q.defer(); permission_resource.remove_cluster_ns_role_from_user({ appId: appId, env: env, clusterName: clusterName, roleType: roleType, user: user }, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; } return { init_app_namespace_permission: function (appId, namespace) { return initAppNamespacePermission(appId, namespace); }, init_cluster_ns_permission: function (appId, env, clusterName) { return initClusterNsPermission(appId, env, clusterName); }, has_manage_app_master_permission: function (appId) { return hasAppPermission(appId, 'ManageAppMaster'); }, has_create_namespace_permission: function (appId) { return hasAppPermission(appId, 'CreateNamespace'); }, has_create_cluster_permission: function (appId) { return hasAppPermission(appId, 'CreateCluster'); }, has_assign_user_permission: function (appId) { return hasAppPermission(appId, 'AssignRole'); }, has_modify_namespace_permission: function (appId, namespaceName) { return hasNamespacePermission(appId, namespaceName, 'ModifyNamespace'); }, has_modify_namespace_env_permission: function (appId, env, namespaceName) { return hasNamespaceEnvPermission(appId, env, namespaceName, 'ModifyNamespace'); }, has_release_namespace_permission: function (appId, namespaceName) { return hasNamespacePermission(appId, namespaceName, 'ReleaseNamespace'); }, has_release_namespace_env_permission: function (appId, env, namespaceName) { return hasNamespaceEnvPermission(appId, env, namespaceName, 'ReleaseNamespace'); }, has_root_permission: function () { var d = $q.defer(); permission_resource.has_root_permission({ }, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; }, assign_modify_namespace_role: function (appId, namespaceName, user) { return assignNamespaceRoleToUser(appId, namespaceName, 'ModifyNamespace', user); }, assign_modify_namespace_env_role: function (appId, env, namespaceName, user) { return assignNamespaceEnvRoleToUser(appId, env, namespaceName, 'ModifyNamespace', user); }, assign_release_namespace_role: function (appId, namespaceName, user) { return assignNamespaceRoleToUser(appId, namespaceName, 'ReleaseNamespace', user); }, assign_release_namespace_env_role: function (appId, env, namespaceName, user) { return assignNamespaceEnvRoleToUser(appId, env, namespaceName, 'ReleaseNamespace', user); }, remove_modify_namespace_role: function (appId, namespaceName, user) { return removeNamespaceRoleFromUser(appId, namespaceName, 'ModifyNamespace', user); }, remove_modify_namespace_env_role: function (appId, env, namespaceName, user) { return removeNamespaceEnvRoleFromUser(appId, env, namespaceName, 'ModifyNamespace', user); }, remove_release_namespace_role: function (appId, namespaceName, user) { return removeNamespaceRoleFromUser(appId, namespaceName, 'ReleaseNamespace', user); }, remove_release_namespace_env_role: function (appId, env, namespaceName, user) { return removeNamespaceEnvRoleFromUser(appId, env, namespaceName, 'ReleaseNamespace', user); }, get_namespace_role_users: function (appId, namespaceName) { var d = $q.defer(); permission_resource.get_namespace_role_users({ appId: appId, namespaceName: namespaceName, }, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; }, get_namespace_env_role_users: function (appId, env, namespaceName) { var d = $q.defer(); permission_resource.get_namespace_env_role_users({ appId: appId, namespaceName: namespaceName, env: env }, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; }, get_app_role_users: function (appId) { var d = $q.defer(); permission_resource.get_app_role_users({ appId: appId }, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; }, assign_master_role: function (appId, user) { var d = $q.defer(); permission_resource.assign_app_role_to_user({ appId: appId, roleType: 'Master' }, user, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; }, remove_master_role: function (appId, user) { var d = $q.defer(); permission_resource.remove_app_role_from_user({ appId: appId, roleType: 'Master', user: user }, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; }, has_open_manage_app_master_role_limit: function () { var d = $q.defer(); permission_resource.has_open_manage_app_master_role_limit({}, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; }, has_modify_cluster_ns_permission: function (appId, env, clusterName) { return hasClusterNsPermission(appId, env, clusterName, 'ModifyNamespacesInCluster'); }, has_release_cluster_ns_permission: function (appId, env, clusterName) { return hasClusterNsPermission(appId, env, clusterName, 'ReleaseNamespacesInCluster'); }, get_cluster_ns_role_users: function (appId, env, clusterName) { var d = $q.defer(); permission_resource.get_cluster_ns_role_users({ appId: appId, env: env, clusterName: clusterName }, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; }, assign_modify_cluster_ns_role: function (appId, env, clusterName, user) { return assignClusterNsRoleToUser(appId, env, clusterName, 'ModifyNamespacesInCluster', user); }, assign_release_cluster_ns_role: function (appId, env, clusterName, user) { return assignClusterNsRoleToUser(appId, env, clusterName, 'ReleaseNamespacesInCluster', user); }, remove_modify_cluster_ns_role: function (appId, env, clusterName, user) { return removeClusterNsRoleFromUser(appId, env, clusterName, 'ModifyNamespacesInCluster', user); }, remove_release_cluster_ns_role: function (appId, env, clusterName, user) { return removeClusterNsRoleFromUser(appId, env, clusterName, 'ReleaseNamespacesInCluster', user); }, } }]); ================================================ FILE: apollo-portal/src/main/resources/static/scripts/services/ReleaseHistoryService.js ================================================ /* * Copyright 2024 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ appService.service('ReleaseHistoryService', ['$resource', '$q', 'AppUtil', function ($resource, $q, AppUtil) { var resource = $resource('', {}, { find_release_history_by_namespace: { method: 'GET', url: AppUtil.prefixPath() + '/apps/:appId/envs/:env/clusters/:clusterName/namespaces/:namespaceName/releases/histories', isArray: true } }); function findReleaseHistoryByNamespace(appId, env, clusterName, namespaceName, page, size) { var d = $q.defer(); resource.find_release_history_by_namespace({ appId: appId, env: env, clusterName: clusterName, namespaceName: namespaceName, page: page, size: size }, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; } return { findReleaseHistoryByNamespace: findReleaseHistoryByNamespace } }]); ================================================ FILE: apollo-portal/src/main/resources/static/scripts/services/ReleaseService.js ================================================ /* * Copyright 2024 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ appService.service('ReleaseService', ['$resource', '$q','AppUtil', function ($resource, $q,AppUtil) { var resource = $resource('', {}, { get: { method: 'GET', url: AppUtil.prefixPath() + '/envs/:env/releases/:releaseId' }, find_all_releases: { method: 'GET', url: AppUtil.prefixPath() + '/apps/:appId/envs/:env/clusters/:clusterName/namespaces/:namespaceName/releases/all', isArray: true }, find_active_releases: { method: 'GET', url: AppUtil.prefixPath() + '/apps/:appId/envs/:env/clusters/:clusterName/namespaces/:namespaceName/releases/active', isArray: true }, compare: { method: 'GET', url: AppUtil.prefixPath() + '/envs/:env/releases/compare' }, release: { method: 'POST', url: AppUtil.prefixPath() + '/apps/:appId/envs/:env/clusters/:clusterName/namespaces/:namespaceName/releases' }, gray_release: { method: 'POST', url: AppUtil.prefixPath() + '/apps/:appId/envs/:env/clusters/:clusterName/namespaces/:namespaceName/branches/:branchName/releases' }, rollback: { method: 'PUT', url: AppUtil.prefixPath() + "/envs/:env/releases/:releaseId/rollback" } }); function createRelease(appId, env, clusterName, namespaceName, releaseTitle, comment, isEmergencyPublish) { var d = $q.defer(); resource.release({ appId: appId, env: env, clusterName: clusterName, namespaceName: namespaceName }, { releaseTitle: releaseTitle, releaseComment: comment, isEmergencyPublish: isEmergencyPublish }, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; } function createGrayRelease(appId, env, clusterName, namespaceName, branchName, releaseTitle, comment, isEmergencyPublish) { var d = $q.defer(); resource.gray_release({ appId: appId, env: env, clusterName: clusterName, namespaceName: namespaceName, branchName: branchName }, { releaseTitle: releaseTitle, releaseComment: comment, isEmergencyPublish: isEmergencyPublish }, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; } function get(env, releaseId) { var d = $q.defer(); resource.get({ env: env, releaseId: releaseId }, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; } function findAllReleases(appId, env, clusterName, namespaceName, page, size) { var d = $q.defer(); resource.find_all_releases({ appId: appId, env: env, clusterName: clusterName, namespaceName: namespaceName, page: page, size: size }, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; } function findActiveReleases(appId, env, clusterName, namespaceName, page, size) { var d = $q.defer(); resource.find_active_releases({ appId: appId, env: env, clusterName: clusterName, namespaceName: namespaceName, page: page, size: size }, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; } function findLatestActiveRelease(appId, env, clusterName, namespaceName) { var d = $q.defer(); resource.find_active_releases({ appId: appId, env: env, clusterName: clusterName, namespaceName: namespaceName, page: 0, size: 1 }, function (result) { if (result && result.length) { d.resolve(result[0]); } d.resolve(undefined); }, function (result) { d.reject(result); }); return d.promise; } function compare(env, baseReleaseId, toCompareReleaseId) { var d = $q.defer(); resource.compare({ env: env, baseReleaseId: baseReleaseId, toCompareReleaseId: toCompareReleaseId }, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; } function rollback(env, releaseId) { var d = $q.defer(); resource.rollback({ env: env, releaseId: releaseId }, {}, function (result) { d.resolve(result); }, function (result) { d.reject(result); } ); return d.promise; } function rollbackTo(env, releaseId, toReleaseId) { var d = $q.defer(); resource.rollback({ env: env, releaseId: releaseId, toReleaseId: toReleaseId }, {}, function (result) { d.resolve(result); }, function (result) { d.reject(result); } ); return d.promise; } return { publish: createRelease, grayPublish: createGrayRelease, get: get, findAllRelease: findAllReleases, findActiveReleases: findActiveReleases, findLatestActiveRelease: findLatestActiveRelease, compare: compare, rollback: rollback, rollbackTo: rollbackTo } }]); ================================================ FILE: apollo-portal/src/main/resources/static/scripts/services/ServerConfigService.js ================================================ /* * Copyright 2024 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ appService.service('ServerConfigService', ['$resource', '$q', 'AppUtil', function ($resource, $q, AppUtil) { let server_config_resource = $resource('', {}, { create_portal_db_config: { method: 'POST', url: AppUtil.prefixPath() + '/server/portal-db/config' }, create_config_db_config: { method: 'POST', url: AppUtil.prefixPath() + '/server/envs/:env/config-db/config' }, find_portal_db_config: { method: 'GET', isArray: true, url: AppUtil.prefixPath() + '/server/portal-db/config/find-all-config' }, find_config_db_config: { method: 'GET', isArray: true, url: AppUtil.prefixPath() + '/server/envs/:env/config-db/config/find-all-config' } }); return { createPortalDBConfig: function (serverConfig) { let d = $q.defer(); server_config_resource.create_portal_db_config({}, serverConfig, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; }, createConfigDBConfig: function (env, serverConfig) { let d = $q.defer(); server_config_resource.create_config_db_config({env:env}, serverConfig, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; }, findPortalDBConfig:function (){ let d = $q.defer(); server_config_resource.find_portal_db_config({ }, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; }, findConfigDBConfig:function (env){ let d = $q.defer(); server_config_resource.find_config_db_config({env: env}, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; } } }]); ================================================ FILE: apollo-portal/src/main/resources/static/scripts/services/SystemInfoService.js ================================================ /* * Copyright 2024 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ appService.service('SystemInfoService', ['$resource', '$q', 'AppUtil', function ($resource, $q, AppUtil) { var system_info_resource = $resource('', {}, { load_system_info: { method: 'GET', url: AppUtil.prefixPath() + '/system-info' }, check_health: { method: 'GET', url: AppUtil.prefixPath() + '/system-info/health' } }); return { load_system_info: function () { var d = $q.defer(); system_info_resource.load_system_info({}, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; }, check_health: function (instanceId, host) { var d = $q.defer(); system_info_resource.check_health({ instanceId: instanceId }, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; } } }]); ================================================ FILE: apollo-portal/src/main/resources/static/scripts/services/SystemRoleService.js ================================================ /* * Copyright 2024 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ appService.service('SystemRoleService', ['$resource', '$q', 'AppUtil', function ($resource, $q,AppUtil) { var system_role_service = $resource('', {}, { add_create_application_role: { method: 'POST', url: AppUtil.prefixPath() + '/system/role/createApplication' }, delete_create_application_role: { method: 'DELETE', url: AppUtil.prefixPath() + '/system/role/createApplication/:userId' }, get_create_application_role_users: { method: 'GET', url: AppUtil.prefixPath() + '/system/role/createApplication', isArray: true }, has_open_manage_app_master_role_limit: { method: 'GET', url: AppUtil.prefixPath() + '/system/role/manageAppMaster' } }); return { add_create_application_role: function (userId) { var finished = false; var d = $q.defer(); system_role_service.add_create_application_role([ userId ], function (result) { finished = true; d.resolve(result); }, function (result) { finished = true; d.reject(result); }); return d.promise; }, delete_create_application_role: function (userId) { var finished = false; var d = $q.defer(); system_role_service.delete_create_application_role({ "userId" : userId }, function (result) { finished = true; d.resolve(result); }, function (result) { finished = true; d.reject(result); }); return d.promise; }, get_create_application_role_users: function () { var finished = false; var d = $q.defer(); system_role_service.get_create_application_role_users({}, function (result) { finished = true; d.resolve(result); }, function (result) { finished = true; d.reject(result); }); return d.promise; }, has_open_manage_app_master_role_limit: function () { var finished = false; var d = $q.defer(); system_role_service.has_open_manage_app_master_role_limit({}, function (result) { finished = true; d.resolve(result); }, function (result) { finished = true; d.reject(result); }); return d.promise; } } }]); ================================================ FILE: apollo-portal/src/main/resources/static/scripts/services/UserService.js ================================================ /* * Copyright 2024 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ appService.service('UserService', ['$resource', '$q', 'AppUtil', function ($resource, $q, AppUtil) { const user_resource = $resource('', {}, { load_user: { method: 'GET', url: AppUtil.prefixPath() + '/user' }, find_users: { method: 'GET', isArray: true, url: AppUtil.prefixPath() + '/users?keyword=:keyword&includeInactiveUsers=:includeInactiveUsers&offset=:offset&limit=:limit' }, change_user_enabled: { method: 'PUT', url: AppUtil.prefixPath() + '/users/enabled' }, create_or_update_user: { method: 'POST', url: AppUtil.prefixPath() + '/users?isCreate=:isCreate' } }); return { load_user: function () { var finished = false; var d = $q.defer(); user_resource.load_user({}, function (result) { finished = true; d.resolve(result); }, function (result) { finished = true; d.reject(result); }); return d.promise; }, find_users: function (keyword, includeInactiveUsers) { var d = $q.defer(); user_resource.find_users({ keyword: keyword, includeInactiveUsers: includeInactiveUsers }, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; }, change_user_enabled: function (user) { var d = $q.defer(); user_resource.change_user_enabled({}, user, function (result) { d.resolve(result) }, function (result) { d.reject(result); }); return d.promise; }, createOrUpdateUser: function (isCreate, user) { var d = $q.defer(); user_resource.create_or_update_user({ isCreate: isCreate }, user, function (result) { d.resolve(result); }, function (result) { d.reject(result); }); return d.promise; } } }]); ================================================ FILE: apollo-portal/src/main/resources/static/scripts/valdr.js ================================================ /* * Copyright 2024 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ app_module.config(appValdr); setting_module.config(appValdr); function appValdr(valdrProvider) { valdrProvider.addConstraints({ 'App': { 'appId': { 'size': { 'max': 64, 'message': 'Valdr.App.AppId.Size' }, 'required': { 'message': 'Valdr.App.AppId.Required' } }, 'appName': { 'size': { 'max': 128, 'message': 'Valdr.App.appName.Size' }, 'required': { 'message': 'Valdr.App.appName.Required' } } } }) } cluster_module.config(function (valdrProvider) { valdrProvider.addConstraints({ 'Cluster': { 'clusterName': { 'size': { 'max': 32, 'message': 'Valdr.Cluster.ClusterName.Size' }, 'required': { 'message': 'Valdr.Cluster.ClusterName.Required' } } } }) }); namespace_module.config(function (valdrProvider) { valdrProvider.addConstraints({ 'AppNamespace': { 'namespaceName': { 'size': { 'max': 32, 'message': 'Valdr.AppNamespace.NamespaceName.Size' }, 'required': { 'message': 'Valdr.AppNamespace.NamespaceName.Required' } }, 'comment': { 'size': { 'max': 64, 'message': 'Valdr.AppNamespace.Comment.Size' } } } }) }); application_module.config(function (valdrProvider) { valdrProvider.addConstraints({ 'Item': { 'key': { 'size': { 'max': 128, 'message': 'Valdr.Item.Key.Size' }, 'required': { 'message': 'Valdr.Item.Key.Required' } }, 'comment': { 'size': { 'max': 256, 'message': 'Valdr.Item.Comment.Size' } } }, 'Release': { 'releaseName': { 'size': { 'max': 64, 'message': 'Valdr.Release.ReleaseName.Size' }, 'required': { 'message': 'Valdr.Release.ReleaseName.Size' } }, 'comment': { 'size': { 'max': 256, 'message': 'Valdr.Release.Comment.Size' } } } }) }); ================================================ FILE: apollo-portal/src/main/resources/static/server_config_manage.html ================================================ {{'ServiceConfig.Title' | translate }}
    {{'ServiceConfig.Title' | translate }} {{'ServiceConfig.PortalDB.Tips' | translate }}
    {{'Config.Key' | translate }} {{'Config.Value' | translate }} {{'Config.Comment' | translate }} {{'Config.Operation' | translate }}
    {{ config.key }} {{ config.value }} {{ config.comment }} {{'UserMange.Edit' | translate }}
    {{'ServiceConfig.Title' | translate }} {{'ServiceConfig.ConfigDB.Tips' | translate }}

    {{'Config.Key' | translate }} {{'Config.Value' | translate }} {{'Config.Comment' | translate }} {{'Config.Operation' | translate }}
    {{ config.key }} {{ config.value }} {{ config.comment }} {{'UserMange.Edit' | translate }}
    ================================================ FILE: apollo-portal/src/main/resources/static/styles/audit-log.css ================================================ /* * Copyright 2024 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ #audit-not-enabled-prompt { background-color: #ffffff; /*实现垂直居中*/ align-items: center; /*实现水平居中*/ justify-content: center; } #audit-not-enabled-prompt .display-text { padding: 15px; margin-left: 30px; margin-right: 30px; box-shadow: 0 2px 4px rgba(0, 0, 0, .12), 0 0 6px rgba(0, 0, 0, .09); } /*audit-menu page*/ #audit-menu h5 { word-wrap: break-word; word-break: break-all; } #audit-menu { background-color: #ffffff; /*实现垂直居中*/ align-items: center; /*实现水平居中*/ justify-content: center; } #audit-menu .main-table { padding-top: 15px; width: 100%; min-width: 500px; } #audit-menu .more-img { width: 20px; height: 20px; } #audit-menu .operate-panel { position: absolute; top: 5px; right: 20px; } #audit-menu .display-table { padding: 15px; margin-left: 30px; margin-right: 30px; box-shadow: 0 2px 4px rgba(0, 0, 0, .12), 0 0 6px rgba(0, 0, 0, .09); } #audit-menu .search-bar { padding: 15px; margin-left: 30px; margin-right: 30px; box-shadow: 0 2px 4px rgba(0, 0, 0, .12), 0 0 6px rgba(0, 0, 0, .09); } #audit-menu table { width: 100%; } #audit-menu tr { } #audit-menu th { border-bottom: 1px solid #E4E7ED; padding: 15px 15px 15px 15px; font-size: 16px; text-align: left; word-break: break-all; } #audit-menu td { border-bottom: 1px solid #E4E7ED; padding: 15px 15px 15px 15px; font-size: 14px; word-break: break-all; } .data-influence { display: table-row; padding: 8px 8px 8px 8px; font-size: 16px; color: #292a29; } .audit-field:hover{ background-color: #f5f5f5; } #dataInfluenceEntity { padding: 8px; margin-top: 8px; box-shadow: 0 2px 4px rgba(0, 0, 0, .12), 0 0 6px rgba(0, 0, 0, .09); border-radius: 0%; } .options-container { max-height: 150px; /* 设置最大高度,可根据需求调整 */ overflow-y: auto; /* 启用垂直滚动条 */ border: 1px solid #ccc; /* 添加边框,可根据需要自定义样式 */ border-top: 0; /* 避免重叠边框 */ background: #fff; /* 背景颜色,可根据需要调整 */ z-index: 1000; } .options-list { list-style: none; padding: 0; margin: 0; } .options-list li { padding: 5px; cursor: pointer; } .options-list li:hover { background-color: #f0f0f0; } .treeview { background-color: #ffffff; font-size: 14px; } .node-treeview { padding: 5px 0px; transition: background-color 0.5s; word-break: break-all; } .node-treeview:last-child { border-bottom: none; } .node-treeview:hover { background-color: #f0f0f0; /* 悬停时的背景颜色 */ } #treeview .list-group .list-group-item { border: 0; border-top: 1px solid #eff2f7; border-top-width: 0px; color: #797979; } ================================================ FILE: apollo-portal/src/main/resources/static/styles/common-style.css ================================================ /* * Copyright 2024 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ html, body { height: 100%; scroll-behavior: smooth; } body { min-width: 960px; color: #797979; padding: 0 !important; margin: 0 !important; font-size: 13px; background: #f1f2f7; font-family: 'Open Sans', sans-serif; } body.modal-open { overflow: visible; } a { cursor: pointer; } p, td, span { word-wrap: break-word; word-break: break-all; } .modal { overflow-y: scroll } .no-radius { border-radius: 0; } .no-border { border: 0; } .no-margin { margin: 0; } .cursor-pointer { cursor: pointer; } .word-break { word-wrap: break-word; word-break: break-all; } .border { border: solid 1px #c3c3c3; } .padding-top-5 { padding-top: 5px; } .border-top { border-top: 1px solid #ddd; } .bg-info, .bg-primary, .bg-warning, .bg-danger, .bg-success { padding: 10px; } .active { background: #f5f5f5; } .label-default-light { background: #A4A4A4 } .panel-default .panel-heading { color: #797979; } pre { white-space: pre-wrap; /* Since CSS 2.1 */ white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ white-space: -pre-wrap; /* Opera 4-6 */ white-space: -o-pre-wrap; /* Opera 7 */ word-wrap: break-word; /* Internet Explorer 5.5+ */ } .pre { white-space: pre; } .hover:hover { background: #f5f5f5; cursor: pointer } .highlight { background: #ffa; } .hide-border-top { border-top: 0; } .logo { font-size: 25px; } .i-20 { height: 20px; width: 20px; } .i-25-20 { height: 20px; width: 25px; } .i-15 { height: 15px; width: 15px; } .badge { padding: 1px 4px; } .badge-grey { background: #777; color: #fff; } .badge-white { background: #ffffff; color: #6c6c6c; } .modal-dialog { width: 960px; } .apollo-container { min-height: 90%; } .navbar-default { background-color: #ffffff; border: none; } .footer { display: inline-block; width: 100%; height: 20px; line-height: 20px; position: relative; } .footer .footer-repo { position: relative; text-decoration: none; height: 100%; } .footer .footer-repo .footer-repo-img { position: absolute; top: 2px; height: 16px; } .footer .footer-repo .footer-repo-name { color: #3c3c3c; margin-left: 18px; } /*panel*/ .panel { border: 1px solid #ddd; } table th { text-align: center; } /*首页*/ .site-notice { padding: 5px 0; text-align: center; background-color: #208d4e; } .site-notice { color: #eee; } .site-notice a { color: #ffffff; } .site-notice a:hover { text-underline: none; } .site-notice .selected { color: #000000; } .site-header { position: relative; text-align: center; background-color: #27AE60; color: #fff; margin-bottom: 0; } .site-header .search { border: 2px solid #27AE60; -webkit-box-shadow: none; box-shadow: none; font-size: 16px; padding: 13px 30px; border-radius: 0; height: auto; text-align: center; } .site-header h1 { font-size: 56px; text-shadow: -5px 5px 0 rgba(0, 0, 0, 0.1); } .site-header span { font-size: 14px; } .list-group { margin-top: 20px; } .side-bar { position: absolute; width: 195px; top: 85px; left: 15px; margin-bottom: 25px; background: #f1f2f7; z-index: 2; } .position-absolute { position: absolute; } .position-fix { position: fixed; } .view-mode-1 { margin-left: 235px; padding-right: 15px; } .view-mode-2 { padding: 0 15px; } .side-bar-switch { padding: 10px 10px; margin-right: 30px; } .node-treeview { color: #797979; } .apps .apps-description { color: gray; font-size: 16px; } .app { padding-bottom: 75px; overflow: hidden; } .app td, th { display: table-cell; vertical-align: inherit; } .project-info { width: 100%; } .panel-heading { border-color: #eff2f7; font-size: 16px; font-weight: 300; } .panel-heading .header-namespace { min-width: 415px; } .panel-heading .header-buttons { min-width: 405px; } #treeview .list-group { margin: 0; } #treeview .list-group .list-group-item { border: 0; border-top: 1px solid #eff2f7; } .project-info th { text-align: right; padding: 4px 2px; width: 5em; } .project-info td { border-bottom: 1px dotted gray; padding: 4px 6px; } .project-info td span { margin-right: 5px; } #config-info { min-height: 500px; } #config-edit { border: 1px solid #ddd; border-radius: 3px; } #config-edit .panel-heading { border-bottom: 1px solid #ddd; } .tocify-header { font-size: 14px; } .tocify-subheader { font-size: 13px; } .config-item-container .panel { border-radius: 0; } .config-item-container .panel-heading b { font-size: 18px; } .config-item-container .form-control[disabled] { background: #ffffff; border: 0; } .config-item-container .second-panel-heading .ns_btn { width: 25px; height: 25px; border-top: solid 1px #ffffff; } .config-item-container .second-panel-heading .nav-tabs .node_active { border-bottom: 3px #1b6d85 solid; } .config-item-container .config-items { height: 500px; overflow: scroll; } .config-item-container .panel-heading button img { width: 12px; height: 12px; } .config-item-container .panel-heading a img { width: 12px; height: 12px; } .config-item-container .panel-heading li img { width: 12px; height: 12px; } .config-item-container .second-panel-heading { height: 45px; } .config-item-container .second-panel-heading a { height: 35px; color: #555; font-size: 13px; margin-bottom: 2px; } .config-item-container .second-panel-heading .text-fullscreen-icon { border: 0; margin: 0; padding: 0; vertical-align: top; } .namespace-panel { border-top: 0; border-bottom: 0; } .namespace-panel .namespace-name { font-size: 20px; } .namespace-panel .namespace-label { margin-left: 5px; } .namespace-panel .namespace-attribute-panel { margin-left: 0; color: #fff; border-top: 0; background: #f1f2f7; } .namespace-panel .namespace-attribute-public { margin-right:5px; } .namespace-panel .second-panel-heading .nav-tabs { border-bottom: 0; } .namespace-panel .namespace-view-table td img { cursor: pointer; width: 23px; height: 23px; } .namespace-panel .namespace-view-table table { table-layout: inherit; } .namespace-panel .namespace-view-table td { word-wrap: break-word; } .namespace-panel .namespace-view-table .glyphicon { cursor: pointer; } .namespace-panel .namespace-view-table .search-input { padding: 15px 0 15px 10px; } .namespace-panel .namespace-view-table .search-input input { width: 500px; } .namespace-panel .no-config-panel { padding: 15px 0; } .namespace-panel .history-view { padding: 10px 20px; } .namespace-text-editor-container:fullscreen, .namespace-text-editor-container:-webkit-full-screen, .namespace-text-editor-container:-moz-full-screen, .namespace-text-editor-container:-ms-fullscreen { background: #fff; box-sizing: border-box; display: block; width: 100vw; height: 100vh; max-width: 100vw; max-height: 100vh; margin: 0; overflow: auto; padding: 12px; } .namespace-text-editor-container:fullscreen::backdrop { background: #fff; } .namespace-panel .instance-view .btn-primary .badge { color: #337ab7; background-color: #fff; } .namespace-panel .instance-view .btn-default .badge { background: #777; color: #fff; } .namespace-panel .rules-manage-view { padding: 45px 20px; } .line { width: 20px; border: 1px solid #ddd; } .editable-table > tbody > tr > td { padding: 4px } .editable-text { padding-left: 4px; padding-top: 4px; padding-bottom: 4px; display: inline-block; } .editable-table tbody > tr > td > .controls { / / width: 100 % } .editable-input { padding-left: 3px; } .editable-input.input-sm { height: 30px; font-size: 14px; padding-top: 4px; padding-bottom: 4px; } .list-group-item .btn-title { color: gray; font-size: 14px; margin: 0; } .list-group-item .icon-project-manage { background: url(../img/manage.png) no-repeat; } .list-group-item .icon-accesskey-manage { background: url(../img/secret.png) no-repeat; } .list-group-item .icon-plus-orange { background: url(../img/add.png) no-repeat; } .list-group-item .icon-text { background-size: 20px; background-position: 5% 50%; padding: 5px 0 5px 50px; } /*搜索框*/ ::-webkit-scrollbar { /*滚动条整体样式*/ width: 8px; /*高宽分别对应横竖滚动条的尺寸*/ height: 8px; } ::-webkit-scrollbar-thumb { /*滚动条里面小方块*/ border-radius: 10px; background-color: rgba(178, 178, 178, 0.8); } ::-webkit-scrollbar-thumb:hover { background-color: rgba(178, 178, 178, 1); } .app-list { width: 350px; height: 200px; position: absolute; margin-left: 0; cursor: pointer; background: #ffffff; border: 1px solid #ddd; overflow-y: scroll; z-index: 1000; } .app-list .app-item { font-size: medium; padding: 5px 10px; } .app-list .app-item:hover { color: #ffffff; background: #C3C3C3; } .app-list .app-selected { color: #ffffff; background: #c3c3c3; } .item-container { border: solid 1px #f1f2f7; margin-top: 15px; padding: 20px 15px } .item-container .item-info { margin-left: 5px; } .change-type-mark { width: 5px; height: 5px; } .release-history .media-body { padding-left: 20px; } .release-history .panel-body .load-more { margin-top: 20px; } .release-history .media-body textarea { margin-top: 10px; } .release-history .release-history-container { padding: 0; } .release-history .release-history-list { max-height: 750px; padding: 0; border-right: solid 1px #eff2f7; overflow: scroll; } .release-history .release-history-list .media { position: relative; margin: 0; padding: 10px; border-bottom: solid 1px #eff2f7; } .release-history .release-history-list .release-operation { position: absolute; right: 0; top: 0; width: 5px; height: 100%; } .release-history .release-history-list .media .media-left { padding-top: 10px; } .release-history .release-history-list .media .media-body .release-title { padding: 0; } .release-history .release-history-list .emergency-publish { position: absolute; left: 0; top: 0; } .release-history .release-history-list .load-more { height: 45px; background: #f5f5f5; } .release-history .release-operation-normal { background: #316510; } .release-history .release-operation-rollback { background: #997f1c; } .release-history .release-operation-gray { background: #999999; } .release-history .operation-caption-container { position: relative; } .release-history .section-title { padding: 15px 10px 0 10px; } .release-history .operation-caption { position: absolute; top: 45px; width: 100px; height: 18px; color: #fff; font-size: 12px; } .release-history .panel-heading .back-btn { position: absolute; top: 45px; right: 10px; } .release-history .release-info { padding: 0; border: 0; } .release-history .panel-heading { padding: 15px; } .release-history .panel-heading button img { width: 12px; height: 12px; } .empty-container { padding: 15px; } .valdr-message { display: none; } .valdr-message.ng-dirty.ng-invalid.ng-touched { display: inline; color: #a94442; } .form-group .form-control.ng-invalid.ng-dirty.ng-touched { border-color: #a94442; } .app-not-found { padding-top: 50px; font-size: 18px; } /*index page*/ #app-list h5 { word-wrap: break-word; word-break: break-all; } #app-list { display: flex; background-color: #ffffff; width: 100vw; } #app-list .left-bar { width: 15%; min-width: 200px; } #app-list .main-table { padding-top: 15px; width: 85%; min-width: 500px; } #app-list .media-body { padding-top: 15px; } #app-list .media { background: #fff; display: table; } #app-list .media-left { min-width: 200px; max-width: 400px; color: #fff; display: table-cell; vertical-align: middle; } #app-list .more-img { width: 20px; height: 20px; } #app-list .app-panel { position: relative; height: 100px; } #app-list .operate-panel { position: absolute; top: 5px; right: 20px; } #app-list .display-table { padding: 15px; margin-left: 30px; margin-right: 30px; box-shadow: 0 2px 4px rgba(0, 0, 0, .12), 0 0 6px rgba(0, 0, 0, .09); } #app-list table { width: 100%; } #app-list tr { } #app-list th { border-bottom: 1px solid #E4E7ED; padding: 15px 8px 15px 8px; font-size: 16px; text-align: left; word-break: break-all; } #app-list td { border-bottom: 1px solid #E4E7ED; padding: 15px 8px 15px 8px; font-size: 14px; word-break: break-all; } .left-bar h5 { font-size: 15px; margin-left: 12px; } .left-bar .app-list-show { height: 60px; color: #337AB7; display: flex; align-items: center; justify-content: left; padding-left: 25px; border-right: 1px solid #DCDFE6; cursor: pointer; } .left-bar .app-list-choose { height: 60px; display: flex; align-items: center; justify-content: left; padding-left: 25px; border-right: 1px solid #DCDFE6; cursor: pointer; } .left-bar .app-list-choose:hover { background-color: #F2F6FC; } .left-bar .favorite-list-show { height: 60px; color: #337AB7; display: flex; align-items: center; justify-content: left; padding-left: 25px; border-right: 1px solid #DCDFE6; cursor: pointer; } .left-bar .favorite-list-choose { height: 60px; display: flex; align-items: center; justify-content: left; padding-left: 25px; border-right: 1px solid #DCDFE6; cursor: pointer; } .left-bar .favorite-list-choose:hover { background-color: #F2F6FC; } .left-bar .public-namespace-list-show { height: 60px; color: #337AB7; display: flex; align-items: center; justify-content: left; padding-left: 25px; border-right: 1px solid #DCDFE6; cursor: pointer; } .left-bar .public-namespace-list-choose { height: 60px; display: flex; align-items: center; justify-content: left; padding-left: 25px; border-right: 1px solid #DCDFE6; cursor: pointer; } .left-bar .public-namespace-list-choose:hover { background-color: #F2F6FC } .left-bar .visited-list-show { height: 60px; color: #337AB7; display: flex; align-items: center; justify-content: left; padding-left: 25px; border-right: 1px solid #DCDFE6; cursor: pointer; } .left-bar .visited-list-choose { height: 60px; display: flex; align-items: center; justify-content: left; padding-left: 25px; border-right: 1px solid #DCDFE6; cursor: pointer; } .left-bar .visited-list-choose:hover { background-color: #F2F6FC } .wapper { width: 600px; padding: 15px 0 30px 30px; } .select-wapper { width: 500px; height: 40px; position: relative; } .select-wapper>input { width: 100%; height: 100%; border: 1px solid #CCCCCC; font-size: 15px; padding: 0 10px 0 0; box-sizing: border-box; border-radius: 4px; padding: 5px; -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075); box-shadow: inset 0 1px 1px rgba(0,0,0,.075); -webkit-transition: border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s; -o-transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s; transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s; } .select-wapper>input:focus { border: 1px solid rgb(102,175,233); -webkit-box-shadow: inset 0 1px 1px rgba(102,175,233, .075); box-shadow: inset 0 1px 1px rgba(122,156,211, .075); outline: none; } .select-wapper:after { content: ''; width: 0; height: 0; border-top: 8px solid #77705d; border-right: 5px solid transparent; border-left: 5px solid transparent; display: inline; position: absolute; right: 8px; top: 11px; } .select-wapper .select-content-panel { width: 100%; height: auto; max-height: 300px; position: absolute; top: 100%; left: 0; border: 1px solid rgb(122,156,211); margin-top: 0; padding: 0; overflow-y: auto; box-sizing: border-box; background-color: white; } .select-wapper .select-content-panel li { display: block; list-style: none; padding: 2px 10px; font-size: 14px; height: 30px; border: 0 !important; } .select-wapper .select-content-panel li:hover { background-color: rgb(30, 144, 255); color: white !important; } .select-wapper .select-content-panel li:active { background-color: rgb(30, 144, 255); color: white !important; } .select-wapper .item-bg { background-color: rgb(30, 144, 255); color: white !important; } .select-wapper .hidden-cls { display: none; } .create-app-list .media-left { background: #a9d96c; } .create-app-list .create-btn-panel { padding-right: 35px; padding-bottom: 15px; } .create-app-list .create-btn img { width: 16px; height: 16px; } .favorites-app-list .media-left { background: #57c8f2; } .favorites-app-list .no-favorites { padding-top: 75px; } .public-namespace-list .media-left { background: #65c294; } .public-namespace-list .no-public { padding-bottom: 15px; } .visit-app-list .media-left { background: #41cac0; } .homepage-loading-more-panel { margin-top: 20px; padding-right: 30px; padding-left: 30px; } #rulesModal .rules-ip-selector { width: 500px; height: 50px; } #rulesModal textarea { width: 500px; margin-bottom: 5px; } #rulesModal .rule-edit-panel { padding: 15px 0; } #rulesModal .add-rule { margin-left: 15px; } #rulesModal .select2-container .select2-search__field:not([placeholder='']) { width: auto !important; } .search-onblur { width: 165px; background: #f5f5f5; } .search-focus { width: 165px; background: #fff; } .project-setting .panel-body { padding-top: 35px; } .project-setting .panel-body .context { padding-left: 30px; padding-right: 30px; } .app-search-list .select2-container, .app-search-list .select2-container .select2-selection { height: 34px; } .app-search-list .select2-container .select2-selection .select2-selection__rendered { line-height: 34px; font-size: 14px; } #app-search-list { width: 375px; } /*backTop component style*/ .back-top { position: fixed; height: 40px; width: 40px; border-radius: 4px; color: #ffffff; z-index: 5; cursor: pointer; right: 40px; bottom: 40px; text-align: center; line-height: 40px; font-size: 16px; background: #0c4c7f; opacity: .2; user-select: none; } .back-top:hover { opacity: .4; } .back-top:active { opacity: .6; } .navbar-nav .nav-switch-lang { width: 12px; height: 12px; margin-top: -4px; margin-right: -5px; } .navbar-header .logo { height: 35px; margin-top: -4px } .margin-top10 { margin-top:10px; } #consumer-list th { border-bottom: 1px solid #E4E7ED; padding: 15px 8px 15px 8px; font-size: 16px; text-align: left; word-break: break-all; } #consumer-list td { border-bottom: 1px solid #E4E7ED; padding: 15px 8px 15px 8px; font-size: 14px; word-break: break-all; } #consumer-list table { margin-left: 20px; } #consumer-list .more-img { width: 20px; height: 20px; } #consumer-list .create-btn img { width: 16px; height: 16px; } .left-overflow { overflow-x: auto; text-align: left; } .table-fixed{ table-layout: fixed; } .block { display: block; } .cluster-info-panel { margin: 20px 0px; } ================================================ FILE: apollo-portal/src/main/resources/static/system-role-manage.html ================================================ {{'SystemRole.Title' | translate }}
    {{'SystemRole.AddCreateAppRoleToUser' | translate }} {{'SystemRole.AddCreateAppRoleToUserTips' | translate }}

    {{'SystemRole.AuthorizedUser' | translate }}
    {{'SystemRole.ModifyAppAdminUser' | translate }} {{'SystemRole.ModifyAppAdminUserTips' | translate }}

    {{'SystemRole.Title' | translate }}

    {{'Common.IsRootUser' | translate }}

    ================================================ FILE: apollo-portal/src/main/resources/static/system_info.html ================================================ {{'SystemInfo.Title' | translate }}

    {{'SystemInfo.Title' | translate }}

    {{'SystemInfo.SystemVersion' | translate }}: {{systemInfo.version}}

    {{'Common.Environment' | translate }}: {{env.env}}

    {{'SystemInfo.Active' | translate }}: {{env.active}} {{'SystemInfo.ActiveTips' | translate }}
    {{'SystemInfo.MetaServerAddress' | translate }}: {{env.metaServerAddress}}

    {{'SystemInfo.ConfigServices' | translate }}

    {{'SystemInfo.ConfigServices.Name' | translate }} {{'SystemInfo.ConfigServices.InstanceId' | translate }} {{'SystemInfo.ConfigServices.HomePageUrl' | translate }} {{'SystemInfo.ConfigServices.CheckHealth' | translate }}
    {{'SystemInfo.NoConfigServiceTips' | translate }}
    {{service.appName}} {{service.instanceId}} {{service.homepageUrl}} {{'SystemInfo.Check' | translate }}

    {{'SystemInfo.AdminServices' | translate }}

    {{'SystemInfo.AdminServices.Name' | translate }} {{'SystemInfo.AdminServices.InstanceId' | translate }} {{'SystemInfo.AdminServices.HomePageUrl' | translate }} {{'SystemInfo.AdminServices.CheckHealth' | translate }}
    {{'SystemInfo.NoAdminServiceTips' | translate }}
    {{service.appName}} {{service.instanceId}} {{service.homepageUrl}} {{'SystemInfo.Check' | translate }}

    {{'Common.IsRootUser' | translate }}

    ================================================ FILE: apollo-portal/src/main/resources/static/user-manage.html ================================================ {{'UserMange.Title' | translate }}
    {{'UserMange.Title' | translate }} {{'UserMange.TitleTips' | translate }}
    {{'UserMange.UserName' | translate }} {{'UserMange.UserDisplayName' | translate }} {{'UserMange.Email' | translate }} {{'UserMange.Enabled' | translate }} {{'UserMange.Operation' | translate }}
    {{ user.userId }} {{ user.name }} {{ user.email }} {{('UserMange.Enable' | translate)}} {{('UserMange.Disable' | translate)}} {{'UserMange.Edit' | translate }} {{user.enabled === 1 ? ('UserMange.Disable' | translate) : ('UserMange.Enable' | translate) }}
    {{status==='3' ? ('UserMange.Edit' | translate) : ('UserMange.Add' | translate) }}
    {{'UserMange.PwdNotMatch' | translate }}
    {{'UserMange.Enable' | translate }} {{'UserMange.Disable' | translate }}
    ================================================ FILE: apollo-portal/src/main/resources/static/vendor/iconfont/iconfont.css ================================================ @font-face { font-family: "iconfont"; /* Project id 2742392 */ src: url('../iconfont/iconfont.woff2') format('woff2'), url('../iconfont/iconfont.woff') format('woff'), url('../iconfont/iconfont.ttf') format('truetype'); } .iconfont { font-family: "iconfont" !important; font-size: 16px; font-style: normal; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } .icon-icon:before { content: "\e60b"; } .icon-liulan:before { content: "\e663"; } .icon-gonggongziliao:before { content: "\e7d1"; } .icon-02:before { content: "\e64a"; } ================================================ FILE: apollo-portal/src/main/resources/static/vendor/jquery-plugin/jquery.textareafullscreen.js ================================================ /* jQuery Textarea Fullscreen Editor v1.0 Fullscreen text editor plugin for jQuery. :For more details visit http://github.com/CreoArt/jquery.textareafullscreen - CreoArt - http://github.com/CreoArt Licensed under Apache - https://raw.githubusercontent.com/CreoArt/jquery.textareafullscreen/master/LICENSE */ (function($) { "use strict"; var isFullscreen = false, $el, $wrapper, $editor, $icon, $overlay, transitionDuration = 300, sourceWidth, sourceHeight, settings = { overlay: true, maxWidth: '', maxHeight: '' }; var methods = { init: function(opts) { settings = settings || {}; $.extend(true, settings, settings); $.extend(true, settings, opts); $el = $(this); if (!$el.is('textarea')) { $.error( 'Error initializing Textarea Fullscreen Editor Plugin. It can only work on
    ================================================ FILE: apollo-portal/src/main/resources/static/views/component/import-namespace-modal.html ================================================ ================================================ FILE: apollo-portal/src/main/resources/static/views/component/item-modal.html ================================================ ================================================ FILE: apollo-portal/src/main/resources/static/views/component/merge-and-publish-modal.html ================================================ ================================================ FILE: apollo-portal/src/main/resources/static/views/component/multiple-user-selector.html ================================================ ================================================ FILE: apollo-portal/src/main/resources/static/views/component/namespace-panel-branch-tab.html ================================================
    {{'Component.Namespace.Branch.NoPermissionTips' | translate }}
    {{'Component.Namespace.Branch.Body.Item' | translate }}
    {{'Component.Namespace.Branch.Body.PublishState' | translate }} {{'Component.Namespace.Branch.Body.ItemKey' | translate }}  {{'Component.ConfigItem.ItemTypeName' | translate }} {{'Component.Namespace.Branch.Body.ItemMasterValue' | translate }} {{'Component.Namespace.Branch.Body.ItemGrayscaleValue' | translate }} {{'Component.Namespace.Branch.Body.ItemComment' | translate }} {{'Component.Namespace.Branch.Body.ItemLastModify' | translate }} {{'Component.Namespace.Branch.Body.ItemLastModifyTime' | translate }} {{'Component.Namespace.Branch.Body.ItemOperator' | translate }}
    {{'Component.Namespace.Branch.Body.ItemNoPublish' | translate }} {{'Component.Namespace.Branch.Body.ItemPublished' | translate }} {{'Component.Namespace.Branch.Body.Delete' | translate }} {{'Component.Namespace.Branch.Body.Modify' | translate }} {{'Component.Namespace.Branch.Body.Added' | translate }} {{'Component.ConfigItem.ItemTypeString' | translate }} {{'Component.ConfigItem.ItemTypeNumber' | translate }} {{'Component.ConfigItem.ItemTypeBoolean' | translate }} {{'Component.ConfigItem.ItemTypeJson' | translate }}
    {{'Component.Namespace.MasterBranch.Body.Title' | translate }}
    {{'Component.Namespace.MasterBranch.Body.PublishState' | translate }} {{'Component.Namespace.MasterBranch.Body.ItemKey' | translate }}  {{'Component.ConfigItem.ItemTypeName' | translate }} {{'Component.Namespace.MasterBranch.Body.ItemValue' | translate }} {{'Component.Namespace.MasterBranch.Body.ItemComment' | translate }} {{'Component.Namespace.MasterBranch.Body.ItemLastModify' | translate }} {{'Component.Namespace.MasterBranch.Body.ItemLastModifyTime' | translate }} {{'Component.Namespace.MasterBranch.Body.ItemOperator' | translate }}
    {{'Component.Namespace.MasterBranch.Body.ItemNoPublish' | translate }} {{'Component.Namespace.MasterBranch.Body.ItemPublished' | translate }} {{'Component.Namespace.Branch.Body.Added' | translate }} {{'Component.Namespace.Branch.Body.Modify' | translate }} {{'Component.Namespace.Branch.Body.Delete' | translate }} {{'Component.ConfigItem.ItemTypeString' | translate }} {{'Component.ConfigItem.ItemTypeNumber' | translate }} {{'Component.ConfigItem.ItemTypeBoolean' | translate }} {{'Component.ConfigItem.ItemTypeJson' | translate }}
    Tips: {{'Component.Namespace.Branch.GrayScaleRule.NoPermissionTips' | translate }}
    {{'Component.Namespace.Branch.GrayScaleRule.AppId' | translate }} {{'Component.Namespace.Branch.GrayScaleRule.RuleList' | translate }} {{'Component.Namespace.Branch.GrayScaleRule.Operator' | translate }}
    {{'Component.Namespace.Branch.GrayScaleRule.ApplyToAllInstances' | translate }}
    {{'Component.Namespace.Branch.Instance.InstanceAppId' | translate }} {{'Component.Namespace.Branch.Instance.InstanceClusterName' | translate }} {{'Component.Namespace.Branch.Instance.InstanceDataCenter' | translate }} {{'Component.Namespace.Branch.Instance.InstanceIp' | translate }} {{'Component.Namespace.Branch.Instance.InstanceGetItemTime' | translate }}
    {{instance.configs && instance.configs.length ? (instance.configs[0].releaseDeliveryTime | date: 'yyyy-MM-dd HH:mm:ss') : ''}}
    {{'Component.Namespace.Branch.Instance.NoInstance' | translate }}

    {{'Component.Namespace.Branch.History.ItemType' | translate }} {{'Component.Namespace.Branch.History.ItemKey' | translate }} {{'Component.Namespace.Branch.History.ItemOldValue' | translate }} {{'Component.Namespace.Branch.History.ItemNewValue' | translate }} {{'Component.Namespace.Branch.History.ItemComment' | translate }}
    {{'Component.Namespace.Branch.History.NewAdded' | translate }}
    {{'Component.Namespace.Branch.History.Modified' | translate }}
    {{'Component.Namespace.Branch.History.Deleted' | translate }}
    {{'Component.Namespace.Branch.History.ItemType' | translate }} {{'Component.Namespace.Branch.History.ItemOldValue' | translate }} {{'Component.Namespace.Branch.History.ItemNewValue' | translate }} {{'Component.Namespace.Branch.History.ItemComment' | translate }}
    {{'Component.Namespace.Branch.History.NewAdded' | translate }}
    {{'Component.Namespace.Branch.History.Modified' | translate }}
    {{'Component.Namespace.Branch.History.Deleted' | translate }}

    {{'Component.Namespace.Branch.History.NoHistory' | translate }}
    ================================================ FILE: apollo-portal/src/main/resources/static/views/component/namespace-panel-header.html ================================================
    {{'Component.Namespace.Header.Title.Private' | translate }} {{'Component.Namespace.Header.Title.Public' | translate }} {{'Component.Namespace.Header.Title.Extend' | translate }}
    ================================================ FILE: apollo-portal/src/main/resources/static/views/component/namespace-panel-master-tab.html ================================================
    {{'Component.Namespace.Master.Items.Changed' | translate }}
    {{'Component.Namespace.Master.Items.NoPermissionTips' | translate }}
             
    {{'Component.Namespace.Master.Items.Body.ItemsNoPublishedTips' | translate }}
    {{'Component.Namespace.Master.Items.Body.PublishState' | translate }} {{'Component.Namespace.Master.Items.Body.ItemKey' | translate }}  {{'Component.ConfigItem.ItemTypeName' | translate }} {{'Component.Namespace.Master.Items.Body.ItemValue' | translate }} {{'Component.Namespace.Master.Items.Body.ItemComment' | translate }} {{'Component.Namespace.Master.Items.Body.ItemLastModify' | translate }} {{'Component.Namespace.Master.Items.Body.ItemLastModifyTime' | translate }} {{'Component.Namespace.Master.Items.Body.ItemOperator' | translate }}
    {{'Component.Namespace.Master.Items.Body.NoPublish' | translate }} {{'Component.Namespace.Master.Items.Body.Published' | translate }} {{'Component.Namespace.Master.Items.Body.Grayscale' | translate }} {{'Component.Namespace.Master.Items.Body.NewAdded' | translate }} {{'Component.Namespace.Master.Items.Body.Modified' | translate }} {{'Component.Namespace.Master.Items.Body.Deleted' | translate }} {{'Component.ConfigItem.ItemTypeString' | translate }} {{'Component.ConfigItem.ItemTypeNumber' | translate }} {{'Component.ConfigItem.ItemTypeBoolean' | translate }} {{'Component.ConfigItem.ItemTypeJson' | translate }}
    {{'Component.Namespace.Master.Items.Body.Link.Title' | translate }}
    {{'Component.Namespace.Master.Items.Body.PublishState' | translate }} {{'Component.Namespace.Master.Items.Body.ItemKey' | translate }}  {{'Component.ConfigItem.ItemTypeName' | translate }} {{'Component.Namespace.Master.Items.Body.ItemValue' | translate }} {{'Component.Namespace.Master.Items.Body.ItemComment' | translate }} {{'Component.Namespace.Master.Items.Body.ItemLastModify' | translate }} {{'Component.Namespace.Master.Items.Body.ItemLastModifyTime' | translate }} {{'Component.Namespace.Master.Items.Body.ItemOperator' | translate }}
    {{'Component.Namespace.Master.Items.Body.NoPublish' | translate }} {{'Component.Namespace.Master.Items.Body.Published' | translate }} {{'Component.Namespace.Master.Items.Body.Grayscale' | translate }} {{'Component.Namespace.Master.Items.Body.NewAdded' | translate }} {{'Component.Namespace.Master.Items.Body.Modified' | translate }} {{'Component.Namespace.Master.Items.Body.Deleted' | translate }} {{'Component.ConfigItem.ItemTypeString' | translate }} {{'Component.ConfigItem.ItemTypeNumber' | translate }} {{'Component.ConfigItem.ItemTypeBoolean' | translate }} {{'Component.ConfigItem.ItemTypeJson' | translate }}
    {{'Component.Namespace.Master.Items.Body.Link.NoCoverLinkItem' | translate }}
    {{'Component.Namespace.Master.Items.Body.ItemKey' | translate }}  {{'Component.ConfigItem.ItemTypeName' | translate }} {{'Component.Namespace.Master.Items.Body.ItemValue' | translate }} {{'Component.Namespace.Master.Items.Body.ItemComment' | translate }} {{'Component.Namespace.Master.Items.Body.ItemLastModify' | translate }} {{'Component.Namespace.Master.Items.Body.ItemLastModifyTime' | translate }} {{'Component.Namespace.Master.Items.Body.ItemOperator' | translate }}
    {{'Component.ConfigItem.ItemTypeString' | translate }} {{'Component.ConfigItem.ItemTypeNumber' | translate }} {{'Component.ConfigItem.ItemTypeBoolean' | translate }} {{'Component.ConfigItem.ItemTypeJson' | translate }}
    {{'Component.Namespace.Master.Items.Body.Public.NoPublished' | translate }}
    {{'Component.Namespace.Master.Items.Body.ItemKey' | translate }}  {{'Component.Namespace.Master.Items.Body.NoPublished.PublishedValue' | translate }} {{'Component.Namespace.Master.Items.Body.NoPublished.NoPublishedValue' | translate }} {{'Component.Namespace.Master.Items.Body.ItemComment' | translate }} {{'Component.Namespace.Master.Items.Body.ItemLastModifyTime' | translate }} {{'Component.Namespace.Master.Items.Body.ItemOperator' | translate }}
    {{'Component.Namespace.Master.Items.Body.NewAdded' | translate }} {{'Component.Namespace.Master.Items.Body.Modified' | translate }} {{'Component.Namespace.Master.Items.Body.Deleted' | translate }}
    {{'Component.Namespace.Master.Items.Body.NoPublished.Title' | translate }}
    {{'Component.Namespace.Master.Items.Body.Public.Title' | translate }}
    {{'Component.Namespace.Master.Items.Body.Public.NoPublicNamespaceTips1' | translate }} {{namespace.parentAppId}} {{'Component.Namespace.Master.Items.Body.Public.NoPublicNamespaceTips2' | translate:this }}
    {{'Component.Namespace.Master.Items.Body.Link.Title' | translate }}

    {{'Component.Namespace.Master.Items.Body.HistoryView.ItemType' | translate }} {{'Component.Namespace.Master.Items.Body.HistoryView.ItemKey' | translate }} {{'Component.Namespace.Master.Items.Body.HistoryView.ItemOldValue' | translate }} {{'Component.Namespace.Master.Items.Body.HistoryView.ItemNewValue' | translate }} {{'Component.Namespace.Master.Items.Body.HistoryView.ItemComment' | translate }}
    {{'Component.Namespace.Master.Items.Body.HistoryView.NewAdded' | translate }}
    {{'Component.Namespace.Master.Items.Body.HistoryView.Updated' | translate }}
    {{'Component.Namespace.Master.Items.Body.HistoryView.Deleted' | translate }}
    {{'Component.Namespace.Master.Items.Body.HistoryView.ItemType' | translate }} {{'Component.Namespace.Master.Items.Body.HistoryView.ItemOldValue' | translate }} {{'Component.Namespace.Master.Items.Body.HistoryView.ItemNewValue' | translate }} {{'Component.Namespace.Master.Items.Body.HistoryView.ItemComment' | translate }}
    {{'Component.Namespace.Master.Items.Body.HistoryView.NewAdded' | translate }}
    {{'Component.Namespace.Master.Items.Body.HistoryView.Updated' | translate }}
    {{'Component.Namespace.Master.Items.Body.HistoryView.Deleted' | translate }}

    {{'Component.Namespace.Master.Items.Body.HistoryView.NoHistory' | translate }}
    {{'Component.Namespace.Master.Items.Body.Instance.Tips' | translate }}
    {{'Component.Namespace.Master.Items.Body.Instance.ItemAppId' | translate }} {{'Component.Namespace.Master.Items.Body.Instance.ItemCluster' | translate }} {{'Component.Namespace.Master.Items.Body.Instance.ItemDataCenter' | translate }} {{'Component.Namespace.Master.Items.Body.Instance.ItemIp' | translate }} {{'Component.Namespace.Master.Items.Body.Instance.ItemGetTime' | translate }}
    {{instance.configs && instance.configs.length ? (instance.configs[0].releaseDeliveryTime | date: 'yyyy-MM-dd HH:mm:ss') : ''}}
    {{'Component.Namespace.Master.Items.Body.Instance.NoInstanceTips' | translate }}
    {{'Component.Namespace.Master.Items.Body.Instance.ItemAppId' | translate }} {{'Component.Namespace.Master.Items.Body.Instance.ItemCluster' | translate }} {{'Component.Namespace.Master.Items.Body.Instance.ItemDataCenter' | translate }} {{'Component.Namespace.Master.Items.Body.Instance.ItemIp' | translate }} {{'Component.Namespace.Master.Items.Body.Instance.ItemGetTime' | translate }}
    {{instance.configs && instance.configs.length ? (instance.configs[0].releaseDeliveryTime | date: 'yyyy-MM-dd HH:mm:ss') : ''}}
    {{'Component.Namespace.Master.Items.Body.Instance.NoInstanceTips' | translate }}
    {{'Component.Namespace.Master.Items.Body.Instance.ItemAppId' | translate }} {{'Component.Namespace.Master.Items.Body.Instance.ItemCluster' | translate }} {{'Component.Namespace.Master.Items.Body.Instance.ItemDataCenter' | translate }} {{'Component.Namespace.Master.Items.Body.Instance.ItemIp' | translate }}
    {{'Component.Namespace.Master.Items.Body.Instance.NoInstanceTips' | translate }}
    ================================================ FILE: apollo-portal/src/main/resources/static/views/component/namespace-panel.html ================================================
    ================================================ FILE: apollo-portal/src/main/resources/static/views/component/publish-deny-modal.html ================================================ ================================================ FILE: apollo-portal/src/main/resources/static/views/component/release-modal.html ================================================ ================================================ FILE: apollo-portal/src/main/resources/static/views/component/rollback-modal.html ================================================ ================================================ FILE: apollo-portal/src/main/resources/static/views/component/show-text-modal.html ================================================ ================================================ FILE: apollo-portal/src/main/resources/static/views/component/user-selector.html ================================================ ================================================ FILE: apollo-portal/src/main/scripts/shutdown.sh ================================================ #!/bin/bash # # Copyright 2024 Apollo Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # SERVICE_NAME=apollo-portal export APP_NAME=$SERVICE_NAME if [[ -z "$JAVA_HOME" && -d /usr/java/latest/ ]]; then export JAVA_HOME=/usr/java/latest/ fi cd `dirname $0`/.. if [[ ! -f $SERVICE_NAME".jar" && -d current ]]; then cd current fi if [[ -f $SERVICE_NAME".jar" ]]; then chmod a+x $SERVICE_NAME".jar" ./$SERVICE_NAME".jar" stop fi ================================================ FILE: apollo-portal/src/main/scripts/startup.sh ================================================ #!/bin/bash # # Copyright 2024 Apollo Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # SERVICE_NAME=apollo-portal ## Adjust log dir if necessary LOG_DIR=${LOG_DIR:=/opt/logs} ## Adjust server port if necessary SERVER_PORT=${SERVER_PORT:=8070} ## Adjust context path if necessary CONTEXT_PATH=${CONTEXT_PATH:=/} ## Create log directory if not existed because JDK 8+ won't do that mkdir -p $LOG_DIR # Create directory of -XX:HeapDumpPath mkdir -p $LOG_DIR/HeapDumpOnOutOfMemoryError/ ## Adjust memory settings if necessary #export JAVA_OPTS="-Xms2560m -Xmx2560m -Xss256k -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=384m -XX:NewSize=1536m -XX:MaxNewSize=1536m -XX:SurvivorRatio=8" ## Only uncomment the following when you are using server jvm #export JAVA_OPTS="$JAVA_OPTS -server -XX:-ReduceInitialCardMarks" ########### The following is the same for configservice, adminservice, portal ########### export JAVA_OPTS="$JAVA_OPTS -XX:ParallelGCThreads=4 -XX:MaxTenuringThreshold=9 -XX:+DisableExplicitGC -XX:+ScavengeBeforeFullGC -XX:SoftRefLRUPolicyMSPerMB=0 -XX:+ExplicitGCInvokesConcurrent -XX:+HeapDumpOnOutOfMemoryError -XX:-OmitStackTraceInFastThrow -Duser.timezone=Asia/Shanghai -Dclient.encoding.override=UTF-8 -Dfile.encoding=UTF-8 -Djava.security.egd=file:/dev/./urandom" # DS_URL, DS_USERNAME, DS_PASSWORD are deprecated, please use SPRING_DATASOURCE_URL, SPRING_DATASOURCE_USERNAME, SPRING_DATASOURCE_PASSWORD instead # DataSource URL USERNAME PASSWORD if [ "$DS_URL"x != x ] then export SPRING_DATASOURCE_URL=$DS_URL export SPRING_DATASOURCE_USERNAME=$DS_USERNAME export SPRING_DATASOURCE_PASSWORD=$DS_PASSWORD fi export JAVA_OPTS="$JAVA_OPTS -Dserver.port=$SERVER_PORT -Dlogging.file.name=$LOG_DIR/$SERVICE_NAME.log -XX:HeapDumpPath=$LOG_DIR/HeapDumpOnOutOfMemoryError/" export APP_NAME=$SERVICE_NAME PATH_TO_JAR=$SERVICE_NAME".jar" CONTEXT_PATH=$(echo "$CONTEXT_PATH" | sed 's/^\/*//; s/\/*$//') SERVER_URL="http://localhost:${SERVER_PORT}${CONTEXT_PATH:+/$CONTEXT_PATH}" function getPid() { pgrep -f $SERVICE_NAME } function checkPidAlive() { for i in `ls -t $APP_NAME/$APP_NAME.pid 2>/dev/null` do read pid < $i result=$(ps -p "$pid") if [ "$?" -eq 0 ]; then return 0 else printf "\npid - $pid just quit unexpectedly, please check logs under $LOG_DIR and /tmp for more information!\n" exit 1; fi done printf "\nNo pid file found, startup may failed. Please check logs under $LOG_DIR and /tmp for more information!\n" exit 1; } function existProcessUsePort() { if [ "$(curl -X GET --silent --connect-timeout 1 --max-time 2 --head $SERVER_URL | grep "HTTP")" != "" ]; then true else false fi } function isServiceRunning() { if [ "$(curl -X GET --silent --connect-timeout 1 --max-time 2 $SERVER_URL/health | grep "UP")" != "" ]; then true else false fi } if [ "$(uname)" == "Darwin" ]; then windows="0" elif [ "$(expr substr $(uname -s) 1 5)" == "Linux" ]; then windows="0" elif [ "$(expr substr $(uname -s) 1 5)" == "MINGW" ]; then windows="1" else windows="0" fi # for Windows if [ "$windows" == "1" ] && [[ -n "$JAVA_HOME" ]] && [[ -x "$JAVA_HOME/bin/java" ]]; then tmp_java_home=`cygpath -sw "$JAVA_HOME"` export JAVA_HOME=`cygpath -u $tmp_java_home` echo "Windows new JAVA_HOME is: $JAVA_HOME" fi # Find Java if [[ -n "$JAVA_HOME" ]] && [[ -x "$JAVA_HOME/bin/java" ]]; then javaexe="$JAVA_HOME/bin/java" elif type -p java > /dev/null 2>&1; then javaexe=$(type -p java) elif [[ -x "/usr/bin/java" ]]; then javaexe="/usr/bin/java" else echo "Unable to find Java" exit 1 fi if [[ "$javaexe" ]]; then version=$("$javaexe" -version 2>&1 | awk -F '"' '/version/ {print $2}') version=$(echo "$version" | awk -F. '{printf("%03d%03d",$1,$2);}') # now version is of format 009003 (9.3.x) if [ $version -ge 011000 ]; then JAVA_OPTS="$JAVA_OPTS -Xlog:gc*:$LOG_DIR/$SERVICE_NAME.gc.log:time,level,tags -Xlog:safepoint -Xlog:gc+heap=trace" elif [ $version -ge 010000 ]; then JAVA_OPTS="$JAVA_OPTS -Xlog:gc*:$LOG_DIR/$SERVICE_NAME.gc.log:time,level,tags -Xlog:safepoint -Xlog:gc+heap=trace" elif [ $version -ge 009000 ]; then JAVA_OPTS="$JAVA_OPTS -Xlog:gc*:$LOG_DIR/$SERVICE_NAME.gc.log:time,level,tags -Xlog:safepoint -Xlog:gc+heap=trace" else JAVA_OPTS="$JAVA_OPTS -XX:+UseParNewGC" JAVA_OPTS="$JAVA_OPTS -Xloggc:$LOG_DIR/$SERVICE_NAME.gc.log -XX:+PrintGCDetails" JAVA_OPTS="$JAVA_OPTS -XX:+UseConcMarkSweepGC -XX:+UseCMSCompactAtFullCollection -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=60 -XX:+CMSClassUnloadingEnabled -XX:+CMSParallelRemarkEnabled -XX:CMSFullGCsBeforeCompaction=9 -XX:+CMSClassUnloadingEnabled -XX:+PrintGCDateStamps -XX:+PrintGCApplicationConcurrentTime -XX:+PrintHeapAtGC -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=5M" fi fi cd `dirname $0`/.. for i in `ls $SERVICE_NAME-*.jar 2>/dev/null` do if [[ ! $i == *"-sources.jar" ]] then PATH_TO_JAR=$i break fi done if [[ ! -f $PATH_TO_JAR && -d current ]]; then cd current for i in `ls $SERVICE_NAME-*.jar 2>/dev/null` do if [[ ! $i == *"-sources.jar" ]] then PATH_TO_JAR=$i break fi done fi # For Docker environment, start in foreground mode if [[ -n "$APOLLO_RUN_MODE" ]] && [[ "$APOLLO_RUN_MODE" == "Docker" ]]; then exec $javaexe -Dsun.misc.URLClassPath.disableJarChecking=true $JAVA_OPTS -jar $PATH_TO_JAR else # before running check there is another process use port or not if existProcessUsePort; then if isServiceRunning; then echo "$(date) ==== $SERVICE_NAME is running already with port $SERVER_PORT, pid $(getPid)" exit 0 else echo "$(date) ==== $SERVICE_NAME failed to start. The port $SERVER_PORT already be in use by another process" echo "maybe you can figure out which process use port $SERVER_PORT by following ways:" echo "1. access http://change-to-this-machine-ip:$SERVER_PORT by browser" echo "2. run command 'curl $SERVER_URL'" echo "3. run command 'sudo netstat -tunlp | grep :$SERVER_PORT'" echo "4. run command 'sudo lsof -nP -iTCP:$SERVER_PORT -sTCP:LISTEN'" exit 1 fi fi printf "$(date) ==== $SERVICE_NAME Starting ==== \n" if [[ -f $SERVICE_NAME".jar" ]]; then rm -rf $SERVICE_NAME".jar" fi ln $PATH_TO_JAR $SERVICE_NAME".jar" chmod a+x $SERVICE_NAME".jar" ./$SERVICE_NAME".jar" start rc=$?; if [[ $rc != 0 ]]; then echo "$(date) Failed to start $SERVICE_NAME.jar, return code: $rc" exit $rc; fi declare -i counter=0 declare -i max_counter=48 # 48*5=240s declare -i total_time=0 printf "Waiting for server startup" until [[ (( counter -ge max_counter )) ]]; do printf "." sleep 5 counter+=1 total_time=$((counter*5)) checkPidAlive if isServiceRunning; then printf "\n$(date) Server started in $total_time seconds!\n" exit 0; fi done printf "\n$(date) Server failed to start in $total_time seconds!\n" exit 1; fi ================================================ FILE: apollo-portal/src/test/java/com/ctrip/framework/apollo/ControllableAuthorizationConfiguration.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import com.ctrip.framework.apollo.portal.service.ItemService; import com.ctrip.framework.apollo.portal.service.RolePermissionService; import com.ctrip.framework.apollo.portal.spi.UserInfoHolder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; @Configuration public class ControllableAuthorizationConfiguration { @Primary @Bean public UserInfoHolder userInfoHolder() { return mock(UserInfoHolder.class); } @Primary @Bean public RolePermissionService rolePermissionService() { final RolePermissionService mock = mock(RolePermissionService.class); when(mock.userHasPermission(eq("luke"), any(), any())).thenReturn(true); return mock; } @Primary @Bean public ItemService itemService() { return mock(ItemService.class); } } ================================================ FILE: apollo-portal/src/test/java/com/ctrip/framework/apollo/LocalPortalApplication.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.builder.SpringApplicationBuilder; @SpringBootApplication public class LocalPortalApplication { public static void main(String[] args) { new SpringApplicationBuilder(LocalPortalApplication.class).run(args); } } ================================================ FILE: apollo-portal/src/test/java/com/ctrip/framework/apollo/SkipAuthorizationConfiguration.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo; import com.ctrip.framework.apollo.openapi.auth.ConsumerPermissionValidator; import com.ctrip.framework.apollo.openapi.entity.ConsumerToken; import com.ctrip.framework.apollo.openapi.util.ConsumerAuthUtil; import com.ctrip.framework.apollo.portal.component.UserPermissionValidator; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.context.annotation.Profile; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; /** * Created by kezhenxu at 2019/1/8 20:19. * * Configuration class that will disable authorization. * * @author kezhenxu (kezhenxu94@163.com) */ @Profile("skipAuthorization") @Configuration public class SkipAuthorizationConfiguration { @Primary @Bean public ConsumerPermissionValidator consumerPermissionValidator() { final ConsumerPermissionValidator mock = mock(ConsumerPermissionValidator.class); when(mock.hasCreateNamespacePermission(any())).thenReturn(true); return mock; } @Primary @Bean public ConsumerAuthUtil consumerAuthUtil() { final ConsumerAuthUtil mock = mock(ConsumerAuthUtil.class); when(mock.getConsumerId(any())).thenReturn(1L); ConsumerToken someConsumerToken = new ConsumerToken(); someConsumerToken.setConsumerId(1L); someConsumerToken.setToken("some-token"); someConsumerToken.setRateLimit(20); when(mock.getConsumerToken(any())).thenReturn(someConsumerToken); return mock; } @Primary @Bean("userPermissionValidator") public UserPermissionValidator permissionValidator() { final UserPermissionValidator mock = mock(UserPermissionValidator.class); when(mock.isSuperAdmin()).thenReturn(true); when(mock.hasAssignRolePermission(any())).thenReturn(true); return mock; } } ================================================ FILE: apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/auth/ConsumerPermissionValidatorTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.openapi.auth; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.anyList; import static org.mockito.Mockito.anyLong; import static org.mockito.Mockito.anyString; import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import com.ctrip.framework.apollo.common.entity.AppNamespace; import com.ctrip.framework.apollo.openapi.service.ConsumerRolePermissionService; import com.ctrip.framework.apollo.openapi.util.ConsumerAuthUtil; import com.ctrip.framework.apollo.portal.constant.PermissionType; import com.ctrip.framework.apollo.portal.entity.po.Permission; import com.ctrip.framework.apollo.portal.service.SystemRoleManagerService; import java.util.Arrays; import java.util.Collections; import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; /** * Unit tests for the ConsumerPermissionValidator#hasPermissions method */ @ExtendWith(MockitoExtension.class) public class ConsumerPermissionValidatorTest { private static final long CONSUMER_ID = 123L; private static final String TARGET_ID = "targetId"; private static final String PERMISSION_TYPE = "permissionType"; private static final String APP_ID = "testAppId"; @Mock private ConsumerRolePermissionService permissionService; @Mock private ConsumerAuthUtil consumerAuthUtil; private ConsumerPermissionValidator validator; @BeforeEach public void setUp() { validator = new ConsumerPermissionValidator(permissionService, consumerAuthUtil); // Default mock behavior lenient().when(consumerAuthUtil.retrieveConsumerIdFromCtx()).thenReturn(CONSUMER_ID); } /** * Test that hasCreateAppNamespacePermission method throws UnsupportedOperationException */ @Test public void testHasCreateAppNamespacePermission_ThrowsUnsupportedOperationException() { // Arrange String appId = "testAppId"; AppNamespace appNamespace = new AppNamespace(); // Act & Assert assertThrows(UnsupportedOperationException.class, () -> { validator.hasCreateAppNamespacePermission(appId, appNamespace); }); } /** * Test that isSuperAdmin method always returns false */ @Test public void testIsSuperAdmin_ReturnsFalse() { // Act boolean result = validator.isSuperAdmin(); // Assert assertFalse(result); } /** * Test that shouldHideConfigToCurrentUser method throws UnsupportedOperationException */ @Test public void testShouldHideConfigToCurrentUser_ThrowsUnsupportedOperationException() { // Arrange String appId = "testAppId"; String env = "DEV"; String clusterName = "default"; String namespaceName = "application"; // Act & Assert assertThrows(UnsupportedOperationException.class, () -> { validator.shouldHideConfigToCurrentUser(appId, env, clusterName, namespaceName); }); } /** * TC005: Consumer has create application permission, should return true */ @Test public void testHasCreateApplicationPermission_UserHasPermission_ReturnsTrue() { // Arrange when(consumerAuthUtil.retrieveConsumerIdFromCtx()).thenReturn(CONSUMER_ID); when(permissionService.consumerHasPermission( CONSUMER_ID, PermissionType.CREATE_APPLICATION, SystemRoleManagerService.SYSTEM_PERMISSION_TARGET_ID)) .thenReturn(true); // Act boolean result = validator.hasCreateApplicationPermission(); // Assert assertTrue(result); verify(consumerAuthUtil, times(1)).retrieveConsumerIdFromCtx(); verify(permissionService, times(1)) .consumerHasPermission(CONSUMER_ID, PermissionType.CREATE_APPLICATION, SystemRoleManagerService.SYSTEM_PERMISSION_TARGET_ID); } /** * TC006: Consumer does not have create application permission, should return false */ @Test public void testHasCreateApplicationPermission_UserHasNoPermission_ReturnsFalse() { // Arrange when(consumerAuthUtil.retrieveConsumerIdFromCtx()).thenReturn(CONSUMER_ID); when(permissionService.consumerHasPermission( CONSUMER_ID, PermissionType.CREATE_APPLICATION, SystemRoleManagerService.SYSTEM_PERMISSION_TARGET_ID)) .thenReturn(false); // Act boolean result = validator.hasCreateApplicationPermission(); // Assert assertFalse(result); verify(consumerAuthUtil, times(1)).retrieveConsumerIdFromCtx(); verify(permissionService, times(1)) .consumerHasPermission(CONSUMER_ID, PermissionType.CREATE_APPLICATION, SystemRoleManagerService.SYSTEM_PERMISSION_TARGET_ID); } /** * TC007: retrieveConsumerIdFromCtx throws exception, should throw exception */ @Test public void testHasCreateApplicationPermission_RetrieveConsumerIdThrowsException_ThrowsException() { // Arrange when(consumerAuthUtil.retrieveConsumerIdFromCtx()).thenThrow( new RuntimeException("Consumer ID not found")); // Act & Assert assertThrows(RuntimeException.class, () -> { validator.hasCreateApplicationPermission(); }); verify(consumerAuthUtil, times(1)).retrieveConsumerIdFromCtx(); verify(permissionService, never()) .consumerHasPermission(anyLong(), anyString(), anyString()); } /** * TC008: consumerHasPermission throws exception, should throw exception */ @Test public void testHasCreateApplicationPermission_ConsumerHasPermissionThrowsException_ThrowsException() { // Arrange when(consumerAuthUtil.retrieveConsumerIdFromCtx()).thenReturn(CONSUMER_ID); when(permissionService.consumerHasPermission( CONSUMER_ID, PermissionType.CREATE_APPLICATION, SystemRoleManagerService.SYSTEM_PERMISSION_TARGET_ID)) .thenThrow(new RuntimeException("Permission check failed")); // Act & Assert assertThrows(RuntimeException.class, () -> { validator.hasCreateApplicationPermission(); }); verify(consumerAuthUtil, times(1)).retrieveConsumerIdFromCtx(); verify(permissionService, times(1)) .consumerHasPermission(CONSUMER_ID, PermissionType.CREATE_APPLICATION, SystemRoleManagerService.SYSTEM_PERMISSION_TARGET_ID); } @Test public void testHasManageAppMasterPermission_NotSupported_ThrowsException() { // Act & Assert assertThrows(UnsupportedOperationException.class, () -> { validator.hasManageAppMasterPermission(APP_ID); }); } /** * TC001: Consumer has at least one of the required permissions, should return true */ @Test public void testHasPermissions_UserHasPermission_ReturnsTrue() { // 创建 Permission 对象列表 List requiredPerms = Arrays.asList(new Permission("a", "b"), new Permission("c", "d")); // 模拟 permissionService.hasAnyPermission 返回 true when(consumerAuthUtil.retrieveConsumerIdFromCtx()).thenReturn(CONSUMER_ID); when(permissionService.hasAnyPermission(CONSUMER_ID, requiredPerms)).thenReturn(true); boolean result = validator.hasPermissions(requiredPerms); assertTrue(result); verify(consumerAuthUtil, times(1)).retrieveConsumerIdFromCtx(); verify(permissionService, times(1)).hasAnyPermission(CONSUMER_ID, requiredPerms); } /** * TC002: Consumer does not have any of the required permissions, should return false */ @Test public void testHasPermissions_UserHasNoPermission_ReturnsFalse() { List requiredPerms = Arrays.asList(new Permission("a", "b"), new Permission("c", "d")); when(consumerAuthUtil.retrieveConsumerIdFromCtx()).thenReturn(CONSUMER_ID); when(permissionService.hasAnyPermission(CONSUMER_ID, requiredPerms)).thenReturn(false); boolean result = validator.hasPermissions(requiredPerms); assertFalse(result); } /** * TC003: requiredPerms is an empty list, should return false */ @Test public void testHasPermissions_RequiredPermsIsEmpty_ReturnsFalse() { List requiredPerms = Collections.emptyList(); boolean result = validator.hasPermissions(requiredPerms); assertFalse(result); verify(permissionService, never()).hasAnyPermission(anyLong(), anyList()); } /** * TC004: requiredPerms is null, should return false */ @Test public void testHasPermissions_RequiredPermsIsNull_ReturnsFalse() { boolean result = validator.hasPermissions(null); assertFalse(result); verify(permissionService, never()).hasAnyPermission(anyLong(), anyList()); } } ================================================ FILE: apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/filter/ConsumerAuthenticationFilterTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.openapi.filter; import com.ctrip.framework.apollo.openapi.entity.ConsumerToken; import com.ctrip.framework.apollo.openapi.util.ConsumerAuditUtil; import com.ctrip.framework.apollo.openapi.util.ConsumerAuthUtil; import java.io.IOException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import jakarta.servlet.ServletException; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import jakarta.servlet.FilterChain; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.http.HttpHeaders; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.atLeast; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.atMost; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; /** * @author Jason Song(song_s@ctrip.com) */ @RunWith(MockitoJUnitRunner.class) public class ConsumerAuthenticationFilterTest { private static final int TOO_MANY_REQUESTS = 429; private ConsumerAuthenticationFilter authenticationFilter; @Mock private ConsumerAuthUtil consumerAuthUtil; @Mock private ConsumerAuditUtil consumerAuditUtil; @Mock private HttpServletRequest request; @Mock private HttpServletResponse response; @Mock private FilterChain filterChain; @Before public void setUp() throws Exception { authenticationFilter = new ConsumerAuthenticationFilter(consumerAuthUtil, consumerAuditUtil); } @Test public void testAuthSuccessfully() throws Exception { String someToken = "someToken"; Long someConsumerId = 1L; ConsumerToken someConsumerToken = new ConsumerToken(); someConsumerToken.setConsumerId(someConsumerId); when(request.getHeader(HttpHeaders.AUTHORIZATION)).thenReturn(someToken); when(consumerAuthUtil.getConsumerToken(someToken)).thenReturn(someConsumerToken); authenticationFilter.doFilter(request, response, filterChain); verify(consumerAuthUtil, times(1)).storeConsumerId(request, someConsumerId); verify(consumerAuditUtil, times(1)).audit(request, someConsumerId); verify(filterChain, times(1)).doFilter(request, response); } @Test public void testAuthFailed() throws Exception { String someInvalidToken = "someInvalidToken"; when(request.getHeader(HttpHeaders.AUTHORIZATION)).thenReturn(someInvalidToken); when(consumerAuthUtil.getConsumerToken(someInvalidToken)).thenReturn(null); authenticationFilter.doFilter(request, response, filterChain); verify(response, times(1)).sendError(eq(HttpServletResponse.SC_UNAUTHORIZED), anyString()); verify(consumerAuthUtil, never()).storeConsumerId(eq(request), anyLong()); verify(consumerAuditUtil, never()).audit(eq(request), anyLong()); verify(filterChain, never()).doFilter(request, response); } @Test public void testRateLimitSuccessfully() throws Exception { String someToken = "some-ratelimit-success-token"; Long someConsumerId = 1L; int qps = 5; int durationInSeconds = 3; setupRateLimitMocks(someToken, someConsumerId, qps); Runnable task = () -> { try { authenticationFilter.doFilter(request, response, filterChain); } catch (IOException e) { throw new RuntimeException(e); } catch (ServletException e) { throw new RuntimeException(e); } }; int realQps = qps - 1; executeWithQps(realQps, task, durationInSeconds); int total = realQps * durationInSeconds; verify(consumerAuthUtil, times(total)).storeConsumerId(request, someConsumerId); verify(consumerAuditUtil, times(total)).audit(request, someConsumerId); verify(filterChain, times(total)).doFilter(request, response); } @Test public void testRateLimitPartFailure() throws Exception { String someToken = "some-ratelimit-fail-token"; Long someConsumerId = 1L; int qps = 5; int durationInSeconds = 3; setupRateLimitMocks(someToken, someConsumerId, qps); Runnable task = () -> { try { authenticationFilter.doFilter(request, response, filterChain); } catch (IOException e) { throw new RuntimeException(e); } catch (ServletException e) { throw new RuntimeException(e); } }; int realQps = qps + 3; executeWithQps(realQps, task, durationInSeconds); int leastTimes = qps * durationInSeconds; int mostTimes = realQps * durationInSeconds; verify(response, atLeastOnce()).sendError(eq(TOO_MANY_REQUESTS), anyString()); verify(consumerAuthUtil, atLeast(leastTimes)).storeConsumerId(request, someConsumerId); verify(consumerAuthUtil, atMost(mostTimes)).storeConsumerId(request, someConsumerId); verify(consumerAuditUtil, atLeast(leastTimes)).audit(request, someConsumerId); verify(consumerAuditUtil, atMost(mostTimes)).audit(request, someConsumerId); verify(filterChain, atLeast(leastTimes)).doFilter(request, response); verify(filterChain, atMost(mostTimes)).doFilter(request, response); } private void setupRateLimitMocks(String someToken, Long someConsumerId, int qps) { ConsumerToken someConsumerToken = new ConsumerToken(); someConsumerToken.setConsumerId(someConsumerId); someConsumerToken.setRateLimit(qps); someConsumerToken.setToken(someToken); when(request.getHeader(HttpHeaders.AUTHORIZATION)).thenReturn(someToken); when(consumerAuthUtil.getConsumerToken(someToken)).thenReturn(someConsumerToken); } public static void executeWithQps(int qps, Runnable task, int durationInSeconds) { ExecutorService executor = Executors.newFixedThreadPool(qps); long totalTasks = qps * durationInSeconds; for (int i = 0; i < totalTasks; i++) { executor.submit(task); try { TimeUnit.MILLISECONDS.sleep(1000 / qps); // Control QPS } catch (InterruptedException e) { Thread.currentThread().interrupt(); break; } } executor.shutdown(); } } ================================================ FILE: apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/service/ConsumerRolePermissionServiceTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.openapi.service; import com.ctrip.framework.apollo.portal.AbstractIntegrationTest; import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.jdbc.Sql; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; /** * @author Jason Song(song_s@ctrip.com) */ public class ConsumerRolePermissionServiceTest extends AbstractIntegrationTest { @Autowired private ConsumerRolePermissionService consumerRolePermissionService; @Before public void setUp() throws Exception { } @Test @Sql(scripts = "/sql/permission/insert-test-roles.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/sql/permission/insert-test-permissions.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/sql/permission/insert-test-consumerroles.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/sql/permission/insert-test-rolepermissions.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/sql/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testConsumerHasPermission() throws Exception { String someTargetId = "someTargetId"; String anotherTargetId = "anotherTargetId"; String somePermissionType = "somePermissionType"; String anotherPermissionType = "anotherPermissionType"; long someConsumerId = 1; long anotherConsumerId = 2; long someConsumerWithNoPermission = 3; assertTrue(consumerRolePermissionService.consumerHasPermission(someConsumerId, somePermissionType, someTargetId)); assertTrue(consumerRolePermissionService.consumerHasPermission(someConsumerId, anotherPermissionType, anotherTargetId)); assertTrue(consumerRolePermissionService.consumerHasPermission(anotherConsumerId, somePermissionType, someTargetId)); assertTrue(consumerRolePermissionService.consumerHasPermission(anotherConsumerId, anotherPermissionType, anotherTargetId)); assertFalse(consumerRolePermissionService.consumerHasPermission(someConsumerWithNoPermission, somePermissionType, someTargetId)); assertFalse(consumerRolePermissionService.consumerHasPermission(someConsumerWithNoPermission, anotherPermissionType, anotherTargetId)); } } ================================================ FILE: apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/service/ConsumerServiceIntegrationTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.openapi.service; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import com.ctrip.framework.apollo.common.exception.BadRequestException; import com.ctrip.framework.apollo.openapi.entity.Consumer; import com.ctrip.framework.apollo.portal.AbstractIntegrationTest; import com.google.common.collect.Sets; import java.util.List; import java.util.Set; import org.assertj.core.api.Assertions; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Pageable; import org.springframework.test.context.jdbc.Sql; /** * @author wxq */ public class ConsumerServiceIntegrationTest extends AbstractIntegrationTest { @Autowired private ConsumerService consumerService; @Test @Sql( scripts = "/sql/openapi/ConsumerServiceIntegrationTest.testFindAppIdsAuthorizedByConsumerId.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/sql/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testFindAppIdsAuthorizedByConsumerId() { Set appIds = this.consumerService.findAppIdsAuthorizedByConsumerId(1000L); assertEquals(Sets.newHashSet("consumer-test-app-id-0", "consumer-test-app-id-1"), appIds); assertFalse(appIds.contains("consumer-test-app-id-2")); } @Test @Sql(scripts = "/sql/openapi/ConsumerServiceIntegrationTest.commonData.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/sql/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testFindAllConsumer() { List consumerList = consumerService.findAllConsumer(Pageable.ofSize(1)); assertEquals(1, consumerList.size()); consumerList = consumerService.findAllConsumer(Pageable.ofSize(4)); assertEquals(4, consumerList.size()); } @Test @Sql(scripts = "/sql/openapi/ConsumerServiceIntegrationTest.commonData.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/sql/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testDeleteConsumer() { long consumerId = 1000; String appId = "consumer-test-app-role"; Assertions.assertThatNoException().isThrownBy(() -> consumerService.deleteConsumer(appId)); Assertions.assertThatExceptionOfType(BadRequestException.class) .isThrownBy(() -> consumerService.deleteConsumer(appId)) .withMessage("ConsumerApp not exist"); Assertions.assertThat(consumerService.getConsumerByConsumerId(consumerId)).isNull(); } } ================================================ FILE: apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/service/ConsumerServiceTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.openapi.service; import com.ctrip.framework.apollo.openapi.entity.Consumer; import com.ctrip.framework.apollo.openapi.entity.ConsumerRole; import com.ctrip.framework.apollo.openapi.entity.ConsumerToken; import com.ctrip.framework.apollo.openapi.repository.ConsumerAuditRepository; import com.ctrip.framework.apollo.openapi.repository.ConsumerRepository; import com.ctrip.framework.apollo.openapi.repository.ConsumerRoleRepository; import com.ctrip.framework.apollo.openapi.repository.ConsumerTokenRepository; import com.ctrip.framework.apollo.portal.component.config.PortalConfig; import com.ctrip.framework.apollo.portal.entity.bo.UserInfo; import com.ctrip.framework.apollo.portal.entity.po.Role; import com.ctrip.framework.apollo.portal.entity.vo.consumer.ConsumerInfo; import com.ctrip.framework.apollo.portal.environment.Env; import com.ctrip.framework.apollo.portal.repository.RoleRepository; import com.ctrip.framework.apollo.portal.service.RolePermissionService; import com.ctrip.framework.apollo.portal.spi.UserInfoHolder; import com.ctrip.framework.apollo.portal.spi.UserService; import com.ctrip.framework.apollo.portal.util.RoleUtils; import org.junit.jupiter.api.BeforeEach; import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; import java.util.Optional; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.test.context.ContextConfiguration; import static org.mockito.Mockito.*; import static org.junit.jupiter.api.Assertions.*; @SpringBootTest @ContextConfiguration(classes = ConsumerService.class) public class ConsumerServiceTest { @SpyBean private ConsumerService consumerService; @MockBean UserInfoHolder userInfoHolder; @MockBean ConsumerTokenRepository consumerTokenRepository; @MockBean ConsumerRepository consumerRepository; @MockBean ConsumerAuditRepository consumerAuditRepository; @MockBean ConsumerRoleRepository consumerRoleRepository; @MockBean PortalConfig portalConfig; @MockBean RolePermissionService rolePermissionService; @MockBean UserService userService; @MockBean RoleRepository roleRepository; private final String someTokenSalt = "someTokenSalt"; private final String testAppId = "testAppId"; private final String testConsumerName = "testConsumerName"; private final String testOwner = "testOwner"; @BeforeEach public void setUp() throws Exception { when(portalConfig.consumerTokenSalt()).thenReturn(someTokenSalt); } @Test public void testGetConsumerId() throws Exception { String someToken = "someToken"; long someConsumerId = 1; ConsumerToken someConsumerToken = new ConsumerToken(); someConsumerToken.setConsumerId(someConsumerId); when(consumerTokenRepository.findTopByTokenAndExpiresAfter(eq(someToken), any(Date.class))) .thenReturn(someConsumerToken); assertEquals(someConsumerId, consumerService.getConsumerIdByToken(someToken).longValue()); } @Test public void testGetConsumerIdWithNullToken() throws Exception { Long consumerId = consumerService.getConsumerIdByToken(null); assertNull(consumerId); verify(consumerTokenRepository, never()).findTopByTokenAndExpiresAfter(anyString(), any(Date.class)); } @Test public void testGetConsumerByConsumerId() throws Exception { long someConsumerId = 1; Consumer someConsumer = mock(Consumer.class); when(consumerRepository.findById(someConsumerId)).thenReturn(Optional.of(someConsumer)); assertEquals(someConsumer, consumerService.getConsumerByConsumerId(someConsumerId)); verify(consumerRepository, times(1)).findById(someConsumerId); } @Test public void testCreateConsumerToken() throws Exception { ConsumerToken someConsumerToken = mock(ConsumerToken.class); ConsumerToken savedConsumerToken = mock(ConsumerToken.class); when(consumerTokenRepository.save(someConsumerToken)).thenReturn(savedConsumerToken); assertEquals(savedConsumerToken, consumerService.createConsumerToken(someConsumerToken)); } @Test public void testGenerateConsumerToken() throws Exception { String someConsumerAppId = "100003171"; Date generationTime = new GregorianCalendar(2016, Calendar.AUGUST, 9, 12, 10, 50).getTime(); String tokenSalt = "apollo"; String expectedToken = "151067a53d08d70de161fa06b455623741877ce2f019f6e3018844c1a16dd8c6"; String actualToken = consumerService.generateToken(someConsumerAppId, generationTime, tokenSalt); assertEquals(expectedToken, actualToken); } @Test public void testGenerateAndEnrichConsumerToken() throws Exception { String someConsumerAppId = "someAppId"; long someConsumerId = 1; String someToken = "someToken"; Date generationTime = new Date(); Consumer consumer = mock(Consumer.class); when(consumerRepository.findById(someConsumerId)).thenReturn(Optional.of(consumer)); when(consumer.getAppId()).thenReturn(someConsumerAppId); when(consumerService.generateToken(someConsumerAppId, generationTime, someTokenSalt)) .thenReturn(someToken); ConsumerToken consumerToken = new ConsumerToken(); consumerToken.setConsumerId(someConsumerId); consumerToken.setDataChangeCreatedTime(generationTime); consumerService.generateAndEnrichToken(consumer, consumerToken); assertEquals(someToken, consumerToken.getToken()); } @Test public void testGenerateAndEnrichConsumerTokenWithConsumerNotFound() throws Exception { long someConsumerIdNotExist = 1; ConsumerToken consumerToken = new ConsumerToken(); consumerToken.setConsumerId(someConsumerIdNotExist); assertThrows(IllegalArgumentException.class, () -> consumerService.generateAndEnrichToken(null, consumerToken)); } @Test public void testCreateConsumer() { Consumer consumer = createConsumer(testConsumerName, testAppId, testOwner); UserInfo owner = createUser(testOwner); when(consumerRepository.findByAppId(testAppId)).thenReturn(null); when(userService.findByUserId(testOwner)).thenReturn(owner); when(userInfoHolder.getUser()).thenReturn(owner); consumerService.createConsumer(consumer); verify(consumerRepository).save(consumer); } @Test public void testAssignNamespaceRoleToConsumer() { long consumerId = 1L; String token = "token"; doReturn(consumerId).when(consumerService).getConsumerIdByToken(token); String testNamespace = "namespace"; String modifyRoleName = RoleUtils.buildModifyNamespaceRoleName(testAppId, testNamespace); String releaseRoleName = RoleUtils.buildReleaseNamespaceRoleName(testAppId, testNamespace); String envModifyRoleName = RoleUtils.buildModifyNamespaceRoleName(testAppId, testNamespace, Env.DEV.toString()); String envReleaseRoleName = RoleUtils.buildReleaseNamespaceRoleName(testAppId, testNamespace, Env.DEV.toString()); long modifyRoleId = 1; long releaseRoleId = 2; long envModifyRoleId = 3; long envReleaseRoleId = 4; Role modifyRole = createRole(modifyRoleId, modifyRoleName); Role releaseRole = createRole(releaseRoleId, releaseRoleName); Role envModifyRole = createRole(envModifyRoleId, modifyRoleName); Role envReleaseRole = createRole(envReleaseRoleId, releaseRoleName); when(rolePermissionService.findRoleByRoleName(modifyRoleName)).thenReturn(modifyRole); when(rolePermissionService.findRoleByRoleName(releaseRoleName)).thenReturn(releaseRole); when(rolePermissionService.findRoleByRoleName(envModifyRoleName)).thenReturn(envModifyRole); when(rolePermissionService.findRoleByRoleName(envReleaseRoleName)).thenReturn(envReleaseRole); when(consumerRoleRepository.findByConsumerIdAndRoleId(consumerId, modifyRoleId)) .thenReturn(null); UserInfo owner = createUser(testOwner); when(userInfoHolder.getUser()).thenReturn(owner); ConsumerRole namespaceModifyConsumerRole = createConsumerRole(consumerId, modifyRoleId); ConsumerRole namespaceEnvModifyConsumerRole = createConsumerRole(consumerId, envModifyRoleId); ConsumerRole namespaceReleaseConsumerRole = createConsumerRole(consumerId, releaseRoleId); ConsumerRole namespaceEnvReleaseConsumerRole = createConsumerRole(consumerId, envReleaseRoleId); doReturn(namespaceModifyConsumerRole).when(consumerService).createConsumerRole(consumerId, modifyRoleId, testOwner); doReturn(namespaceEnvModifyConsumerRole).when(consumerService).createConsumerRole(consumerId, envModifyRoleId, testOwner); doReturn(namespaceReleaseConsumerRole).when(consumerService).createConsumerRole(consumerId, releaseRoleId, testOwner); doReturn(namespaceEnvReleaseConsumerRole).when(consumerService).createConsumerRole(consumerId, envReleaseRoleId, testOwner); consumerService.assignNamespaceRoleToConsumer(token, testAppId, testNamespace); consumerService.assignNamespaceRoleToConsumer(token, testAppId, testNamespace, Env.DEV.toString()); verify(consumerRoleRepository).save(namespaceModifyConsumerRole); verify(consumerRoleRepository).save(namespaceEnvModifyConsumerRole); verify(consumerRoleRepository).save(namespaceReleaseConsumerRole); verify(consumerRoleRepository).save(namespaceEnvReleaseConsumerRole); } @Test void notAllowCreateApplication() { final String appId = "appId-consumer-2023"; final String token = "token-2023"; final long consumerId = 2023; final long roleId = 202309; { Consumer consumer = new Consumer(); consumer.setAppId(appId); consumer.setId(consumerId); when(consumerRepository.findByAppId(eq(appId))).thenReturn(consumer); ConsumerToken consumerToken = new ConsumerToken(); consumerToken.setToken(token); consumerToken.setRateLimit(0); when(consumerTokenRepository.findByConsumerId(eq(consumerId))).thenReturn(consumerToken); } ConsumerInfo consumerInfo = consumerService.getConsumerInfoByAppId(appId); assertFalse(consumerInfo.isAllowCreateApplication()); assertEquals(appId, consumerInfo.getAppId()); assertEquals(token, consumerInfo.getToken()); } @Test void allowCreateApplication() { final String appId = "appId-consumer-2023"; final String token = "token-2023"; final long consumerId = 2023; final long roleId = 202309; { Consumer consumer = new Consumer(); consumer.setAppId(appId); consumer.setId(consumerId); when(consumerRepository.findByAppId(eq(appId))).thenReturn(consumer); ConsumerToken consumerToken = new ConsumerToken(); consumerToken.setToken(token); consumerToken.setRateLimit(0); when(consumerTokenRepository.findByConsumerId(eq(consumerId))).thenReturn(consumerToken); } { Role role = new Role(); role.setId(roleId); when(rolePermissionService.findRoleByRoleName(any())).thenReturn(role); ConsumerRole consumerRole = new ConsumerRole(); consumerRole.setConsumerId(consumerId); when(consumerRoleRepository.findByConsumerIdAndRoleId(eq(consumerId), eq(roleId))) .thenReturn(consumerRole); } ConsumerInfo consumerInfo = consumerService.getConsumerInfoByAppId(appId); assertTrue(consumerInfo.isAllowCreateApplication()); assertEquals(appId, consumerInfo.getAppId()); assertEquals(token, consumerInfo.getToken()); assertEquals(consumerId, consumerInfo.getConsumerId()); } private Consumer createConsumer(String name, String appId, String ownerName) { Consumer consumer = new Consumer(); consumer.setName(name); consumer.setAppId(appId); consumer.setOwnerName(ownerName); return consumer; } private Role createRole(long roleId, String roleName) { Role role = new Role(); role.setId(roleId); role.setRoleName(roleName); return role; } private ConsumerRole createConsumerRole(long consumerId, long roleId) { ConsumerRole consumerRole = new ConsumerRole(); consumerRole.setConsumerId(consumerId); consumerRole.setRoleId(roleId); return consumerRole; } private UserInfo createUser(String userId) { UserInfo userInfo = new UserInfo(); userInfo.setUserId(userId); return userInfo; } } ================================================ FILE: apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/util/ConsumerAuditUtilTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.openapi.util; import com.ctrip.framework.apollo.openapi.entity.ConsumerAudit; import com.ctrip.framework.apollo.openapi.service.ConsumerService; import com.google.common.util.concurrent.SettableFuture; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import org.mockito.stubbing.Answer; import org.springframework.test.util.ReflectionTestUtils; import jakarta.servlet.http.HttpServletRequest; import java.util.List; import java.util.concurrent.TimeUnit; import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.anyCollection; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.when; /** * @author Jason Song(song_s@ctrip.com) */ @RunWith(MockitoJUnitRunner.class) public class ConsumerAuditUtilTest { private ConsumerAuditUtil consumerAuditUtil; @Mock private ConsumerService consumerService; @Mock private HttpServletRequest request; private long batchTimeout = 50; private TimeUnit batchTimeUnit = TimeUnit.MILLISECONDS; @Before public void setUp() throws Exception { consumerAuditUtil = new ConsumerAuditUtil(consumerService); ReflectionTestUtils.setField(consumerAuditUtil, "BATCH_TIMEOUT", batchTimeout); ReflectionTestUtils.setField(consumerAuditUtil, "BATCH_TIMEUNIT", batchTimeUnit); consumerAuditUtil.afterPropertiesSet(); } @After public void tearDown() throws Exception { consumerAuditUtil.stopAudit(); } @Test public void audit() throws Exception { long someConsumerId = 1; String someUri = "someUri"; String someQuery = "someQuery"; String someMethod = "someMethod"; when(request.getRequestURI()).thenReturn(someUri); when(request.getQueryString()).thenReturn(someQuery); when(request.getMethod()).thenReturn(someMethod); SettableFuture> result = SettableFuture.create(); doAnswer((Answer) invocation -> { Object[] args = invocation.getArguments(); result.set((List) args[0]); return null; }).when(consumerService).createConsumerAudits(anyCollection()); consumerAuditUtil.audit(request, someConsumerId); List audits = result.get(batchTimeout * 5, batchTimeUnit); assertEquals(1, audits.size()); ConsumerAudit audit = audits.get(0); assertEquals(String.format("%s?%s", someUri, someQuery), audit.getUri()); assertEquals(someMethod, audit.getMethod()); assertEquals(someConsumerId, audit.getConsumerId()); } } ================================================ FILE: apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/util/ConsumerAuthUtilTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.openapi.util; import com.ctrip.framework.apollo.openapi.service.ConsumerService; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import jakarta.servlet.http.HttpServletRequest; import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.*; /** * @author Jason Song(song_s@ctrip.com) */ @RunWith(MockitoJUnitRunner.class) public class ConsumerAuthUtilTest { private ConsumerAuthUtil consumerAuthUtil; @Mock private ConsumerService consumerService; @Mock private HttpServletRequest request; @Before public void setUp() throws Exception { consumerAuthUtil = new ConsumerAuthUtil(consumerService); } @Test public void testGetConsumerId() throws Exception { String someToken = "someToken"; Long someConsumerId = 1L; when(consumerService.getConsumerIdByToken(someToken)).thenReturn(someConsumerId); assertEquals(someConsumerId, consumerAuthUtil.getConsumerId(someToken)); verify(consumerService, times(1)).getConsumerIdByToken(someToken); } @Test public void testStoreConsumerId() throws Exception { long someConsumerId = 1L; consumerAuthUtil.storeConsumerId(request, someConsumerId); verify(request, times(1)).setAttribute(ConsumerAuthUtil.CONSUMER_ID, someConsumerId); } @Test public void testRetrieveConsumerId() throws Exception { long someConsumerId = 1; when(request.getAttribute(ConsumerAuthUtil.CONSUMER_ID)).thenReturn(someConsumerId); assertEquals(someConsumerId, consumerAuthUtil.retrieveConsumerId(request)); verify(request, times(1)).getAttribute(ConsumerAuthUtil.CONSUMER_ID); } @Test(expected = IllegalStateException.class) public void testRetrieveConsumerIdWithConsumerIdNotSet() throws Exception { consumerAuthUtil.retrieveConsumerId(request); } @Test(expected = IllegalStateException.class) public void testRetrieveConsumerIdWithConsumerIdInvalid() throws Exception { String someInvalidConsumerId = "abc"; when(request.getAttribute(ConsumerAuthUtil.CONSUMER_ID)).thenReturn(someInvalidConsumerId); consumerAuthUtil.retrieveConsumerId(request); } } ================================================ FILE: apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/v1/controller/AbstractControllerTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.openapi.v1.controller; import jakarta.annotation.PostConstruct; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.http.HttpMessageConverters; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.client.TestRestTemplate; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.web.client.DefaultResponseErrorHandler; import org.springframework.web.client.RestTemplate; /** * Created by kezhenxu at 2019/1/8 18:19. * * @author kezhenxu (kezhenxu94@163.com) */ @RunWith(SpringJUnit4ClassRunner.class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public abstract class AbstractControllerTest { @Autowired private HttpMessageConverters httpMessageConverters; protected RestTemplate restTemplate = (new TestRestTemplate()).getRestTemplate(); @PostConstruct protected void postConstruct() { restTemplate.setErrorHandler(new DefaultResponseErrorHandler()); restTemplate.setMessageConverters(httpMessageConverters.getConverters()); } @Value("${local.server.port}") protected int port; protected String url(String path) { return "http://localhost:" + port + path; } } ================================================ FILE: apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/v1/controller/AppControllerIntegrationTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.openapi.v1.controller; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import com.ctrip.framework.apollo.openapi.dto.OpenAppDTO; import com.ctrip.framework.apollo.portal.AbstractIntegrationTest; import java.util.HashSet; import java.util.Set; import org.junit.Test; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.ResponseEntity; import org.springframework.test.context.jdbc.Sql; /** * Integration test for {@link AppController}. * * @author wxq */ public class AppControllerIntegrationTest extends AbstractIntegrationTest { @Test @Sql( scripts = "/sql/openapi/ConsumerServiceIntegrationTest.testFindAppIdsAuthorizedByConsumerId.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/sql/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testFindAppsAuthorized() { final String token = "3c16bf5b1f44b465179253442460e8c0ad845289"; HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.set(HttpHeaders.AUTHORIZATION, token); ResponseEntity responseEntity = restTemplate.exchange(this.url("/openapi/v1/apps/authorized"), HttpMethod.GET, new HttpEntity<>(httpHeaders), OpenAppDTO[].class); OpenAppDTO[] openAppDTOS = responseEntity.getBody(); assertEquals(2, openAppDTOS.length); Set appIds = new HashSet<>(); for (OpenAppDTO openAppDTO : openAppDTOS) { appIds.add(openAppDTO.getAppId()); } assertTrue(appIds.contains("consumer-test-app-id-0")); assertTrue(appIds.contains("consumer-test-app-id-1")); } } ================================================ FILE: apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/v1/controller/AppControllerParamBindLowLevelTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.openapi.v1.controller; import com.ctrip.framework.apollo.openapi.model.OpenAppDTO; import com.ctrip.framework.apollo.openapi.repository.ConsumerAuditRepository; import com.ctrip.framework.apollo.openapi.repository.ConsumerRepository; import com.ctrip.framework.apollo.openapi.repository.ConsumerRoleRepository; import com.ctrip.framework.apollo.openapi.repository.ConsumerTokenRepository; import com.ctrip.framework.apollo.openapi.server.service.AppOpenApiService; import com.ctrip.framework.apollo.openapi.service.ConsumerService; import com.ctrip.framework.apollo.openapi.util.ConsumerAuthUtil; import com.ctrip.framework.apollo.portal.component.PortalSettings; import com.ctrip.framework.apollo.portal.component.UnifiedPermissionValidator; import com.ctrip.framework.apollo.portal.component.UserIdentityContextHolder; import com.ctrip.framework.apollo.portal.constant.UserIdentityConstants; import com.ctrip.framework.apollo.portal.entity.bo.UserInfo; import com.ctrip.framework.apollo.portal.repository.PermissionRepository; import com.ctrip.framework.apollo.portal.repository.RolePermissionRepository; import com.ctrip.framework.apollo.portal.service.AppService; import com.ctrip.framework.apollo.portal.service.ClusterService; import com.ctrip.framework.apollo.portal.service.RoleInitializationService; import com.ctrip.framework.apollo.portal.service.RolePermissionService; import com.ctrip.framework.apollo.portal.spi.UserInfoHolder; import com.ctrip.framework.apollo.portal.spi.UserService; import com.ctrip.framework.apollo.portal.repository.RoleRepository; import com.google.gson.Gson; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MockMvc; import java.util.HashSet; import java.util.Set; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; @RunWith(SpringRunner.class) @SpringBootTest @AutoConfigureMockMvc(addFilters = false) public class AppControllerParamBindLowLevelTest { @Autowired private MockMvc mockMvc; // Keep the same mocks as your working test to satisfy context wiring @MockBean(name = "unifiedPermissionValidator") private UnifiedPermissionValidator unifiedPermissionValidator; @MockBean private PortalSettings portalSettings; @MockBean private AppService appService; @MockBean private ClusterService clusterService; @MockBean private ConsumerAuthUtil consumerAuthUtil; @MockBean private PermissionRepository permissionRepository; @MockBean private AppOpenApiService appOpenApiService; @MockBean private ConsumerService consumerService; @MockBean private RolePermissionRepository rolePermissionRepository; @MockBean private UserInfoHolder userInfoHolder; @MockBean private ConsumerTokenRepository consumerTokenRepository; @MockBean private ConsumerRepository consumerRepository; @MockBean private ConsumerAuditRepository consumerAuditRepository; @MockBean private ConsumerRoleRepository consumerRoleRepository; @MockBean private RolePermissionService rolePermissionService; @MockBean private UserService userService; @MockBean private RoleRepository roleRepository; @MockBean private RoleInitializationService roleInitializationService; private final Gson gson = new Gson(); @Before public void setUp() { when(unifiedPermissionValidator.hasCreateApplicationPermission()).thenReturn(true); when(unifiedPermissionValidator.isAppAdmin(anyString())).thenReturn(true); UserInfo user = new UserInfo(); user.setUserId("tester"); when(userService.findByUserId(anyString())).thenReturn(user); UserIdentityContextHolder.setAuthType(UserIdentityConstants.CONSUMER); } @Before public void setAuthentication() { // put a dummy Authentication into SecurityContext so @PreAuthorize won't fail SecurityContextHolder.clearContext(); SecurityContextHolder.getContext().setAuthentication( new UsernamePasswordAuthenticationToken("tester", "N/A", AuthorityUtils.NO_AUTHORITIES)); } @After public void clearAuthentication() { SecurityContextHolder.clearContext(); UserIdentityContextHolder.clear(); } @Test public void createAppInEnv_shouldBind_env_query_body() throws Exception { OpenAppDTO dto = new OpenAppDTO(); dto.setAppId("demo"); dto.setName("demo-name"); dto.setOwnerName("owner"); dto.setOwnerEmail("owner@example.com"); dto.setOrgId("org-1"); dto.setOrgName("Org"); // Adjust URL here if your mapping is different mockMvc.perform(post("/openapi/v1/apps/envs/{env}", "DEV").param("operator", "bob") .contentType(MediaType.APPLICATION_JSON).content(gson.toJson(dto))).andExpect( org.springframework.test.web.servlet.result.MockMvcResultMatchers.status().isOk()); ArgumentCaptor envCap = ArgumentCaptor.forClass(String.class); ArgumentCaptor dtoCap = ArgumentCaptor.forClass(OpenAppDTO.class); ArgumentCaptor opCap = ArgumentCaptor.forClass(String.class); verify(appOpenApiService, times(1)).createAppInEnv(envCap.capture(), dtoCap.capture(), opCap.capture()); assertThat(envCap.getValue()).isEqualTo("DEV"); assertThat(opCap.getValue()).isEqualTo("bob"); assertThat(dtoCap.getValue().getAppId()).isEqualTo("demo"); assertThat(dtoCap.getValue().getName()).isEqualTo("demo-name"); } @Test public void getAppsBySelf_shouldBind_page_size_and_ids() throws Exception { long consumerId = 9L; Set authorizedAppIds = new HashSet<>(); authorizedAppIds.add("app1"); authorizedAppIds.add("app2"); when(consumerAuthUtil.retrieveConsumerIdFromCtx()).thenReturn(consumerId); when(consumerService.findAppIdsAuthorizedByConsumerId(consumerId)).thenReturn(authorizedAppIds); mockMvc.perform(get("/openapi/v1/apps/by-self").param("page", "0").param("size", "10")) .andExpect( org.springframework.test.web.servlet.result.MockMvcResultMatchers.status().isOk()); ArgumentCaptor idsCap = ArgumentCaptor.forClass(Set.class); ArgumentCaptor pageCap = ArgumentCaptor.forClass(Integer.class); ArgumentCaptor sizeCap = ArgumentCaptor.forClass(Integer.class); verify(appOpenApiService, times(1)).getAppsBySelf(idsCap.capture(), pageCap.capture(), sizeCap.capture()); assertThat(idsCap.getValue()).containsExactlyInAnyOrder("app1", "app2"); assertThat(pageCap.getValue()).isEqualTo(0); assertThat(sizeCap.getValue()).isEqualTo(10); } @Test public void updateApp_shouldBind_path_query_body() throws Exception { OpenAppDTO dto = new OpenAppDTO(); dto.setAppId("app-1"); dto.setName("new-name"); doNothing().when(appOpenApiService).updateApp(any(OpenAppDTO.class)); mockMvc.perform(put("/openapi/v1/apps/{appId}", "app-1").param("operator", "david") .contentType(MediaType.APPLICATION_JSON).content(gson.toJson(dto))).andExpect( org.springframework.test.web.servlet.result.MockMvcResultMatchers.status().isOk()); ArgumentCaptor dtoCap = ArgumentCaptor.forClass(OpenAppDTO.class); verify(appOpenApiService, times(1)).updateApp(dtoCap.capture()); assertThat(dtoCap.getValue().getAppId()).isEqualTo("app-1"); assertThat(dtoCap.getValue().getName()).isEqualTo("new-name"); } @Test public void deleteApp_shouldBind_path_and_query() throws Exception { when(appOpenApiService.deleteApp("app-1")).thenReturn(new OpenAppDTO()); mockMvc.perform(delete("/openapi/v1/apps/{appId}", "app-1") .param("operator", "alice")) .andExpect(org.springframework.test.web.servlet.result.MockMvcResultMatchers.status().isOk()); verify(appOpenApiService, times(1)).deleteApp("app-1"); } } ================================================ FILE: apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/v1/controller/AppControllerTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.openapi.v1.controller; import com.ctrip.framework.apollo.openapi.model.MultiResponseEntity; import com.ctrip.framework.apollo.openapi.model.OpenAppDTO; import com.ctrip.framework.apollo.openapi.model.OpenEnvClusterDTO; import com.ctrip.framework.apollo.openapi.repository.ConsumerAuditRepository; import com.ctrip.framework.apollo.openapi.repository.ConsumerRepository; import com.ctrip.framework.apollo.openapi.repository.ConsumerRoleRepository; import com.ctrip.framework.apollo.openapi.repository.ConsumerTokenRepository; import com.ctrip.framework.apollo.openapi.server.service.AppOpenApiService; import com.ctrip.framework.apollo.openapi.service.ConsumerService; import com.ctrip.framework.apollo.openapi.util.ConsumerAuthUtil; import com.ctrip.framework.apollo.portal.component.PortalSettings; import com.ctrip.framework.apollo.portal.component.UnifiedPermissionValidator; import com.ctrip.framework.apollo.portal.component.UserIdentityContextHolder; import com.ctrip.framework.apollo.portal.constant.UserIdentityConstants; import com.ctrip.framework.apollo.portal.entity.bo.UserInfo; import com.ctrip.framework.apollo.portal.repository.PermissionRepository; import com.ctrip.framework.apollo.portal.repository.RolePermissionRepository; import com.ctrip.framework.apollo.portal.service.AppService; import com.ctrip.framework.apollo.portal.service.ClusterService; import com.ctrip.framework.apollo.portal.service.RoleInitializationService; import com.ctrip.framework.apollo.portal.service.RolePermissionService; import com.ctrip.framework.apollo.portal.spi.UserInfoHolder; import com.ctrip.framework.apollo.portal.spi.UserService; import com.ctrip.framework.apollo.portal.repository.RoleRepository; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import com.google.gson.Gson; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.ApplicationEventPublisher; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.test.web.servlet.result.MockMvcResultHandlers; import org.springframework.test.web.servlet.result.MockMvcResultMatchers; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Set; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; /** * @author wxq */ @RunWith(SpringRunner.class) @SpringBootTest @AutoConfigureMockMvc(addFilters = false) @TestPropertySource(properties = {"api.pool.max.total=100", "api.pool.max.per.route=100", "api.connectionTimeToLive=30000", "api.connectTimeout=5000", "api.readTimeout=5000"}) public class AppControllerTest { @Autowired private MockMvc mockMvc; @MockBean(name = "unifiedPermissionValidator") private UnifiedPermissionValidator unifiedPermissionValidator; @MockBean private PortalSettings portalSettings; @MockBean private AppService appService; @MockBean private ClusterService clusterService; @MockBean private ConsumerAuthUtil consumerAuthUtil; @MockBean private PermissionRepository permissionRepository; @MockBean private AppOpenApiService appOpenApiService; @MockBean private ConsumerService consumerService; @MockBean private RolePermissionRepository rolePermissionRepository; @MockBean private UserInfoHolder userInfoHolder; @MockBean private ConsumerTokenRepository consumerTokenRepository; @MockBean private ConsumerRepository consumerRepository; @MockBean private ConsumerAuditRepository consumerAuditRepository; @MockBean private ConsumerRoleRepository consumerRoleRepository; @MockBean private RolePermissionService rolePermissionService; @MockBean private UserService userService; @MockBean private RoleRepository roleRepository; @MockBean private RoleInitializationService roleInitializationService; @MockBean private ApplicationEventPublisher applicationEventPublisher; private final Gson gson = new Gson(); @Before public void setUpSecurityMocks() { when(unifiedPermissionValidator.hasCreateApplicationPermission()).thenReturn(true); when(unifiedPermissionValidator.hasCreateNamespacePermission(Mockito.any())) .thenReturn(true); when(unifiedPermissionValidator.isAppAdmin(Mockito.anyString())).thenReturn(true); UserInfo userInfo = new UserInfo(); userInfo.setUserId("test"); when(userService.findByUserId(Mockito.anyString())).thenReturn(userInfo); UserIdentityContextHolder.setAuthType(UserIdentityConstants.CONSUMER); } @After public void tearDown() { UserIdentityContextHolder.clear(); } @Test public void testFindAppsAuthorized() throws Exception { final long consumerId = 123456; when(this.consumerAuthUtil.retrieveConsumerIdFromCtx()).thenReturn(consumerId); Set authorizedAppIds = Sets.newHashSet("app1", "app2"); when(this.consumerService.findAppIdsAuthorizedByConsumerId(consumerId)) .thenReturn(authorizedAppIds); when(this.appOpenApiService.getAppsInfo(Mockito.anyList())).thenReturn(Collections.emptyList()); this.mockMvc.perform(MockMvcRequestBuilders.get("/openapi/v1/apps/authorized")) .andDo(MockMvcResultHandlers.print()) .andExpect(MockMvcResultMatchers.status().is2xxSuccessful()); Mockito.verify(this.consumerService, Mockito.times(1)) .findAppIdsAuthorizedByConsumerId(consumerId); ArgumentCaptor appIdsCaptor = ArgumentCaptor.forClass(List.class); Mockito.verify(this.appOpenApiService).getAppsInfo(appIdsCaptor.capture()); @SuppressWarnings("unchecked") List appIds = appIdsCaptor.getValue(); assertEquals(authorizedAppIds, Sets.newHashSet(appIds)); } @Test public void testGetEnvClusterInfo() throws Exception { String appId = "someAppId"; OpenEnvClusterDTO devCluster = new OpenEnvClusterDTO(); devCluster.setEnv("DEV"); devCluster.setClusters(Lists.newArrayList("default")); OpenEnvClusterDTO fatCluster = new OpenEnvClusterDTO(); fatCluster.setEnv("FAT"); fatCluster.setClusters(Lists.newArrayList("default", "feature")); when(appOpenApiService.getEnvClusterInfo(appId)) .thenReturn(Lists.newArrayList(devCluster, fatCluster)); mockMvc.perform(MockMvcRequestBuilders.get("/openapi/v1/apps/" + appId + "/envclusters")) .andExpect(MockMvcResultMatchers.status().isOk()) .andExpect(MockMvcResultMatchers.jsonPath("$.[0].env").value("DEV")) .andExpect(MockMvcResultMatchers.jsonPath("$.[0].clusters[0]").value("default")) .andExpect(MockMvcResultMatchers.jsonPath("$.[1].env").value("FAT")) .andExpect(MockMvcResultMatchers.jsonPath("$.[1].clusters[0]").value("default")) .andExpect(MockMvcResultMatchers.jsonPath("$.[1].clusters[1]").value("feature")); Mockito.verify(appOpenApiService).getEnvClusterInfo(appId); } @Test public void testFindAppsByIds() throws Exception { String appId1 = "app1"; String appId2 = "app2"; Set appIds = Sets.newHashSet(appId1, appId2); OpenAppDTO app1 = new OpenAppDTO(); app1.setAppId(appId1); OpenAppDTO app2 = new OpenAppDTO(); app2.setAppId(appId2); List apps = Lists.newArrayList(app1, app2); when(appOpenApiService.getAppsInfo(Mockito.anyList())).thenReturn(apps); mockMvc .perform(MockMvcRequestBuilders.get("/openapi/v1/apps").param("appIds", String.join(",", appIds))) .andExpect(MockMvcResultMatchers.status().isOk()) .andExpect(MockMvcResultMatchers.jsonPath("$.[0].appId").value(appId1)) .andExpect(MockMvcResultMatchers.jsonPath("$.[1].appId").value(appId2)); ArgumentCaptor requestIdsCaptor = ArgumentCaptor.forClass(List.class); Mockito.verify(appOpenApiService).getAppsInfo(requestIdsCaptor.capture()); @SuppressWarnings("unchecked") List requestedIds = requestIdsCaptor.getValue(); assertEquals(appIds, Sets.newHashSet(requestedIds)); } @Test public void testFindAllApps() throws Exception { OpenAppDTO app1 = new OpenAppDTO(); app1.setAppId("app1"); OpenAppDTO app2 = new OpenAppDTO(); app2.setAppId("app2"); List apps = Lists.newArrayList(app1, app2); when(appOpenApiService.getAllApps()).thenReturn(apps); mockMvc.perform(MockMvcRequestBuilders.get("/openapi/v1/apps")) .andExpect(MockMvcResultMatchers.status().isOk()) .andExpect(MockMvcResultMatchers.jsonPath("$.[0].appId").value("app1")) .andExpect(MockMvcResultMatchers.jsonPath("$.[1].appId").value("app2")); Mockito.verify(appOpenApiService).getAllApps(); } @Test public void testGetApp() throws Exception { String appId = "someAppId"; OpenAppDTO app = new OpenAppDTO(); app.setAppId(appId); when(appOpenApiService.getAppsInfo(Collections.singletonList(appId))) .thenReturn(Collections.singletonList(app)); mockMvc.perform(MockMvcRequestBuilders.get("/openapi/v1/apps/" + appId)) .andExpect(MockMvcResultMatchers.status().isOk()) .andExpect(MockMvcResultMatchers.jsonPath("$.appId").value(appId)); Mockito.verify(appOpenApiService).getAppsInfo(Collections.singletonList(appId)); } @Test public void testGetAppNotFound() throws Exception { String appId = "someAppId"; when(appOpenApiService.getAppsInfo(Collections.singletonList(appId))) .thenReturn(Collections.emptyList()); mockMvc.perform(MockMvcRequestBuilders.get("/openapi/v1/apps/" + appId)) .andExpect(MockMvcResultMatchers.status().isBadRequest()); Mockito.verify(appOpenApiService).getAppsInfo(Collections.singletonList(appId)); } @Test public void testGetAppsBySelf() throws Exception { long consumerId = 1L; int page = 0; int size = 10; String app1Id = "app1"; String app2Id = "app2"; Set authorizedAppIds = Sets.newHashSet(app1Id, app2Id); when(consumerAuthUtil.retrieveConsumerIdFromCtx()).thenReturn(consumerId); when(this.consumerService.findAppIdsAuthorizedByConsumerId(consumerId)) .thenReturn(authorizedAppIds); OpenAppDTO app1 = new OpenAppDTO(); app1.setAppId(app1Id); OpenAppDTO app2 = new OpenAppDTO(); app2.setAppId(app2Id); List apps = Lists.newArrayList(app1, app2); when(appOpenApiService.getAppsBySelf(authorizedAppIds, page, size)).thenReturn(apps); mockMvc .perform(MockMvcRequestBuilders.get("/openapi/v1/apps/by-self") .param("page", String.valueOf(page)).param("size", String.valueOf(size))) .andExpect(MockMvcResultMatchers.status().isOk()) .andExpect(MockMvcResultMatchers.jsonPath("$.[0].appId").value(app1Id)) .andExpect(MockMvcResultMatchers.jsonPath("$.[1].appId").value(app2Id)); Mockito.verify(this.consumerService).findAppIdsAuthorizedByConsumerId(consumerId); Mockito.verify(this.appOpenApiService).getAppsBySelf(authorizedAppIds, page, size); } @Test public void testFindMissEnvs() throws Exception { String appId = "someAppId"; when(appOpenApiService.findMissEnvs(appId)) .thenReturn(new MultiResponseEntity(HttpStatus.OK.value(), new ArrayList<>())); mockMvc.perform(MockMvcRequestBuilders.get("/openapi/v1/apps/" + appId + "/miss_envs")) .andExpect(MockMvcResultMatchers.status().isOk()); Mockito.verify(appOpenApiService).findMissEnvs(appId); } @Test public void testUpdateApp() throws Exception { String appId = "app1"; String operator = "operatorUser"; OpenAppDTO requestDto = new OpenAppDTO(); requestDto.setAppId(appId); requestDto.setName("App One"); UserInfo userInfo = new UserInfo(); userInfo.setUserId("test"); SecurityContextHolder.getContext().setAuthentication( new UsernamePasswordAuthenticationToken(userInfo, null, Collections.emptyList())); Mockito.doNothing().when(appOpenApiService).updateApp(Mockito.any(OpenAppDTO.class)); when(unifiedPermissionValidator.isAppAdmin(appId)).thenReturn(true); mockMvc .perform(MockMvcRequestBuilders.put("/openapi/v1/apps/" + appId).param("operator", operator) .contentType(MediaType.APPLICATION_JSON).content(gson.toJson(requestDto))) .andExpect(MockMvcResultMatchers.status().isOk()) .andExpect(MockMvcResultMatchers.jsonPath("$.appId").value(appId)) .andExpect(MockMvcResultMatchers.jsonPath("$.name").value("App One")); } @Test public void testUpdateAppWithMismatchedAppId() throws Exception { String pathAppId = "app-path"; String operator = "operatorUser"; OpenAppDTO requestDto = new OpenAppDTO(); requestDto.setAppId("app-body"); UserInfo userInfo = new UserInfo(); userInfo.setUserId("test"); SecurityContextHolder.getContext().setAuthentication( new UsernamePasswordAuthenticationToken(userInfo, null, Collections.emptyList())); when(unifiedPermissionValidator.isAppAdmin(pathAppId)).thenReturn(true); mockMvc .perform( MockMvcRequestBuilders.put("/openapi/v1/apps/" + pathAppId).param("operator", operator) .contentType(MediaType.APPLICATION_JSON).content(gson.toJson(requestDto))) .andExpect(MockMvcResultMatchers.status().isBadRequest()); Mockito.verify(appOpenApiService, Mockito.never()).updateApp(Mockito.any()); } @Test public void testDeleteApp() throws Exception { String appId = "app1"; String operator = "deleter"; UserInfo userInfo = new UserInfo(); userInfo.setUserId("test"); SecurityContextHolder.getContext().setAuthentication( new UsernamePasswordAuthenticationToken(userInfo, null, Collections.emptyList())); when(appOpenApiService.deleteApp(appId)).thenReturn(new OpenAppDTO()); when(unifiedPermissionValidator.isAppAdmin(appId)).thenReturn(true); mockMvc.perform(delete("/openapi/v1/apps/" + appId).param("operator", operator)) .andExpect(MockMvcResultMatchers.status().isOk()) .andExpect(MockMvcResultMatchers.content().string("")); Mockito.verify(appOpenApiService).deleteApp(appId); } } ================================================ FILE: apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/v1/controller/ClusterControllerParamBindLowLevelTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.openapi.v1.controller; import com.ctrip.framework.apollo.openapi.model.OpenClusterDTO; import com.ctrip.framework.apollo.openapi.server.service.ClusterOpenApiService; import com.ctrip.framework.apollo.portal.component.UnifiedPermissionValidator; import com.ctrip.framework.apollo.portal.component.UserIdentityContextHolder; import com.ctrip.framework.apollo.portal.constant.UserIdentityConstants; import com.ctrip.framework.apollo.portal.entity.bo.UserInfo; import com.ctrip.framework.apollo.portal.spi.UserService; import com.google.gson.Gson; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MockMvc; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @RunWith(SpringRunner.class) @SpringBootTest @AutoConfigureMockMvc(addFilters = false) public class ClusterControllerParamBindLowLevelTest { @Autowired private MockMvc mockMvc; @MockBean(name = "unifiedPermissionValidator") private UnifiedPermissionValidator unifiedPermissionValidator; @MockBean private UserService userService; @MockBean private ClusterOpenApiService clusterOpenApiService; private final Gson gson = new Gson(); @Before public void setUp() { when(unifiedPermissionValidator.hasCreateClusterPermission(anyString())).thenReturn(true); when(unifiedPermissionValidator.isAppAdmin(anyString())).thenReturn(true); UserInfo user = new UserInfo(); user.setUserId("tester"); when(userService.findByUserId(anyString())).thenReturn(user); UserIdentityContextHolder.setAuthType(UserIdentityConstants.CONSUMER); } @Before public void setAuthentication() { // put a dummy Authentication into SecurityContext so @PreAuthorize won't fail SecurityContextHolder.clearContext(); SecurityContextHolder.getContext().setAuthentication( new UsernamePasswordAuthenticationToken("tester", "N/A", AuthorityUtils.NO_AUTHORITIES)); } @After public void clearAuthentication() { SecurityContextHolder.clearContext(); UserIdentityContextHolder.clear(); } @Test public void getCluster_shouldBind_path() throws Exception { String appId = "app-1"; String env = "DEV"; String clusterName = "default"; when(clusterOpenApiService.getCluster(appId, env, clusterName)) .thenReturn(new OpenClusterDTO()); mockMvc.perform( get("/openapi/v1/envs/{env}/apps/{appId}/clusters/{clusterName}", env, appId, clusterName)) .andExpect(status().isOk()); ArgumentCaptor appIdCaptor = ArgumentCaptor.forClass(String.class); ArgumentCaptor envCaptor = ArgumentCaptor.forClass(String.class); ArgumentCaptor clusterNameCaptor = ArgumentCaptor.forClass(String.class); verify(clusterOpenApiService, times(1)).getCluster(appIdCaptor.capture(), envCaptor.capture(), clusterNameCaptor.capture()); assertThat(appIdCaptor.getValue()).isEqualTo(appId); assertThat(envCaptor.getValue()).isEqualTo(env); assertThat(clusterNameCaptor.getValue()).isEqualTo(clusterName); } @Test public void createCluster_shouldBind_path_and_body() throws Exception { String appId = "app-1"; String env = "DEV"; OpenClusterDTO dto = new OpenClusterDTO(); dto.setAppId(appId); dto.setName("new-cluster"); dto.setDataChangeCreatedBy("tester"); when(clusterOpenApiService.createCluster(anyString(), any(OpenClusterDTO.class))) .thenReturn(dto); mockMvc .perform(post("/openapi/v1/envs/{env}/apps/{appId}/clusters", env, appId) .contentType(MediaType.APPLICATION_JSON).content(gson.toJson(dto))) .andExpect(status().isOk()); ArgumentCaptor envCaptor = ArgumentCaptor.forClass(String.class); ArgumentCaptor dtoCap = ArgumentCaptor.forClass(OpenClusterDTO.class); verify(clusterOpenApiService, times(1)).createCluster(envCaptor.capture(), dtoCap.capture()); assertThat(envCaptor.getValue()).isEqualTo(env); assertThat(dtoCap.getValue().getAppId()).isEqualTo(appId); assertThat(dtoCap.getValue().getName()).isEqualTo("new-cluster"); } @Test public void deleteCluster_shouldBind_path_and_query() throws Exception { String appId = "app-1"; String env = "DEV"; String clusterName = "default"; String operator = "tester"; doNothing().when(clusterOpenApiService).deleteCluster(env, appId, clusterName); mockMvc.perform(delete("/openapi/v1/envs/{env}/apps/{appId}/clusters/{clusterName}", env, appId, clusterName).param("operator", operator)).andExpect(status().isOk()); ArgumentCaptor envCaptor = ArgumentCaptor.forClass(String.class); ArgumentCaptor appIdCaptor = ArgumentCaptor.forClass(String.class); ArgumentCaptor clusterNameCaptor = ArgumentCaptor.forClass(String.class); verify(clusterOpenApiService, times(1)).deleteCluster(envCaptor.capture(), appIdCaptor.capture(), clusterNameCaptor.capture()); assertThat(envCaptor.getValue()).isEqualTo(env); assertThat(appIdCaptor.getValue()).isEqualTo(appId); assertThat(clusterNameCaptor.getValue()).isEqualTo(clusterName); } } ================================================ FILE: apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/v1/controller/ClusterControllerTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.openapi.v1.controller; import com.ctrip.framework.apollo.openapi.model.OpenClusterDTO; import com.ctrip.framework.apollo.openapi.server.service.ClusterOpenApiService; import com.ctrip.framework.apollo.portal.component.UnifiedPermissionValidator; import com.ctrip.framework.apollo.portal.component.UserIdentityContextHolder; import com.ctrip.framework.apollo.portal.constant.UserIdentityConstants; import com.ctrip.framework.apollo.portal.entity.bo.UserInfo; import com.ctrip.framework.apollo.portal.spi.UserService; import com.google.gson.Gson; import java.util.Collections; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.test.web.servlet.result.MockMvcResultHandlers; import static org.hamcrest.Matchers.is; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @RunWith(SpringRunner.class) @SpringBootTest @AutoConfigureMockMvc(addFilters = false) @TestPropertySource(properties = {"api.pool.max.total=100", "api.pool.max.per.route=100", "api.connectionTimeToLive=30000", "api.connectTimeout=5000", "api.readTimeout=5000"}) public class ClusterControllerTest { @Autowired private MockMvc mockMvc; @MockBean(name = "unifiedPermissionValidator") private UnifiedPermissionValidator unifiedPermissionValidator; @MockBean private UserService userService; @MockBean private ClusterOpenApiService clusterOpenApiService; private final Gson gson = new Gson(); private UserInfo authenticatedUser; @Before public void setUpSecurityMocks() { when(unifiedPermissionValidator.hasCreateClusterPermission(Mockito.anyString())).thenReturn( true); when(unifiedPermissionValidator.isAppAdmin(Mockito.anyString())).thenReturn(true); authenticatedUser = new UserInfo(); authenticatedUser.setUserId("test-operator"); when(userService.findByUserId(Mockito.anyString())).thenReturn(authenticatedUser); SecurityContextHolder.clearContext(); UserIdentityContextHolder.setAuthType(UserIdentityConstants.CONSUMER); } @After public void clearAuthentication() { SecurityContextHolder.clearContext(); UserIdentityContextHolder.clear(); } private void authenticate() { SecurityContextHolder.getContext().setAuthentication( new UsernamePasswordAuthenticationToken(authenticatedUser, null, Collections.emptyList())); } @Test public void testGetCluster() throws Exception { String appId = "test-app"; String env = "DEV"; String clusterName = "default"; OpenClusterDTO clusterDTO = new OpenClusterDTO(); clusterDTO.setAppId(appId); clusterDTO.setName(clusterName); when(clusterOpenApiService.getCluster(Mockito.anyString(), Mockito.anyString(), Mockito.anyString())).thenReturn(clusterDTO); this.mockMvc .perform( MockMvcRequestBuilders.get("/openapi/v1/envs/{env}/apps/{appId}/clusters/{clusterName}", env, appId, clusterName).accept(MediaType.APPLICATION_JSON)) .andDo(MockMvcResultHandlers.print()).andExpect(status().isOk()) .andExpect(jsonPath("$.appId", is(appId))).andExpect(jsonPath("$.name", is(clusterName))); verify(clusterOpenApiService).getCluster(appId, env, clusterName); } @Test public void testCreateCluster() throws Exception { String appId = "test-app"; String env = "DEV"; String clusterName = "new-cluster"; String operator = "apollo"; authenticate(); OpenClusterDTO clusterDTO = new OpenClusterDTO(); clusterDTO.setAppId(appId); clusterDTO.setName(clusterName); clusterDTO.setDataChangeCreatedBy(operator); UserInfo user = new UserInfo(); user.setUserId(operator); when(userService.findByUserId(operator)).thenReturn(user); when(clusterOpenApiService.createCluster(eq(env), any(OpenClusterDTO.class))) .thenReturn(clusterDTO); this.mockMvc .perform( MockMvcRequestBuilders.post("/openapi/v1/envs/{env}/apps/{appId}/clusters", env, appId) .contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON) .content(gson.toJson(clusterDTO))) .andDo(MockMvcResultHandlers.print()).andExpect(status().isOk()) .andExpect(jsonPath("$.appId", is(appId))).andExpect(jsonPath("$.name", is(clusterName))); verify(clusterOpenApiService).createCluster(eq(env), any(OpenClusterDTO.class)); } @Test public void testCreateClusterWithAppIdMismatch() throws Exception { String appIdInPath = "app-in-path"; String appIdInBody = "app-in-body"; String env = "DEV"; String clusterName = "new-cluster"; String operator = "apollo"; authenticate(); OpenClusterDTO clusterDTO = new OpenClusterDTO(); clusterDTO.setAppId(appIdInBody); clusterDTO.setName(clusterName); clusterDTO.setDataChangeCreatedBy(operator); this.mockMvc .perform(MockMvcRequestBuilders .post("/openapi/v1/envs/{env}/apps/{appId}/clusters", env, appIdInPath) .contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON) .content(gson.toJson(clusterDTO))) .andDo(MockMvcResultHandlers.print()).andExpect(status().isBadRequest()); verify(clusterOpenApiService, never()).createCluster(Mockito.anyString(), Mockito.any(OpenClusterDTO.class)); } @Test public void testDeleteCluster() throws Exception { String appId = "test-app"; String env = "DEV"; String clusterName = "default"; String operator = "apollo"; authenticate(); UserInfo user = new UserInfo(); user.setUserId(operator); when(userService.findByUserId(operator)).thenReturn(user); Mockito.doNothing().when(clusterOpenApiService).deleteCluster(env, appId, clusterName); this.mockMvc .perform(MockMvcRequestBuilders .delete("/openapi/v1/envs/{env}/apps/{appId}/clusters/{clusterName}", env, appId, clusterName) .accept(MediaType.APPLICATION_JSON).param("operator", operator)) .andDo(MockMvcResultHandlers.print()).andExpect(status().isOk()); verify(clusterOpenApiService, times(1)).deleteCluster(env, appId, clusterName); } } ================================================ FILE: apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/v1/controller/EnvControllerTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.openapi.v1.controller; import com.ctrip.framework.apollo.openapi.server.service.EnvOpenApiService; import com.ctrip.framework.apollo.portal.component.UnifiedPermissionValidator; import com.ctrip.framework.apollo.portal.component.UserIdentityContextHolder; import com.ctrip.framework.apollo.portal.constant.UserIdentityConstants; import com.ctrip.framework.apollo.portal.entity.bo.UserInfo; import com.ctrip.framework.apollo.portal.spi.UserService; import java.util.Arrays; import java.util.Collections; import java.util.List; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.test.web.servlet.result.MockMvcResultHandlers; import static org.hamcrest.Matchers.is; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @RunWith(SpringRunner.class) @SpringBootTest @AutoConfigureMockMvc(addFilters = false) @TestPropertySource(properties = {"api.pool.max.total=100", "api.pool.max.per.route=100", "api.connectionTimeToLive=30000", "api.connectTimeout=5000", "api.readTimeout=5000"}) public class EnvControllerTest { @Autowired private MockMvc mockMvc; @MockBean(name = "unifiedPermissionValidator") private UnifiedPermissionValidator unifiedPermissionValidator; @MockBean private UserService userService; @MockBean private EnvOpenApiService envOpenApiService; private UserInfo authenticatedUser; @Before public void setUpSecurityMocks() { when(unifiedPermissionValidator.hasCreateClusterPermission(Mockito.anyString())).thenReturn( true); when(unifiedPermissionValidator.isAppAdmin(Mockito.anyString())).thenReturn(true); authenticatedUser = new UserInfo(); authenticatedUser.setUserId("test-operator"); when(userService.findByUserId(Mockito.anyString())).thenReturn(authenticatedUser); SecurityContextHolder.clearContext(); UserIdentityContextHolder.setAuthType(UserIdentityConstants.CONSUMER); } @After public void clearAuthentication() { SecurityContextHolder.clearContext(); UserIdentityContextHolder.clear(); } private void authenticate() { SecurityContextHolder.getContext().setAuthentication( new UsernamePasswordAuthenticationToken(authenticatedUser, null, Collections.emptyList())); } @Test public void testGetEnvs() throws Exception { List envs = Arrays.asList("DEV", "FAT"); when(envOpenApiService.getEnvs()).thenReturn(envs); authenticate(); this.mockMvc .perform(MockMvcRequestBuilders.get("/openapi/v1/envs").accept(MediaType.APPLICATION_JSON)) .andDo(MockMvcResultHandlers.print()).andExpect(status().isOk()) .andExpect(jsonPath("$[0]", is("DEV"))).andExpect(jsonPath("$[1]", is("FAT"))); verify(envOpenApiService).getEnvs(); } } ================================================ FILE: apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/v1/controller/NamespaceControllerTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.openapi.v1.controller; import com.ctrip.framework.apollo.common.utils.InputValidator; import com.ctrip.framework.apollo.core.enums.ConfigFileFormat; import com.ctrip.framework.apollo.openapi.auth.ConsumerPermissionValidator; import com.ctrip.framework.apollo.openapi.dto.OpenAppNamespaceDTO; import org.junit.Assert; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ActiveProfiles; import org.springframework.web.client.HttpClientErrorException; import static org.hamcrest.Matchers.containsString; /** * Created by kezhenxu at 2019/1/8 18:17. * * @author kezhenxu (kezhenxu94@163.com) */ @ActiveProfiles("skipAuthorization") public class NamespaceControllerTest extends AbstractControllerTest { @Autowired private ConsumerPermissionValidator consumerPermissionValidator; @Test public void shouldFailWhenAppNamespaceNameIsInvalid() { Assert.assertTrue(consumerPermissionValidator.hasCreateNamespacePermission(null)); OpenAppNamespaceDTO dto = new OpenAppNamespaceDTO(); dto.setAppId("appId"); dto.setName("invalid name"); dto.setFormat(ConfigFileFormat.Properties.getValue()); dto.setDataChangeCreatedBy("apollo"); try { restTemplate.postForEntity(url("/openapi/v1/apps/{appId}/appnamespaces"), dto, OpenAppNamespaceDTO.class, dto.getAppId()); Assert.fail("should throw"); } catch (HttpClientErrorException e) { String result = e.getResponseBodyAsString(); Assert.assertThat(result, containsString(InputValidator.INVALID_CLUSTER_NAMESPACE_MESSAGE)); Assert.assertThat(result, containsString(InputValidator.INVALID_NAMESPACE_NAMESPACE_MESSAGE)); } } } ================================================ FILE: apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/v1/controller/NamespaceControllerWithAuthorizationTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.openapi.v1.controller; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import com.ctrip.framework.apollo.common.utils.InputValidator; import com.ctrip.framework.apollo.core.enums.ConfigFileFormat; import com.ctrip.framework.apollo.openapi.dto.OpenAppNamespaceDTO; import com.ctrip.framework.apollo.openapi.dto.OpenNamespaceDTO; import java.util.Arrays; import java.util.UUID; import org.junit.Assert; import org.junit.Ignore; import org.junit.Test; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.test.context.jdbc.Sql; import org.springframework.web.client.HttpClientErrorException; /** * @author wxq */ public class NamespaceControllerWithAuthorizationTest extends AbstractControllerTest { static final HttpHeaders HTTP_HEADERS_WITH_TOKEN = new HttpHeaders() { { set(HttpHeaders.AUTHORIZATION, "3c16bf5b1f44b465179253442460e8c0ad845289"); } }; /** * test method {@link NamespaceController#createAppNamespace(String, OpenAppNamespaceDTO)}. */ @Ignore("need admin server for this case") @Test @Sql(scripts = "/sql/openapi/NamespaceControllerTest.testCreateAppNamespace.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/sql/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testCreateAppNamespace() { final String appId = "consumer-test-app-id-0"; final String namespaceName = "create-app-namespace-success"; // query { ResponseEntity responseEntity = restTemplate.exchange( url("/openapi/v1/envs/{env}/apps/{appId}/clusters/{clusterName}/namespaces"), HttpMethod.GET, new HttpEntity<>(HTTP_HEADERS_WITH_TOKEN), String.class, "DEV", appId, "default"); String responseEntityBody = responseEntity.getBody(); assertNotNull(responseEntityBody); assertFalse(responseEntityBody.contains(namespaceName)); } // create it final OpenAppNamespaceDTO dto = new OpenAppNamespaceDTO(); dto.setAppId(appId); dto.setName(namespaceName); dto.setFormat(ConfigFileFormat.Properties.getValue()); dto.setDataChangeCreatedBy("apollo"); restTemplate.exchange(this.url("/openapi/v1/apps/{appId}/appnamespaces"), HttpMethod.POST, new HttpEntity<>(dto, HTTP_HEADERS_WITH_TOKEN), OpenAppNamespaceDTO.class, dto.getAppId()); // query again to confirm { ResponseEntity responseEntity = restTemplate.getForEntity( "/openapi/v1/envs/{env}/apps/{appId}/clusters/{clusterName}/namespaces", OpenNamespaceDTO[].class, "DEV", appId, "default"); OpenNamespaceDTO[] openNamespaceDTOS = responseEntity.getBody(); assertNotNull(openNamespaceDTOS); assertEquals(1, Arrays.stream(openNamespaceDTOS) .filter(openNamespaceDTO -> namespaceName.equals(openNamespaceDTO.getNamespaceName())) .count()); } } /** * test method {@link NamespaceController#createAppNamespace(String, OpenAppNamespaceDTO)}. */ @Test @Sql(scripts = "/sql/openapi/NamespaceControllerTest.testCreateAppNamespace.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/sql/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testCreateAppNamespaceUnauthorized() { OpenAppNamespaceDTO dto = new OpenAppNamespaceDTO(); dto.setAppId("consumer-test-app-id-0"); dto.setName("namespace-0"); dto.setFormat(ConfigFileFormat.Properties.getValue()); dto.setDataChangeCreatedBy("apollo"); try { restTemplate.postForEntity(url("/openapi/v1/apps/{appId}/appnamespaces"), dto, OpenAppNamespaceDTO.class, dto.getAppId()); Assert.fail("should throw"); } catch (HttpClientErrorException e) { assertEquals(HttpStatus.UNAUTHORIZED, e.getStatusCode()); } } /** * test method {@link NamespaceController#createAppNamespace(String, OpenAppNamespaceDTO)}. Just * for check Authorization is ok. */ @Test @Sql(scripts = "/sql/openapi/NamespaceControllerTest.testCreateAppNamespace.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/sql/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testCreateAppNamespaceInvalidNamespaceName() { OpenAppNamespaceDTO dto = new OpenAppNamespaceDTO(); dto.setAppId("consumer-test-app-id-0"); dto.setName("invalid name"); dto.setFormat(ConfigFileFormat.Properties.getValue()); dto.setDataChangeCreatedBy("apollo"); try { restTemplate.exchange(this.url("/openapi/v1/apps/{appId}/appnamespaces"), HttpMethod.POST, new HttpEntity<>(dto, HTTP_HEADERS_WITH_TOKEN), OpenAppNamespaceDTO.class, dto.getAppId()); Assert.fail("should throw"); } catch (HttpClientErrorException e) { assertEquals(HttpStatus.BAD_REQUEST, e.getStatusCode()); String result = e.getResponseBodyAsString(); assertTrue(result.contains(InputValidator.INVALID_CLUSTER_NAMESPACE_MESSAGE)); assertTrue(result.contains(InputValidator.INVALID_NAMESPACE_NAMESPACE_MESSAGE)); } } /** * test method {@link NamespaceController#createAppNamespace(String, OpenAppNamespaceDTO)} without * authority. */ @Test @Sql(scripts = "/sql/openapi/NamespaceControllerTest.testCreateAppNamespace.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/sql/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testCreateAppNamespaceWithoutAuthority() { final OpenAppNamespaceDTO dto = new OpenAppNamespaceDTO(); dto.setAppId("consumer-test-app-id-1"); dto.setName("create-app-namespace-fail"); dto.setFormat(ConfigFileFormat.Properties.getValue()); dto.setDataChangeCreatedBy("apollo"); try { restTemplate.exchange(this.url("/openapi/v1/apps/{appId}/appnamespaces"), HttpMethod.POST, new HttpEntity<>(dto, HTTP_HEADERS_WITH_TOKEN), OpenAppNamespaceDTO.class, dto.getAppId()); fail("should throw"); } catch (HttpClientErrorException e) { assertEquals(HttpStatus.FORBIDDEN, e.getStatusCode()); String result = e.getResponseBodyAsString(); assertTrue(result.contains("AccessDeniedException") || result.contains("AuthorizationDeniedException")); } // random app id dto.setAppId(UUID.randomUUID().toString()); try { restTemplate.exchange(this.url("/openapi/v1/apps/{appId}/appnamespaces"), HttpMethod.POST, new HttpEntity<>(dto, HTTP_HEADERS_WITH_TOKEN), OpenAppNamespaceDTO.class, dto.getAppId()); fail("should throw"); } catch (HttpClientErrorException e) { assertEquals(HttpStatus.FORBIDDEN, e.getStatusCode()); String result = e.getResponseBodyAsString(); assertTrue(result.contains("AccessDeniedException") || result.contains("AuthorizationDeniedException")); } } } ================================================ FILE: apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/v1/controller/OrganizationControllerTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.openapi.v1.controller; import com.ctrip.framework.apollo.openapi.model.OpenOrganizationDto; import com.ctrip.framework.apollo.openapi.server.service.OrganizationOpenApiService; import com.ctrip.framework.apollo.portal.component.UnifiedPermissionValidator; import com.ctrip.framework.apollo.portal.component.UserIdentityContextHolder; import com.ctrip.framework.apollo.portal.constant.UserIdentityConstants; import com.ctrip.framework.apollo.portal.entity.bo.UserInfo; import com.ctrip.framework.apollo.portal.spi.UserService; import java.util.Arrays; import java.util.Collections; import java.util.List; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.test.web.servlet.result.MockMvcResultHandlers; import static org.hamcrest.Matchers.is; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @RunWith(SpringRunner.class) @SpringBootTest @AutoConfigureMockMvc(addFilters = false) @TestPropertySource(properties = {"api.pool.max.total=100", "api.pool.max.per.route=100", "api.connectionTimeToLive=30000", "api.connectTimeout=5000", "api.readTimeout=5000"}) public class OrganizationControllerTest { @Autowired private MockMvc mockMvc; @MockBean(name = "unifiedPermissionValidator") private UnifiedPermissionValidator unifiedPermissionValidator; @MockBean private UserService userService; @MockBean private OrganizationOpenApiService organizationOpenApiService; private UserInfo authenticatedUser; @Before public void setUpSecurityMocks() { when(unifiedPermissionValidator.hasCreateClusterPermission(Mockito.anyString())).thenReturn( true); when(unifiedPermissionValidator.isAppAdmin(Mockito.anyString())).thenReturn(true); authenticatedUser = new UserInfo(); authenticatedUser.setUserId("test-operator"); when(userService.findByUserId(Mockito.anyString())).thenReturn(authenticatedUser); SecurityContextHolder.clearContext(); UserIdentityContextHolder.setAuthType(UserIdentityConstants.CONSUMER); } @After public void clearAuthentication() { SecurityContextHolder.clearContext(); UserIdentityContextHolder.clear(); } private void authenticate() { SecurityContextHolder.getContext().setAuthentication( new UsernamePasswordAuthenticationToken(authenticatedUser, null, Collections.emptyList())); } @Test public void testGetOrganizations() throws Exception { OpenOrganizationDto org = new OpenOrganizationDto(); org.setOrgId("org-1"); org.setOrgName("Org One"); List organizations = Arrays.asList(org); when(organizationOpenApiService.getOrganizations()).thenReturn(organizations); authenticate(); this.mockMvc .perform(MockMvcRequestBuilders.get("/openapi/v1/organizations") .accept(MediaType.APPLICATION_JSON)) .andDo(MockMvcResultHandlers.print()).andExpect(status().isOk()) .andExpect(jsonPath("$[0].orgId", is("org-1"))) .andExpect(jsonPath("$[0].orgName", is("Org One"))); verify(organizationOpenApiService).getOrganizations(); } } ================================================ FILE: apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/AbstractIntegrationTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal; import com.ctrip.framework.apollo.SkipAuthorizationConfiguration; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.boot.test.web.client.TestRestTemplate; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.web.client.DefaultResponseErrorHandler; import org.springframework.web.client.RestTemplate; import jakarta.annotation.PostConstruct; @RunWith(SpringJUnit4ClassRunner.class) @SpringBootTest(classes = {PortalApplication.class, SkipAuthorizationConfiguration.class}, webEnvironment = WebEnvironment.RANDOM_PORT) public abstract class AbstractIntegrationTest { protected RestTemplate restTemplate = (new TestRestTemplate()).getRestTemplate(); @PostConstruct private void postConstruct() { System.setProperty("spring.profiles.active", "test"); restTemplate.setErrorHandler(new DefaultResponseErrorHandler()); } @Value("${local.server.port}") int port; protected String url(String path) { return "http://localhost:" + port + path; } } ================================================ FILE: apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/AbstractUnitTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal; import org.junit.runner.RunWith; import org.mockito.junit.MockitoJUnitRunner; @RunWith(MockitoJUnitRunner.Silent.class) public abstract class AbstractUnitTest { } ================================================ FILE: apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/RetryableRestTemplateTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import com.ctrip.framework.apollo.common.exception.ServiceException; import com.ctrip.framework.apollo.core.dto.ServiceDTO; import com.ctrip.framework.apollo.portal.component.AdminServiceAddressLocator; import com.ctrip.framework.apollo.portal.component.RestTemplateFactory; import com.ctrip.framework.apollo.portal.component.RetryableRestTemplate; import com.ctrip.framework.apollo.portal.component.config.PortalConfig; import com.ctrip.framework.apollo.portal.environment.Env; import com.ctrip.framework.apollo.portal.environment.PortalMetaDomainService; import com.google.common.collect.Maps; import com.google.gson.Gson; import java.net.SocketTimeoutException; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; import org.apache.hc.client5.http.ConnectTimeoutException; import org.apache.hc.client5.http.HttpHostConnectException; import org.apache.hc.core5.http.HttpHost; import org.junit.Before; import org.junit.Test; import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.ResponseEntity; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.web.client.ResourceAccessException; import org.springframework.web.client.RestTemplate; public class RetryableRestTemplateTest extends AbstractUnitTest { @Mock private AdminServiceAddressLocator serviceAddressLocator; @Mock private RestTemplateFactory restTemplateFactory; @Mock private RestTemplate restTemplate; @Mock private PortalMetaDomainService portalMetaDomainService; @Mock private PortalConfig portalConfig; @InjectMocks private RetryableRestTemplate retryableRestTemplate; private static final Gson GSON = new Gson(); private String path = "app"; private String serviceOne = "http://10.0.0.1"; private String serviceTwo = "http://10.0.0.2"; private String serviceThree = "http://10.0.0.3"; private ResourceAccessException socketTimeoutException = new ResourceAccessException(""); private ResourceAccessException httpHostConnectException = new ResourceAccessException(""); private ResourceAccessException connectTimeoutException = new ResourceAccessException(""); private Object request = new Object(); private Object result = new Object(); private Class requestType = request.getClass(); @Before public void init() { socketTimeoutException.initCause(new SocketTimeoutException()); httpHostConnectException .initCause(new HttpHostConnectException("connect timeout", new HttpHost("10.0.0.1", 80))); connectTimeoutException.initCause(new ConnectTimeoutException("connect timeout")); when(restTemplateFactory.getObject()).thenReturn(restTemplate); ReflectionTestUtils.invokeMethod(retryableRestTemplate, "postConstruct"); } @Test(expected = ServiceException.class) public void testNoAdminServer() { when(serviceAddressLocator.getServiceList(any())).thenReturn(Collections.emptyList()); retryableRestTemplate.get(Env.DEV, path, Object.class); } @Test(expected = ServiceException.class) public void testAllServerDown() { when(serviceAddressLocator.getServiceList(any())) .thenReturn(Arrays .asList(mockService(serviceOne), mockService(serviceTwo), mockService(serviceThree))); when(restTemplate .exchange(eq(serviceOne + "/" + path), eq(HttpMethod.GET), any(HttpEntity.class), eq(Object.class))).thenThrow(socketTimeoutException); when(restTemplate .exchange(eq(serviceTwo + "/" + path), eq(HttpMethod.GET), any(HttpEntity.class), eq(Object.class))).thenThrow(httpHostConnectException); when(restTemplate .exchange(eq(serviceThree + "/" + path), eq(HttpMethod.GET), any(HttpEntity.class), eq(Object.class))).thenThrow(connectTimeoutException); retryableRestTemplate.get(Env.DEV, path, Object.class); verify(restTemplate, times(1)) .exchange(eq(serviceOne + "/" + path), eq(HttpMethod.GET), any(HttpEntity.class), eq(Object.class)); verify(restTemplate, times(1)) .exchange(eq(serviceTwo + "/" + path), eq(HttpMethod.GET), any(HttpEntity.class), eq(Object.class)); verify(restTemplate, times(1)) .exchange(eq(serviceThree + "/" + path), eq(HttpMethod.GET), any(HttpEntity.class), eq(Object.class)); } @Test public void testOneServerDown() { ResponseEntity someEntity = mock(ResponseEntity.class); when(someEntity.getBody()).thenReturn(result); when(serviceAddressLocator.getServiceList(any())).thenReturn( Arrays.asList(mockService(serviceOne), mockService(serviceTwo), mockService(serviceThree))); when(restTemplate.exchange(eq(serviceOne + "/" + path), eq(HttpMethod.GET), any(HttpEntity.class), eq(Object.class))).thenThrow(socketTimeoutException); when(restTemplate.exchange(eq(serviceTwo + "/" + path), eq(HttpMethod.GET), any(HttpEntity.class), eq(Object.class))).thenReturn(someEntity); when(restTemplate.exchange(eq(serviceThree + "/" + path), eq(HttpMethod.GET), any(HttpEntity.class), eq(Object.class))).thenThrow(connectTimeoutException); Object actualResult = retryableRestTemplate.get(Env.DEV, path, Object.class); verify(restTemplate, times(1)).exchange(eq(serviceOne + "/" + path), eq(HttpMethod.GET), any(HttpEntity.class), eq(Object.class)); verify(restTemplate, times(1)).exchange(eq(serviceTwo + "/" + path), eq(HttpMethod.GET), any(HttpEntity.class), eq(Object.class)); verify(restTemplate, never()).exchange(eq(serviceThree + "/" + path), eq(HttpMethod.GET), any(HttpEntity.class), eq(Object.class)); assertEquals(result, actualResult); } @Test public void testPostSocketTimeoutNotRetry() { ResponseEntity someEntity = mock(ResponseEntity.class); when(someEntity.getBody()).thenReturn(result); when(serviceAddressLocator.getServiceList(any())).thenReturn( Arrays.asList(mockService(serviceOne), mockService(serviceTwo), mockService(serviceThree))); when(restTemplate.exchange(eq(serviceOne + "/" + path), eq(HttpMethod.POST), any(HttpEntity.class), eq(Object.class))).thenThrow(socketTimeoutException); when(restTemplate.exchange(eq(serviceTwo + "/" + path), eq(HttpMethod.POST), any(HttpEntity.class), eq(Object.class))).thenReturn(someEntity); Throwable exception = null; Object actualResult = null; try { actualResult = retryableRestTemplate.post(Env.DEV, path, request, Object.class); } catch (Throwable ex) { exception = ex; } assertNull(actualResult); assertSame(socketTimeoutException, exception); verify(restTemplate, times(1)).exchange(eq(serviceOne + "/" + path), eq(HttpMethod.POST), any(HttpEntity.class), eq(Object.class)); verify(restTemplate, never()).exchange(eq(serviceTwo + "/" + path), eq(HttpMethod.POST), any(HttpEntity.class), eq(Object.class)); } @Test public void testDelete() { ResponseEntity someEntity = mock(ResponseEntity.class); when(serviceAddressLocator.getServiceList(any())).thenReturn( Arrays.asList(mockService(serviceOne), mockService(serviceTwo), mockService(serviceThree))); when(restTemplate.exchange(eq(serviceOne + "/" + path), eq(HttpMethod.DELETE), any(HttpEntity.class), (Class) isNull())).thenReturn(someEntity); retryableRestTemplate.delete(Env.DEV, path); verify(restTemplate).exchange(eq(serviceOne + "/" + path), eq(HttpMethod.DELETE), any(HttpEntity.class), (Class) isNull()); } @Test public void testPut() { ResponseEntity someEntity = mock(ResponseEntity.class); when(serviceAddressLocator.getServiceList(any())).thenReturn( Arrays.asList(mockService(serviceOne), mockService(serviceTwo), mockService(serviceThree))); when(restTemplate.exchange(eq(serviceOne + "/" + path), eq(HttpMethod.PUT), any(HttpEntity.class), (Class) isNull())).thenReturn(someEntity); retryableRestTemplate.put(Env.DEV, path, request); ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(HttpEntity.class); verify(restTemplate).exchange(eq(serviceOne + "/" + path), eq(HttpMethod.PUT), argumentCaptor.capture(), (Class) isNull()); assertEquals(request, argumentCaptor.getValue().getBody()); } @Test public void testPostObjectWithNoAccessToken() { Env someEnv = Env.DEV; ResponseEntity someEntity = mock(ResponseEntity.class); when(serviceAddressLocator.getServiceList(someEnv)) .thenReturn(Collections.singletonList(mockService(serviceOne))); when(restTemplate.exchange(eq(serviceOne + "/" + path), eq(HttpMethod.POST), any(HttpEntity.class), eq(requestType))).thenReturn(someEntity); when(someEntity.getBody()).thenReturn(result); Object actualResult = retryableRestTemplate.post(someEnv, path, request, requestType); assertEquals(result, actualResult); ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(HttpEntity.class); verify(restTemplate, times(1)).exchange(eq(serviceOne + "/" + path), eq(HttpMethod.POST), argumentCaptor.capture(), eq(requestType)); HttpEntity entity = argumentCaptor.getValue(); HttpHeaders headers = entity.getHeaders(); assertSame(request, entity.getBody()); assertTrue(headers.isEmpty()); } @Test public void testPostObjectWithAccessToken() { Env someEnv = Env.DEV; String someToken = "someToken"; ResponseEntity someEntity = mock(ResponseEntity.class); when(portalConfig.getAdminServiceAccessTokens()) .thenReturn(mockAdminServiceTokens(someEnv, someToken)); when(serviceAddressLocator.getServiceList(someEnv)) .thenReturn(Collections.singletonList(mockService(serviceOne))); when(restTemplate.exchange(eq(serviceOne + "/" + path), eq(HttpMethod.POST), any(HttpEntity.class), eq(requestType))).thenReturn(someEntity); when(someEntity.getBody()).thenReturn(result); Object actualResult = retryableRestTemplate.post(someEnv, path, request, requestType); assertEquals(result, actualResult); ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(HttpEntity.class); verify(restTemplate, times(1)).exchange(eq(serviceOne + "/" + path), eq(HttpMethod.POST), argumentCaptor.capture(), eq(requestType)); HttpEntity entity = argumentCaptor.getValue(); HttpHeaders headers = entity.getHeaders(); List headerValue = headers.get(HttpHeaders.AUTHORIZATION); assertSame(request, entity.getBody()); assertEquals(1, headers.size()); assertEquals(1, headerValue.size()); assertEquals(someToken, headerValue.get(0)); } @Test public void testPostObjectWithNoAccessTokenForEnv() { Env someEnv = Env.DEV; Env anotherEnv = Env.PRO; String someToken = "someToken"; ResponseEntity someEntity = mock(ResponseEntity.class); when(portalConfig.getAdminServiceAccessTokens()) .thenReturn(mockAdminServiceTokens(someEnv, someToken)); when(serviceAddressLocator.getServiceList(someEnv)) .thenReturn(Collections.singletonList(mockService(serviceOne))); when(serviceAddressLocator.getServiceList(anotherEnv)) .thenReturn(Collections.singletonList(mockService(serviceTwo))); when(restTemplate.exchange(eq(serviceTwo + "/" + path), eq(HttpMethod.POST), any(HttpEntity.class), eq(requestType))).thenReturn(someEntity); when(someEntity.getBody()).thenReturn(result); Object actualResult = retryableRestTemplate.post(anotherEnv, path, request, requestType); assertEquals(result, actualResult); ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(HttpEntity.class); verify(restTemplate, times(1)).exchange(eq(serviceTwo + "/" + path), eq(HttpMethod.POST), argumentCaptor.capture(), eq(requestType)); HttpEntity entity = argumentCaptor.getValue(); HttpHeaders headers = entity.getHeaders(); assertSame(request, entity.getBody()); assertTrue(headers.isEmpty()); } @Test public void testPostEntityWithNoAccessToken() { Env someEnv = Env.DEV; String originalHeader = "someHeader"; String originalValue = "someValue"; HttpHeaders originalHeaders = new HttpHeaders(); originalHeaders.add(originalHeader, originalValue); HttpEntity requestEntity = new HttpEntity<>(request, originalHeaders); ResponseEntity someEntity = mock(ResponseEntity.class); when(serviceAddressLocator.getServiceList(someEnv)) .thenReturn(Collections.singletonList(mockService(serviceOne))); when(restTemplate.exchange(eq(serviceOne + "/" + path), eq(HttpMethod.POST), any(HttpEntity.class), eq(requestType))).thenReturn(someEntity); when(someEntity.getBody()).thenReturn(result); Object actualResult = retryableRestTemplate.post(someEnv, path, requestEntity, requestType); assertEquals(result, actualResult); ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(HttpEntity.class); verify(restTemplate, times(1)).exchange(eq(serviceOne + "/" + path), eq(HttpMethod.POST), argumentCaptor.capture(), eq(requestType)); HttpEntity entity = argumentCaptor.getValue(); assertSame(requestEntity, entity); assertSame(request, entity.getBody()); assertEquals(originalHeaders, entity.getHeaders()); } @Test public void testPostEntityWithAccessToken() { Env someEnv = Env.DEV; String someToken = "someToken"; String originalHeader = "someHeader"; String originalValue = "someValue"; HttpHeaders originalHeaders = new HttpHeaders(); originalHeaders.add(originalHeader, originalValue); HttpEntity requestEntity = new HttpEntity<>(request, originalHeaders); ResponseEntity someEntity = mock(ResponseEntity.class); when(portalConfig.getAdminServiceAccessTokens()) .thenReturn(mockAdminServiceTokens(someEnv, someToken)); when(serviceAddressLocator.getServiceList(someEnv)) .thenReturn(Collections.singletonList(mockService(serviceOne))); when(restTemplate.exchange(eq(serviceOne + "/" + path), eq(HttpMethod.POST), any(HttpEntity.class), eq(requestType))).thenReturn(someEntity); when(someEntity.getBody()).thenReturn(result); Object actualResult = retryableRestTemplate.post(someEnv, path, requestEntity, requestType); assertEquals(result, actualResult); ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(HttpEntity.class); verify(restTemplate, times(1)).exchange(eq(serviceOne + "/" + path), eq(HttpMethod.POST), argumentCaptor.capture(), eq(requestType)); HttpEntity entity = argumentCaptor.getValue(); HttpHeaders headers = entity.getHeaders(); assertSame(request, entity.getBody()); assertEquals(2, headers.size()); assertEquals(originalValue, headers.get(originalHeader).get(0)); assertEquals(someToken, headers.get(HttpHeaders.AUTHORIZATION).get(0)); } @Test public void testGetEntityWithNoAccessToken() { Env someEnv = Env.DEV; ParameterizedTypeReference requestType = mock(ParameterizedTypeReference.class); ResponseEntity someEntity = mock(ResponseEntity.class); when(serviceAddressLocator.getServiceList(someEnv)) .thenReturn(Collections.singletonList(mockService(serviceOne))); when(restTemplate.exchange(eq(serviceOne + "/" + path), eq(HttpMethod.GET), any(HttpEntity.class), eq(requestType))).thenReturn(someEntity); ResponseEntity actualResult = retryableRestTemplate.get(someEnv, path, requestType); assertEquals(someEntity, actualResult); ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(HttpEntity.class); verify(restTemplate, times(1)).exchange(eq(serviceOne + "/" + path), eq(HttpMethod.GET), argumentCaptor.capture(), eq(requestType)); HttpHeaders headers = argumentCaptor.getValue().getHeaders(); assertTrue(headers.isEmpty()); } @Test public void testGetEntityWithAccessToken() { Env someEnv = Env.DEV; String someToken = "someToken"; ParameterizedTypeReference requestType = mock(ParameterizedTypeReference.class); ResponseEntity someEntity = mock(ResponseEntity.class); when(portalConfig.getAdminServiceAccessTokens()) .thenReturn(mockAdminServiceTokens(someEnv, someToken)); when(serviceAddressLocator.getServiceList(someEnv)) .thenReturn(Collections.singletonList(mockService(serviceOne))); when(restTemplate.exchange(eq(serviceOne + "/" + path), eq(HttpMethod.GET), any(HttpEntity.class), eq(requestType))).thenReturn(someEntity); ResponseEntity actualResult = retryableRestTemplate.get(someEnv, path, requestType); assertEquals(someEntity, actualResult); ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(HttpEntity.class); verify(restTemplate, times(1)).exchange(eq(serviceOne + "/" + path), eq(HttpMethod.GET), argumentCaptor.capture(), eq(requestType)); HttpHeaders headers = argumentCaptor.getValue().getHeaders(); List headerValue = headers.get(HttpHeaders.AUTHORIZATION); assertEquals(1, headers.size()); assertEquals(1, headerValue.size()); assertEquals(someToken, headerValue.get(0)); } @Test public void testGetEntityWithNoAccessTokenForEnv() { Env someEnv = Env.DEV; Env anotherEnv = Env.PRO; String someToken = "someToken"; ParameterizedTypeReference requestType = mock(ParameterizedTypeReference.class); ResponseEntity someEntity = mock(ResponseEntity.class); when(portalConfig.getAdminServiceAccessTokens()) .thenReturn(mockAdminServiceTokens(someEnv, someToken)); when(serviceAddressLocator.getServiceList(someEnv)) .thenReturn(Collections.singletonList(mockService(serviceOne))); when(serviceAddressLocator.getServiceList(anotherEnv)) .thenReturn(Collections.singletonList(mockService(serviceTwo))); when(restTemplate.exchange(eq(serviceTwo + "/" + path), eq(HttpMethod.GET), any(HttpEntity.class), eq(requestType))).thenReturn(someEntity); ResponseEntity actualResult = retryableRestTemplate.get(anotherEnv, path, requestType); assertEquals(someEntity, actualResult); ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(HttpEntity.class); verify(restTemplate, times(1)).exchange(eq(serviceTwo + "/" + path), eq(HttpMethod.GET), argumentCaptor.capture(), eq(requestType)); HttpHeaders headers = argumentCaptor.getValue().getHeaders(); assertTrue(headers.isEmpty()); } private String mockAdminServiceTokens(Env env, String token) { Map tokenMap = Maps.newHashMap(); tokenMap.put(env.getName(), token); return GSON.toJson(tokenMap); } private ServiceDTO mockService(String homeUrl) { ServiceDTO serviceDTO = new ServiceDTO(); serviceDTO.setHomepageUrl(homeUrl); return serviceDTO; } } ================================================ FILE: apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/ServiceExceptionTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal; import com.ctrip.framework.apollo.common.exception.ServiceException; import com.ctrip.framework.apollo.portal.controller.AppController; import com.ctrip.framework.apollo.portal.entity.model.AppModel; import com.ctrip.framework.apollo.portal.service.AppService; import com.google.gson.Gson; import java.nio.charset.Charset; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.LinkedHashMap; import java.util.Map; import org.junit.Assert; import org.junit.Test; import org.mockito.InjectMocks; import org.mockito.Mock; import org.springframework.http.HttpStatus; import org.springframework.web.client.HttpServerErrorException; import org.springframework.web.client.HttpStatusCodeException; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; public class ServiceExceptionTest extends AbstractUnitTest { @InjectMocks private AppController appController; @Mock private AppService appService; private static final Gson GSON = new Gson(); @Test public void testAdminServiceException() { String errorMsg = "No available admin service"; String errorCode = "errorCode"; String status = "500"; Map errorAttributes = new LinkedHashMap<>(); errorAttributes.put("status", status); errorAttributes.put("message", errorMsg); errorAttributes.put("timestamp", LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)); errorAttributes.put("exception", ServiceException.class.getName()); errorAttributes.put("errorCode", errorCode); HttpStatusCodeException adminException = new HttpServerErrorException(HttpStatus.INTERNAL_SERVER_ERROR, "admin server error", GSON.toJson(errorAttributes).getBytes(), Charset.defaultCharset()); when(appService.createAppAndAddRolePermission(any(), any())).thenThrow(adminException); AppModel app = generateSampleApp(); try { appController.create(app); } catch (HttpStatusCodeException e) { @SuppressWarnings("unchecked") Map attr = new Gson().fromJson(e.getResponseBodyAsString(), Map.class); Assert.assertEquals(errorMsg, attr.get("message")); Assert.assertEquals(errorCode, attr.get("errorCode")); Assert.assertEquals(status, attr.get("status")); } } private AppModel generateSampleApp() { AppModel app = new AppModel(); app.setAppId("someAppId"); app.setName("someName"); app.setOrgId("someOrgId"); app.setOrgName("someOrgNam"); app.setOwnerName("someOwner"); return app; } } ================================================ FILE: apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/component/AbstractPermissionValidatorTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.component; import com.ctrip.framework.apollo.common.entity.AppNamespace; import com.ctrip.framework.apollo.portal.constant.PermissionType; import com.ctrip.framework.apollo.portal.entity.po.Permission; import com.ctrip.framework.apollo.portal.util.RoleUtils; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import java.util.Arrays; import java.util.Collections; import java.util.List; import static org.junit.Assert.*; @RunWith(MockitoJUnitRunner.class) public class AbstractPermissionValidatorTest { @Mock private AppNamespace appNamespace; private AbstractPermissionValidator permissionValidator; @Before public void setUp() { permissionValidator = new AbstractPermissionValidatorImpl(); } @Test public void testHasModifyNamespacePermission_WhenNoPermission() { String appId = "testApp"; String env = "DEV"; String clusterName = "default"; String namespaceName = "application"; assertFalse( permissionValidator.hasModifyNamespacePermission(appId, env, clusterName, namespaceName)); } @Test public void testHasReleaseNamespacePermission_WhenNoPermission() { String appId = "testApp"; String env = "DEV"; String clusterName = "default"; String namespaceName = "application"; assertFalse( permissionValidator.hasReleaseNamespacePermission(appId, env, clusterName, namespaceName)); } @Test public void testHasAssignRolePermission_WhenNoPermission() { assertFalse(permissionValidator.hasAssignRolePermission("testApp")); } @Test public void testHasCreateNamespacePermission_WhenNoPermission() { assertFalse(permissionValidator.hasCreateNamespacePermission("testApp")); } @Test public void testHasCreateAppNamespacePermission_WhenNoPermission() { assertFalse(permissionValidator.hasCreateAppNamespacePermission("testApp", appNamespace)); } @Test public void testHasCreateClusterPermission_WhenNoPermission() { assertFalse(permissionValidator.hasCreateClusterPermission("testApp")); } @Test public void testIsSuperAdmin_WhenNoPermission() { assertFalse(permissionValidator.isSuperAdmin()); } @Test public void testShouldHideConfigToCurrentUser_WhenNoPermission() { assertFalse(permissionValidator.shouldHideConfigToCurrentUser("testApp", "DEV", "default", "application")); } @Test public void testHasCreateApplicationPermission_WhenNoPermission() { assertFalse(permissionValidator.hasCreateApplicationPermission()); } @Test public void testHasManageAppMasterPermission_WhenNoPermission() { assertFalse(permissionValidator.hasManageAppMasterPermission("testApp")); } @Test public void testHasModifyNamespacePermission_WhenWithPermission() { String appId = "testApp"; String env = "DEV"; String clusterName = "default"; String namespaceName = "application"; List granted = Arrays.asList( new Permission(PermissionType.MODIFY_NAMESPACE, RoleUtils.buildNamespaceTargetId(appId, namespaceName)), new Permission(PermissionType.MODIFY_NAMESPACE, RoleUtils.buildNamespaceTargetId(appId, namespaceName, env)), new Permission(PermissionType.MODIFY_NAMESPACES_IN_CLUSTER, RoleUtils.buildClusterTargetId(appId, env, clusterName))); AbstractPermissionValidator validator = new AbstractPermissionValidatorWithPermissionsImpl(granted); assertTrue(validator.hasModifyNamespacePermission(appId, env, clusterName, namespaceName)); } @Test public void testHasReleaseNamespacePermission_WhenWithPermission() { String appId = "testApp"; String env = "DEV"; String clusterName = "default"; String namespaceName = "application"; List granted = Arrays.asList( new Permission(PermissionType.RELEASE_NAMESPACE, RoleUtils.buildNamespaceTargetId(appId, namespaceName)), new Permission(PermissionType.RELEASE_NAMESPACE, RoleUtils.buildNamespaceTargetId(appId, namespaceName, env)), new Permission(PermissionType.RELEASE_NAMESPACES_IN_CLUSTER, RoleUtils.buildClusterTargetId(appId, env, clusterName))); AbstractPermissionValidator validator = new AbstractPermissionValidatorWithPermissionsImpl(granted); assertTrue(validator.hasReleaseNamespacePermission(appId, env, clusterName, namespaceName)); } @Test public void testHasAssignRolePermission_WhenWithPermission() { String appId = "testApp"; List granted = Collections.singletonList(new Permission(PermissionType.ASSIGN_ROLE, appId)); AbstractPermissionValidator validator = new AbstractPermissionValidatorWithPermissionsImpl(granted); assertTrue(validator.hasAssignRolePermission(appId)); } @Test public void testHasCreateNamespacePermission_WhenWithPermission() { String appId = "testApp"; List granted = Collections.singletonList(new Permission(PermissionType.CREATE_NAMESPACE, appId)); AbstractPermissionValidator validator = new AbstractPermissionValidatorWithPermissionsImpl(granted); assertTrue(validator.hasCreateNamespacePermission(appId)); } @Test public void testHasCreateClusterPermission_WhenWithPermission() { String appId = "testApp"; List granted = Collections.singletonList(new Permission(PermissionType.CREATE_CLUSTER, appId)); AbstractPermissionValidator validator = new AbstractPermissionValidatorWithPermissionsImpl(granted); assertTrue(validator.hasCreateClusterPermission(appId)); } @Test public void testShouldHideConfigToCurrentUser_WhenWithPermission() { String appId = "testApp"; String env = "DEV"; String clusterName = "default"; String namespaceName = "application"; List granted = Arrays.asList( new Permission(PermissionType.MODIFY_NAMESPACE, RoleUtils.buildNamespaceTargetId(appId, namespaceName)), new Permission(PermissionType.RELEASE_NAMESPACE, RoleUtils.buildNamespaceTargetId(appId, namespaceName))); AbstractPermissionValidator validator = new AbstractPermissionValidatorWithPermissionsImpl(granted); assertFalse(validator.shouldHideConfigToCurrentUser(appId, env, clusterName, namespaceName)); } @Test public void testHasManageAppMasterPermission_WhenWithPermission() { String appId = "testApp"; List granted = Collections.singletonList(new Permission(PermissionType.MANAGE_APP_MASTER, appId)); AbstractPermissionValidator validator = new AbstractPermissionValidatorWithPermissionsImpl(granted); assertTrue(validator.hasManageAppMasterPermission(appId)); } private static class AbstractPermissionValidatorImpl extends AbstractPermissionValidator { @Override public boolean hasCreateAppNamespacePermission(String appId, AppNamespace appNamespace) { return false; } @Override public boolean isSuperAdmin() { return false; } @Override public boolean hasCreateApplicationPermission() { return false; } @Override public boolean hasManageAppMasterPermission(String appId) { return false; } @Override protected boolean hasPermissions(List requiredPerms) { return false; } @Override public boolean hasCreateApplicationPermission(String userId) { return false; } } private static class AbstractPermissionValidatorWithPermissionsImpl extends AbstractPermissionValidator { private final List allowed; AbstractPermissionValidatorWithPermissionsImpl(List allowed) { this.allowed = allowed; } @Override public boolean hasCreateAppNamespacePermission(String appId, AppNamespace appNamespace) { return true; } @Override public boolean isSuperAdmin() { return true; } @Override public boolean hasCreateApplicationPermission() { return true; } @Override public boolean hasManageAppMasterPermission(String appId) { return true; } @Override protected boolean hasPermissions(List requiredPerms) { return requiredPerms.stream().anyMatch(allowed::contains); } @Override public boolean hasCreateApplicationPermission(String userId) { return true; } } } ================================================ FILE: apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/component/UnifiedPermissionValidatorTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.component; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.when; import com.ctrip.framework.apollo.openapi.auth.ConsumerPermissionValidator; import com.ctrip.framework.apollo.portal.constant.UserIdentityConstants; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; @ExtendWith(MockitoExtension.class) public class UnifiedPermissionValidatorTest { @Mock private UserPermissionValidator userPermissionValidator; @Mock private ConsumerPermissionValidator consumerPermissionValidator; @InjectMocks private UnifiedPermissionValidator unifiedPermissionValidator; // No additional initialization required before each test method (keep as is) @BeforeEach public void setUp() { // No operation needed, UserIdentityContextHolder state will be set separately in each test } // Clean up UserIdentityContextHolder state after each test method (critical! avoid pollution // between tests) @AfterEach public void tearDown() { UserIdentityContextHolder.clear(); } @Test public void hasManageAppMasterPermission_UserAuthType_DelegatesToUserValidator() { final String appId = "testAppId"; final boolean expectedPermission = true; // Set authentication type to USER UserIdentityContextHolder.setAuthType(UserIdentityConstants.USER); when(userPermissionValidator.hasManageAppMasterPermission(appId)) .thenReturn(expectedPermission); boolean result = unifiedPermissionValidator.hasManageAppMasterPermission(appId); assertTrue(result); } @Test public void hasManageAppMasterPermission_ConsumerAuthType_DelegatesToConsumerValidator() { final String appId = "testAppId"; final boolean expectedPermission = false; // Set authentication type to CONSUMER UserIdentityContextHolder.setAuthType(UserIdentityConstants.CONSUMER); when(consumerPermissionValidator.hasManageAppMasterPermission(appId)) .thenReturn(expectedPermission); boolean result = unifiedPermissionValidator.hasManageAppMasterPermission(appId); assertFalse(result); } @Test public void hasManageAppMasterPermission_UnknownAuthType_ThrowsException() { final String appId = "testAppId"; // Set authentication type to UNKNOWN UserIdentityContextHolder.setAuthType("UNKNOWN"); assertThrows(IllegalStateException.class, () -> { unifiedPermissionValidator.hasManageAppMasterPermission(appId); }); } @Test public void hasCreateNamespacePermission_UserAuthType_UsesUserPermissionValidator() { final String appId = "testAppId"; final boolean expectedPermission = true; // Set authentication type to USER UserIdentityContextHolder.setAuthType(UserIdentityConstants.USER); when(userPermissionValidator.hasCreateNamespacePermission(appId)) .thenReturn(expectedPermission); boolean result = unifiedPermissionValidator.hasCreateNamespacePermission(appId); assertTrue(result); } @Test public void hasCreateNamespacePermission_ConsumerAuthType_UsesConsumerPermissionValidator() { final String appId = "testAppId"; final boolean expectedPermission = true; // Set authentication type to CONSUMER UserIdentityContextHolder.setAuthType(UserIdentityConstants.CONSUMER); when(consumerPermissionValidator.hasCreateNamespacePermission(appId)) .thenReturn(expectedPermission); boolean result = unifiedPermissionValidator.hasCreateNamespacePermission(appId); assertTrue(result); } @Test public void hasCreateNamespacePermission_UnknownAuthType_ThrowsIllegalStateException() { final String appId = "testAppId"; // Set authentication type to UNKNOWN UserIdentityContextHolder.setAuthType("UNKNOWN"); assertThrows(IllegalStateException.class, () -> unifiedPermissionValidator.hasCreateNamespacePermission(appId)); } } ================================================ FILE: apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/component/UserIdentityContextHolderTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.component; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; public class UserIdentityContextHolderTest { @BeforeEach public void setUp() { // Clear ThreadLocal before each test UserIdentityContextHolder.clear(); } @Test public void setAuthType_NonNullAuthType_ShouldSetCorrectly() { String authType = "testAuthType"; UserIdentityContextHolder.setAuthType(authType); assertEquals(authType, UserIdentityContextHolder.getAuthType()); } @Test public void setAuthType_NullAuthType_ShouldSetCorrectly() { UserIdentityContextHolder.setAuthType(null); assertNull(UserIdentityContextHolder.getAuthType()); } @Test public void getAuthType_WhenNotSet_ShouldReturnNull() { // Test: getAuthType should return null when AUTH_TYPE_HOLDER is not set assertNull(UserIdentityContextHolder.getAuthType(), "Expected null when AUTH_TYPE_HOLDER is not set"); } @Test public void getAuthType_WhenSet_ShouldReturnCorrectValue() { // Setup: Set a value in AUTH_TYPE_HOLDER UserIdentityContextHolder.setAuthType("TestAuthType"); // Test: getAuthType should return the set value assertEquals("TestAuthType", UserIdentityContextHolder.getAuthType(), "Expected 'TestAuthType' when AUTH_TYPE_HOLDER is set"); } @Test public void clear_ShouldRemoveAuthTypeHolderValue() { // Step 1: Set authentication type UserIdentityContextHolder.setAuthType("testValue"); assertEquals("testValue", UserIdentityContextHolder.getAuthType()); // Verify setup success // Step 2: Call clear() method UserIdentityContextHolder.clear(); // Step 3: Verify result after clearing (get value via public method) assertNull(UserIdentityContextHolder.getAuthType()); // Directly verify getAuthType() returns // null } } ================================================ FILE: apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/component/UserPermissionValidatorTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.component; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.when; import com.ctrip.framework.apollo.common.entity.AppNamespace; import com.ctrip.framework.apollo.portal.component.config.PortalConfig; import com.ctrip.framework.apollo.portal.constant.PermissionType; import com.ctrip.framework.apollo.portal.entity.bo.UserInfo; import com.ctrip.framework.apollo.portal.entity.po.Permission; import com.ctrip.framework.apollo.portal.service.AppNamespaceService; import com.ctrip.framework.apollo.portal.service.RolePermissionService; import com.ctrip.framework.apollo.portal.service.SystemRoleManagerService; import com.ctrip.framework.apollo.portal.spi.UserInfoHolder; import com.google.common.collect.Lists; import java.util.Collections; import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; @ExtendWith(MockitoExtension.class) class UserPermissionValidatorTest { private static final String USER_ID = "test-user"; private static final String APP_ID = "test-app"; private static final String ENV = "DEV"; private static final String CLUSTER = "default"; private static final String NAMESPACE = "application"; @Mock private UserInfoHolder userInfoHolder; @Mock private RolePermissionService rolePermissionService; @Mock private PortalConfig portalConfig; @Mock private AppNamespaceService appNamespaceService; @Mock private SystemRoleManagerService systemRoleManagerService; @InjectMocks private UserPermissionValidator validator; @BeforeEach void setUp() { // Create a UserInfo instance UserInfo stubUser = new UserInfo(); stubUser.setUserId(USER_ID); stubUser.setName("test"); lenient().when(userInfoHolder.getUser()).thenReturn(stubUser); } // 1. hasCreateAppNamespacePermission tests @Test void hasCreateAppNamespacePermission_publicNamespace() { AppNamespace publicNs = new AppNamespace(); publicNs.setPublic(true); List requiredPermissions = Collections.singletonList(new Permission(PermissionType.CREATE_NAMESPACE, APP_ID)); when(rolePermissionService.hasAnyPermission(USER_ID, requiredPermissions)).thenReturn(true); assertThat(validator.hasCreateAppNamespacePermission(APP_ID, publicNs)).isTrue(); } @Test void hasCreateAppNamespacePermission_privateNamespace_adminCanCreate() { AppNamespace privateNs = new AppNamespace(); privateNs.setPublic(false); when(portalConfig.canAppAdminCreatePrivateNamespace()).thenReturn(true); List requiredPermissions = Collections.singletonList(new Permission(PermissionType.CREATE_NAMESPACE, APP_ID)); when(rolePermissionService.hasAnyPermission(USER_ID, requiredPermissions)).thenReturn(true); assertThat(validator.hasCreateAppNamespacePermission(APP_ID, privateNs)).isTrue(); } @Test void hasCreateAppNamespacePermission_privateNamespace_adminCannotCreate_andUserIsSuperAdmin() { AppNamespace privateNs = new AppNamespace(); privateNs.setPublic(false); when(portalConfig.canAppAdminCreatePrivateNamespace()).thenReturn(false); when(rolePermissionService.isSuperAdmin(USER_ID)).thenReturn(true); assertThat(validator.hasCreateAppNamespacePermission(APP_ID, privateNs)).isTrue(); } @Test void hasCreateAppNamespacePermission_privateNamespace_adminCannotCreate_andUserIsNotSuperAdmin() { AppNamespace privateNs = new AppNamespace(); privateNs.setPublic(false); when(portalConfig.canAppAdminCreatePrivateNamespace()).thenReturn(false); when(rolePermissionService.isSuperAdmin(USER_ID)).thenReturn(false); assertThat(validator.hasCreateAppNamespacePermission(APP_ID, privateNs)).isFalse(); } // 2. isSuperAdmin tests @Test void isSuperAdmin_true() { when(rolePermissionService.isSuperAdmin(USER_ID)).thenReturn(true); assertThat(validator.isSuperAdmin()).isTrue(); } @Test void isSuperAdmin_false() { when(rolePermissionService.isSuperAdmin(USER_ID)).thenReturn(false); assertThat(validator.isSuperAdmin()).isFalse(); } @Test void shouldHideConfigToCurrentUser_publicNamespace() { when(portalConfig.isConfigViewMemberOnly(ENV)).thenReturn(true); AppNamespace publicNs = new AppNamespace(); publicNs.setPublic(true); when(appNamespaceService.findByAppIdAndName(APP_ID, NAMESPACE)).thenReturn(publicNs); assertThat(validator.shouldHideConfigToCurrentUser(APP_ID, ENV, CLUSTER, NAMESPACE)).isFalse(); } @Test void shouldHideConfigToCurrentUser_userIsNotAppAdmin() { when(portalConfig.isConfigViewMemberOnly(ENV)).thenReturn(true); when(appNamespaceService.findByAppIdAndName(APP_ID, NAMESPACE)).thenReturn(null); assertThat(validator.shouldHideConfigToCurrentUser(APP_ID, ENV, CLUSTER, NAMESPACE)).isTrue(); } @Test void shouldHideConfigToCurrentUser_configViewNotMemberOnly() { when(portalConfig.isConfigViewMemberOnly(ENV)).thenReturn(false); assertThat(validator.shouldHideConfigToCurrentUser(APP_ID, ENV, CLUSTER, NAMESPACE)).isFalse(); } // 4. hasCreateApplicationPermission tests @Test void hasCreateApplicationPermission_true() { when(systemRoleManagerService.hasCreateApplicationPermission(USER_ID)).thenReturn(true); assertThat(validator.hasCreateApplicationPermission()).isTrue(); } @Test void hasCreateApplicationPermission_false() { when(systemRoleManagerService.hasCreateApplicationPermission(USER_ID)).thenReturn(false); assertThat(validator.hasCreateApplicationPermission()).isFalse(); } // 5. hasManageAppMasterPermission tests @Test void hasManageAppMasterPermission_superAdmin() { when(rolePermissionService.isSuperAdmin(USER_ID)).thenReturn(true); assertThat(validator.hasManageAppMasterPermission(APP_ID)).isTrue(); } @Test void hasManageAppMasterPermission_normalUser_withAssignRole_andManageAppMaster() { when(rolePermissionService.isSuperAdmin(USER_ID)).thenReturn(false); List requiredPermissions = Collections.singletonList( new Permission(PermissionType.ASSIGN_ROLE, APP_ID)); when(rolePermissionService.hasAnyPermission(USER_ID, requiredPermissions)).thenReturn(true); when(systemRoleManagerService.hasManageAppMasterPermission(USER_ID, APP_ID)).thenReturn(true); assertThat(validator.hasManageAppMasterPermission(APP_ID)).isTrue(); } @Test void hasManageAppMasterPermission_normalUser_withoutManageAppMaster() { when(rolePermissionService.isSuperAdmin(USER_ID)).thenReturn(false); List requiredPermissions = Collections.singletonList( new Permission(PermissionType.ASSIGN_ROLE, APP_ID)); when(rolePermissionService.hasAnyPermission(USER_ID, requiredPermissions)).thenReturn(true); when(systemRoleManagerService.hasManageAppMasterPermission(USER_ID, APP_ID)).thenReturn(false); assertThat(validator.hasManageAppMasterPermission(APP_ID)).isFalse(); } @Test void hasPermissions_match() { List requiredPerms = Lists.newArrayList(new Permission(), new Permission()); when(rolePermissionService.hasAnyPermission(USER_ID, requiredPerms)).thenReturn(true); assertThat(validator.hasPermissions(requiredPerms)).isTrue(); } @Test void hasPermissions_notMatch() { List requiredPerms = Lists.newArrayList(new Permission(), new Permission()); when(rolePermissionService.hasAnyPermission(USER_ID, requiredPerms)).thenReturn(false); assertThat(validator.hasPermissions(requiredPerms)).isFalse(); } @Test void hasReleaseNamespacePermission_match() { when(rolePermissionService.hasAnyPermission(eq(USER_ID), anyList())).thenReturn(true); assertThat(validator.hasReleaseNamespacePermission(APP_ID, ENV, CLUSTER, NAMESPACE)).isTrue(); } @Test void hasPermissions_emptyList() { assertThat(validator.hasPermissions(Collections.emptyList())).isFalse(); } /** * Test that shouldHideConfigToCurrentUser normalizes env names correctly. * Verifies fix for issue #5442 where env aliases (prod/PROD/PRO) caused permission bypass. * * @see #5442 */ @Test void shouldHideConfigToCurrentUser_envNormalization_prodAlias() { // Setup: config view is member-only for "PRO" (canonical form) when(portalConfig.isConfigViewMemberOnly("PRO")).thenReturn(true); when(appNamespaceService.findByAppIdAndName(APP_ID, NAMESPACE)).thenReturn(null); // User is not app admin and has no operate permission when(rolePermissionService.isSuperAdmin(USER_ID)).thenReturn(false); when(rolePermissionService.hasAnyPermission(eq(USER_ID), anyList())).thenReturn(false); // Test with "prod" alias - should normalize to "PRO" and hide config assertThat(validator.shouldHideConfigToCurrentUser(APP_ID, "prod", CLUSTER, NAMESPACE)).isTrue(); // Test with "PROD" alias - should also normalize to "PRO" and hide config assertThat(validator.shouldHideConfigToCurrentUser(APP_ID, "PROD", CLUSTER, NAMESPACE)).isTrue(); } @Test void shouldHideConfigToCurrentUser_envNormalization_localAlias() { // Setup: config view is member-only for "LOCAL" (canonical form) when(portalConfig.isConfigViewMemberOnly("LOCAL")).thenReturn(true); when(appNamespaceService.findByAppIdAndName(APP_ID, NAMESPACE)).thenReturn(null); // User is not app admin and has no operate permission when(rolePermissionService.isSuperAdmin(USER_ID)).thenReturn(false); when(rolePermissionService.hasAnyPermission(eq(USER_ID), anyList())).thenReturn(false); // Test with "local" alias - should normalize to "LOCAL" and hide config assertThat(validator.shouldHideConfigToCurrentUser(APP_ID, "local", CLUSTER, NAMESPACE)).isTrue(); } } ================================================ FILE: apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/component/UserPermissionValidatorTestSupplement.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.component; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.when; import com.ctrip.framework.apollo.common.exception.BadRequestException; import com.ctrip.framework.apollo.portal.component.config.PortalConfig; import com.ctrip.framework.apollo.portal.entity.bo.UserInfo; import com.ctrip.framework.apollo.portal.service.AppNamespaceService; import com.ctrip.framework.apollo.portal.service.RolePermissionService; import com.ctrip.framework.apollo.portal.service.SystemRoleManagerService; import com.ctrip.framework.apollo.portal.spi.UserInfoHolder; import java.util.Collections; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; /** * Supplemental test coverage for Apollo PR #5542 environment normalization fix. * Tests comprehensive coverage of env aliases, boundary values, and invalid env handling. * * @see #5442 */ @ExtendWith(MockitoExtension.class) class UserPermissionValidatorTestSupplement { private static final String USER_ID = "test-user"; private static final String APP_ID = "test-app"; private static final String CLUSTER = "default"; private static final String NAMESPACE = "application"; @Mock private UserInfoHolder userInfoHolder; @Mock private RolePermissionService rolePermissionService; @Mock private PortalConfig portalConfig; @Mock private AppNamespaceService appNamespaceService; @Mock private SystemRoleManagerService systemRoleManagerService; @InjectMocks private UserPermissionValidator validator; @BeforeEach void setUp() { UserInfo stubUser = new UserInfo(); stubUser.setUserId(USER_ID); stubUser.setName("test"); lenient().when(userInfoHolder.getUser()).thenReturn(stubUser); } // ========== All Environment Aliases Tests ========== /** * Test FAT environment and its alias FWS (FWS maps to FAT). * Verifies issue #5442 fix for environment alias normalization. */ @Test void shouldHideConfigToCurrentUser_envNormalization_fatAndFwsAliases() { when(portalConfig.isConfigViewMemberOnly("FAT")).thenReturn(true); when(appNamespaceService.findByAppIdAndName(APP_ID, NAMESPACE)).thenReturn(null); when(rolePermissionService.isSuperAdmin(USER_ID)).thenReturn(false); // Test with "fat" lowercase - should normalize to "FAT" assertThat(validator.shouldHideConfigToCurrentUser(APP_ID, "fat", CLUSTER, NAMESPACE)).isTrue(); // Test with "FAT" uppercase - should remain "FAT" assertThat(validator.shouldHideConfigToCurrentUser(APP_ID, "FAT", CLUSTER, NAMESPACE)).isTrue(); // Test with "FWS" - should normalize to "FAT" (special mapping) assertThat(validator.shouldHideConfigToCurrentUser(APP_ID, "FWS", CLUSTER, NAMESPACE)).isTrue(); // Test with "fws" lowercase - should also normalize to "FAT" assertThat(validator.shouldHideConfigToCurrentUser(APP_ID, "fws", CLUSTER, NAMESPACE)).isTrue(); } /** * Test LPT environment normalization. */ @Test void shouldHideConfigToCurrentUser_envNormalization_lptAlias() { when(portalConfig.isConfigViewMemberOnly("LPT")).thenReturn(true); when(appNamespaceService.findByAppIdAndName(APP_ID, NAMESPACE)).thenReturn(null); when(rolePermissionService.isSuperAdmin(USER_ID)).thenReturn(false); // Test with "lpt" lowercase assertThat(validator.shouldHideConfigToCurrentUser(APP_ID, "lpt", CLUSTER, NAMESPACE)).isTrue(); // Test with "LPT" uppercase assertThat(validator.shouldHideConfigToCurrentUser(APP_ID, "LPT", CLUSTER, NAMESPACE)).isTrue(); } /** * Test TOOLS environment normalization. */ @Test void shouldHideConfigToCurrentUser_envNormalization_toolsAlias() { when(portalConfig.isConfigViewMemberOnly("TOOLS")).thenReturn(true); when(appNamespaceService.findByAppIdAndName(APP_ID, NAMESPACE)).thenReturn(null); when(rolePermissionService.isSuperAdmin(USER_ID)).thenReturn(false); // Test with "tools" lowercase assertThat(validator.shouldHideConfigToCurrentUser(APP_ID, "tools", CLUSTER, NAMESPACE)).isTrue(); // Test with "TOOLS" uppercase assertThat(validator.shouldHideConfigToCurrentUser(APP_ID, "TOOLS", CLUSTER, NAMESPACE)).isTrue(); } /** * Test DEV environment normalization. */ @Test void shouldHideConfigToCurrentUser_envNormalization_devAlias() { when(portalConfig.isConfigViewMemberOnly("DEV")).thenReturn(true); when(appNamespaceService.findByAppIdAndName(APP_ID, NAMESPACE)).thenReturn(null); when(rolePermissionService.isSuperAdmin(USER_ID)).thenReturn(false); // Test with "dev" lowercase assertThat(validator.shouldHideConfigToCurrentUser(APP_ID, "dev", CLUSTER, NAMESPACE)).isTrue(); // Test with "DEV" uppercase assertThat(validator.shouldHideConfigToCurrentUser(APP_ID, "DEV", CLUSTER, NAMESPACE)).isTrue(); } /** * Test UAT environment normalization. */ @Test void shouldHideConfigToCurrentUser_envNormalization_uatAlias() { when(portalConfig.isConfigViewMemberOnly("UAT")).thenReturn(true); when(appNamespaceService.findByAppIdAndName(APP_ID, NAMESPACE)).thenReturn(null); when(rolePermissionService.isSuperAdmin(USER_ID)).thenReturn(false); // Test with "uat" lowercase assertThat(validator.shouldHideConfigToCurrentUser(APP_ID, "uat", CLUSTER, NAMESPACE)).isTrue(); // Test with "UAT" uppercase assertThat(validator.shouldHideConfigToCurrentUser(APP_ID, "UAT", CLUSTER, NAMESPACE)).isTrue(); } // ========== Boundary Value Tests ========== /** * Test empty string environment - should throw BadRequestException. */ @Test void shouldHideConfigToCurrentUser_emptyString_throwsBadRequestException() { assertThatThrownBy( () -> validator.shouldHideConfigToCurrentUser(APP_ID, "", CLUSTER, NAMESPACE)) .isInstanceOf(BadRequestException.class).hasMessageContaining("invalid env format"); } /** * Test whitespace-only environment strings - should throw BadRequestException. */ @Test void shouldHideConfigToCurrentUser_whitespaceStrings_throwsBadRequestException() { // Test with spaces assertThatThrownBy( () -> validator.shouldHideConfigToCurrentUser(APP_ID, " ", CLUSTER, NAMESPACE)) .isInstanceOf(BadRequestException.class); // Test with tab assertThatThrownBy( () -> validator.shouldHideConfigToCurrentUser(APP_ID, "\t", CLUSTER, NAMESPACE)) .isInstanceOf(BadRequestException.class); // Test with newline assertThatThrownBy( () -> validator.shouldHideConfigToCurrentUser(APP_ID, "\n", CLUSTER, NAMESPACE)) .isInstanceOf(BadRequestException.class); } /** * Test special characters in environment name - should throw BadRequestException. */ @Test void shouldHideConfigToCurrentUser_specialCharacters_throwsBadRequestException() { // Test with @ symbol assertThatThrownBy( () -> validator.shouldHideConfigToCurrentUser(APP_ID, "env@123", CLUSTER, NAMESPACE)) .isInstanceOf(BadRequestException.class); // Test with # symbol assertThatThrownBy( () -> validator.shouldHideConfigToCurrentUser(APP_ID, "env#test", CLUSTER, NAMESPACE)) .isInstanceOf(BadRequestException.class); // Test with spaces in name assertThatThrownBy(() -> validator.shouldHideConfigToCurrentUser(APP_ID, "env with spaces", CLUSTER, NAMESPACE)).isInstanceOf(BadRequestException.class); } /** * Test extra-long environment string - should throw BadRequestException. */ @Test void shouldHideConfigToCurrentUser_extraLongString_throwsBadRequestException() { String longEnv = String.join("", Collections.nCopies(1000, "A")); assertThatThrownBy( () -> validator.shouldHideConfigToCurrentUser(APP_ID, longEnv, CLUSTER, NAMESPACE)) .isInstanceOf(BadRequestException.class); } /** * Test mixed case variations - should normalize correctly. */ @Test void shouldHideConfigToCurrentUser_mixedCaseVariations() { when(portalConfig.isConfigViewMemberOnly("PRO")).thenReturn(true); when(appNamespaceService.findByAppIdAndName(APP_ID, NAMESPACE)).thenReturn(null); when(rolePermissionService.isSuperAdmin(USER_ID)).thenReturn(false); // Test "PrOd" - should normalize to "PRO" assertThat(validator.shouldHideConfigToCurrentUser(APP_ID, "PrOd", CLUSTER, NAMESPACE)).isTrue(); when(portalConfig.isConfigViewMemberOnly("FAT")).thenReturn(true); // Test "FaT" - should normalize to "FAT" assertThat(validator.shouldHideConfigToCurrentUser(APP_ID, "FaT", CLUSTER, NAMESPACE)).isTrue(); when(portalConfig.isConfigViewMemberOnly("UAT")).thenReturn(true); // Test "uAt" - should normalize to "UAT" assertThat(validator.shouldHideConfigToCurrentUser(APP_ID, "uAt", CLUSTER, NAMESPACE)).isTrue(); } // ========== Invalid Environment Handling Tests ========== /** * Test invalid environment name - should throw BadRequestException. */ @Test void shouldHideConfigToCurrentUser_invalidEnv_throwsBadRequestException() { assertThatThrownBy( () -> validator.shouldHideConfigToCurrentUser(APP_ID, "INVALID_ENV", CLUSTER, NAMESPACE)) .isInstanceOf(BadRequestException.class).hasMessageContaining("invalid env format"); } /** * Test random string as environment - should throw BadRequestException. */ @Test void shouldHideConfigToCurrentUser_randomString_throwsBadRequestException() { assertThatThrownBy( () -> validator.shouldHideConfigToCurrentUser(APP_ID, "xyz123", CLUSTER, NAMESPACE)) .isInstanceOf(BadRequestException.class); } // ========== End-to-End Scenario Tests ========== /** * Test end-to-end scenario: user configures env=prod → normalizes to PRO → permission check uses PRO. * This verifies the complete flow from user input to permission validation. */ @Test void shouldHideConfigToCurrentUser_endToEnd_prodToPRO() { // Setup: config view is member-only for "PRO" (canonical form) when(portalConfig.isConfigViewMemberOnly("PRO")).thenReturn(true); when(appNamespaceService.findByAppIdAndName(APP_ID, NAMESPACE)).thenReturn(null); when(rolePermissionService.isSuperAdmin(USER_ID)).thenReturn(false); // User inputs "prod" (lowercase) - system should normalize to "PRO" and apply permission check boolean shouldHide = validator.shouldHideConfigToCurrentUser(APP_ID, "prod", CLUSTER, NAMESPACE); // Verify that config is hidden (permission check passed with normalized "PRO") assertThat(shouldHide).isTrue(); } /** * Test consistency: all prod variants (prod/PROD/PRO) should behave identically. */ @Test void shouldHideConfigToCurrentUser_consistency_allProdVariants() { when(portalConfig.isConfigViewMemberOnly("PRO")).thenReturn(true); when(appNamespaceService.findByAppIdAndName(APP_ID, NAMESPACE)).thenReturn(null); when(rolePermissionService.isSuperAdmin(USER_ID)).thenReturn(false); // All variants should produce the same result boolean resultProd = validator.shouldHideConfigToCurrentUser(APP_ID, "prod", CLUSTER, NAMESPACE); boolean resultPROD = validator.shouldHideConfigToCurrentUser(APP_ID, "PROD", CLUSTER, NAMESPACE); boolean resultPRO = validator.shouldHideConfigToCurrentUser(APP_ID, "PRO", CLUSTER, NAMESPACE); assertThat(resultProd).isEqualTo(resultPROD).isEqualTo(resultPRO).isTrue(); } /** * Test consistency: FWS and FAT should behave identically (FWS maps to FAT). */ @Test void shouldHideConfigToCurrentUser_consistency_fwsAndFat() { when(portalConfig.isConfigViewMemberOnly("FAT")).thenReturn(true); when(appNamespaceService.findByAppIdAndName(APP_ID, NAMESPACE)).thenReturn(null); when(rolePermissionService.isSuperAdmin(USER_ID)).thenReturn(false); // FWS and FAT should produce the same result boolean resultFWS = validator.shouldHideConfigToCurrentUser(APP_ID, "FWS", CLUSTER, NAMESPACE); boolean resultFAT = validator.shouldHideConfigToCurrentUser(APP_ID, "FAT", CLUSTER, NAMESPACE); assertThat(resultFWS).isEqualTo(resultFAT).isTrue(); } } ================================================ FILE: apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/component/config/PortalConfigTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.component.config; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.spy; import com.ctrip.framework.apollo.portal.service.PortalDBPropertySource; import java.util.Collections; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; /** * Test coverage for PortalConfig.isConfigViewMemberOnly() environment normalization. * Supplements Apollo PR #5442 fix for environment alias handling. * * @see #5442 */ @ExtendWith(MockitoExtension.class) class PortalConfigTest { @Mock private PortalDBPropertySource portalDBPropertySource; private PortalConfig portalConfig; @BeforeEach void setUp() { portalConfig = spy(new PortalConfig(portalDBPropertySource)); } // ========== All Environment Aliases Tests ========== /** * Test PRO environment and its aliases (prod/PROD/PRO). * Verifies issue #5442 fix where PROD maps to PRO. */ @Test void isConfigViewMemberOnly_prodAliases() { // Setup: configure "PRO" as member-only doReturn(new String[] {"PRO"}).when(portalConfig).getArrayProperty("configView.memberOnly.envs", new String[0]); // Test with "prod" lowercase - should normalize to "PRO" and return true assertThat(portalConfig.isConfigViewMemberOnly("prod")).isTrue(); // Test with "PROD" uppercase - should normalize to "PRO" and return true assertThat(portalConfig.isConfigViewMemberOnly("PROD")).isTrue(); // Test with "PRO" canonical form - should return true assertThat(portalConfig.isConfigViewMemberOnly("PRO")).isTrue(); } /** * Test FAT environment and its alias FWS (FWS maps to FAT). */ @Test void isConfigViewMemberOnly_fatAndFwsAliases() { // Setup: configure "FAT" as member-only doReturn(new String[] {"FAT"}).when(portalConfig).getArrayProperty("configView.memberOnly.envs", new String[0]); // Test with "fat" lowercase assertThat(portalConfig.isConfigViewMemberOnly("fat")).isTrue(); // Test with "FAT" uppercase assertThat(portalConfig.isConfigViewMemberOnly("FAT")).isTrue(); // Test with "FWS" - should normalize to "FAT" assertThat(portalConfig.isConfigViewMemberOnly("FWS")).isTrue(); // Test with "fws" lowercase - should also normalize to "FAT" assertThat(portalConfig.isConfigViewMemberOnly("fws")).isTrue(); } /** * Test LOCAL environment normalization. */ @Test void isConfigViewMemberOnly_localAlias() { doReturn(new String[] {"LOCAL"}).when(portalConfig) .getArrayProperty("configView.memberOnly.envs", new String[0]); // Test with "local" lowercase assertThat(portalConfig.isConfigViewMemberOnly("local")).isTrue(); // Test with "LOCAL" uppercase assertThat(portalConfig.isConfigViewMemberOnly("LOCAL")).isTrue(); } /** * Test DEV environment normalization. */ @Test void isConfigViewMemberOnly_devAlias() { doReturn(new String[] {"DEV"}).when(portalConfig).getArrayProperty("configView.memberOnly.envs", new String[0]); // Test with "dev" lowercase assertThat(portalConfig.isConfigViewMemberOnly("dev")).isTrue(); // Test with "DEV" uppercase assertThat(portalConfig.isConfigViewMemberOnly("DEV")).isTrue(); } /** * Test UAT environment normalization. */ @Test void isConfigViewMemberOnly_uatAlias() { doReturn(new String[] {"UAT"}).when(portalConfig).getArrayProperty("configView.memberOnly.envs", new String[0]); // Test with "uat" lowercase assertThat(portalConfig.isConfigViewMemberOnly("uat")).isTrue(); // Test with "UAT" uppercase assertThat(portalConfig.isConfigViewMemberOnly("UAT")).isTrue(); } /** * Test LPT environment normalization. */ @Test void isConfigViewMemberOnly_lptAlias() { doReturn(new String[] {"LPT"}).when(portalConfig).getArrayProperty("configView.memberOnly.envs", new String[0]); // Test with "lpt" lowercase assertThat(portalConfig.isConfigViewMemberOnly("lpt")).isTrue(); // Test with "LPT" uppercase assertThat(portalConfig.isConfigViewMemberOnly("LPT")).isTrue(); } /** * Test TOOLS environment normalization. */ @Test void isConfigViewMemberOnly_toolsAlias() { doReturn(new String[] {"TOOLS"}).when(portalConfig) .getArrayProperty("configView.memberOnly.envs", new String[0]); // Test with "tools" lowercase assertThat(portalConfig.isConfigViewMemberOnly("tools")).isTrue(); // Test with "TOOLS" uppercase assertThat(portalConfig.isConfigViewMemberOnly("TOOLS")).isTrue(); } // ========== Boundary Value Tests ========== /** * Test empty string environment - should return false (safe default). */ @Test void isConfigViewMemberOnly_emptyString_returnsFalse() { // Empty string should be treated as invalid and return false for safety assertThat(portalConfig.isConfigViewMemberOnly("")).isFalse(); } /** * Test whitespace-only environment strings - should return false. */ @Test void isConfigViewMemberOnly_whitespaceStrings_returnsFalse() { // Test with spaces assertThat(portalConfig.isConfigViewMemberOnly(" ")).isFalse(); // Test with tab assertThat(portalConfig.isConfigViewMemberOnly("\t")).isFalse(); // Test with newline assertThat(portalConfig.isConfigViewMemberOnly("\n")).isFalse(); } /** * Test special characters in environment name - should return false. */ @Test void isConfigViewMemberOnly_specialCharacters_returnsFalse() { // Test with @ symbol assertThat(portalConfig.isConfigViewMemberOnly("env@123")).isFalse(); // Test with # symbol assertThat(portalConfig.isConfigViewMemberOnly("env#test")).isFalse(); // Test with spaces in name assertThat(portalConfig.isConfigViewMemberOnly("env with spaces")).isFalse(); } /** * Test extra-long environment string - should return false. */ @Test void isConfigViewMemberOnly_extraLongString_returnsFalse() { String longEnv = String.join("", Collections.nCopies(1000, "A")); assertThat(portalConfig.isConfigViewMemberOnly(longEnv)).isFalse(); } /** * Test mixed case variations - should normalize correctly. */ @Test void isConfigViewMemberOnly_mixedCaseVariations() { doReturn(new String[] {"PRO", "FAT", "UAT"}).when(portalConfig) .getArrayProperty("configView.memberOnly.envs", new String[0]); // Test "PrOd" - should normalize to "PRO" assertThat(portalConfig.isConfigViewMemberOnly("PrOd")).isTrue(); // Test "FaT" - should normalize to "FAT" assertThat(portalConfig.isConfigViewMemberOnly("FaT")).isTrue(); // Test "uAt" - should normalize to "UAT" assertThat(portalConfig.isConfigViewMemberOnly("uAt")).isTrue(); } // ========== Invalid Environment Handling Tests ========== /** * Test invalid environment name - should return false (safe default). */ @Test void isConfigViewMemberOnly_invalidEnv_returnsFalse() { // Invalid env should return false for safety assertThat(portalConfig.isConfigViewMemberOnly("INVALID_ENV")).isFalse(); } /** * Test random string as environment - should return false. */ @Test void isConfigViewMemberOnly_randomString_returnsFalse() { assertThat(portalConfig.isConfigViewMemberOnly("xyz123")).isFalse(); } /** * Test null environment - should return false. */ @Test void isConfigViewMemberOnly_nullEnv_returnsFalse() { assertThat(portalConfig.isConfigViewMemberOnly(null)).isFalse(); } // ========== Consistency Validation Tests ========== /** * Test consistency: all prod variants (prod/PROD/PRO) should behave identically. * Verifies that PortalConfig and UserPermissionValidator handle env normalization consistently. */ @Test void isConfigViewMemberOnly_consistency_allProdVariants() { doReturn(new String[] {"PRO"}).when(portalConfig).getArrayProperty("configView.memberOnly.envs", new String[0]); // All variants should produce the same result boolean resultProd = portalConfig.isConfigViewMemberOnly("prod"); boolean resultPROD = portalConfig.isConfigViewMemberOnly("PROD"); boolean resultPRO = portalConfig.isConfigViewMemberOnly("PRO"); assertThat(resultProd).isEqualTo(resultPROD).isEqualTo(resultPRO).isTrue(); } /** * Test consistency: FWS and FAT should behave identically (FWS maps to FAT). */ @Test void isConfigViewMemberOnly_consistency_fwsAndFat() { doReturn(new String[] {"FAT"}).when(portalConfig).getArrayProperty("configView.memberOnly.envs", new String[0]); // FWS and FAT should produce the same result boolean resultFWS = portalConfig.isConfigViewMemberOnly("FWS"); boolean resultFAT = portalConfig.isConfigViewMemberOnly("FAT"); assertThat(resultFWS).isEqualTo(resultFAT).isTrue(); } /** * Test consistency: invalid env handling should always return false. * Ensures predictable behavior across all invalid inputs. */ @Test void isConfigViewMemberOnly_consistency_invalidEnvAlwaysFalse() { // All invalid inputs should return false assertThat(portalConfig.isConfigViewMemberOnly("")).isFalse(); assertThat(portalConfig.isConfigViewMemberOnly(" ")).isFalse(); assertThat(portalConfig.isConfigViewMemberOnly("INVALID")).isFalse(); assertThat(portalConfig.isConfigViewMemberOnly("xyz123")).isFalse(); assertThat(portalConfig.isConfigViewMemberOnly(null)).isFalse(); } // ========== End-to-End Scenario Tests ========== /** * Test end-to-end scenario: user configures env=prod → normalizes to PRO → config view check uses PRO. */ @Test void isConfigViewMemberOnly_endToEnd_prodToPRO() { // Setup: configure "PRO" as member-only doReturn(new String[] {"PRO"}).when(portalConfig).getArrayProperty("configView.memberOnly.envs", new String[0]); // User inputs "prod" (lowercase) - system should normalize to "PRO" and return true boolean isMemberOnly = portalConfig.isConfigViewMemberOnly("prod"); // Verify that member-only check passed with normalized "PRO" assertThat(isMemberOnly).isTrue(); } /** * Test multiple environments configured as member-only. */ @Test void isConfigViewMemberOnly_multipleEnvs() { doReturn(new String[] {"PRO", "UAT", "FAT"}).when(portalConfig) .getArrayProperty("configView.memberOnly.envs", new String[0]); // All configured envs and their aliases should return true assertThat(portalConfig.isConfigViewMemberOnly("prod")).isTrue(); assertThat(portalConfig.isConfigViewMemberOnly("PROD")).isTrue(); assertThat(portalConfig.isConfigViewMemberOnly("PRO")).isTrue(); assertThat(portalConfig.isConfigViewMemberOnly("uat")).isTrue(); assertThat(portalConfig.isConfigViewMemberOnly("UAT")).isTrue(); assertThat(portalConfig.isConfigViewMemberOnly("fat")).isTrue(); assertThat(portalConfig.isConfigViewMemberOnly("FAT")).isTrue(); assertThat(portalConfig.isConfigViewMemberOnly("FWS")).isTrue(); // Non-configured env should return false assertThat(portalConfig.isConfigViewMemberOnly("DEV")).isFalse(); } } ================================================ FILE: apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/component/txtresolver/FileTextResolverTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.component.txtresolver; import com.ctrip.framework.apollo.common.dto.ItemChangeSets; import com.ctrip.framework.apollo.common.dto.ItemDTO; import com.ctrip.framework.apollo.core.ConfigConsts; import com.ctrip.framework.apollo.portal.AbstractUnitTest; import org.junit.Assert; import org.junit.Test; import org.mockito.InjectMocks; import java.util.Collections; public class FileTextResolverTest extends AbstractUnitTest { @InjectMocks private FileTextResolver resolver; private final String CONFIG_TEXT = "config_text"; private final long NAMESPACE = 1000; @Test public void testCreateItem() { ItemChangeSets changeSets = resolver.resolve(NAMESPACE, CONFIG_TEXT, Collections.emptyList()); Assert.assertEquals(1, changeSets.getCreateItems().size()); Assert.assertEquals(0, changeSets.getUpdateItems().size()); Assert.assertEquals(0, changeSets.getDeleteItems().size()); ItemDTO createdItem = changeSets.getCreateItems().get(0); Assert.assertEquals(CONFIG_TEXT, createdItem.getValue()); } @Test public void testUpdateItem() { ItemDTO existedItem = new ItemDTO(); existedItem.setId(1000); existedItem.setKey(ConfigConsts.CONFIG_FILE_CONTENT_KEY); existedItem.setValue("before"); ItemChangeSets changeSets = resolver.resolve(NAMESPACE, CONFIG_TEXT, Collections.singletonList(existedItem)); Assert.assertEquals(0, changeSets.getCreateItems().size()); Assert.assertEquals(1, changeSets.getUpdateItems().size()); Assert.assertEquals(0, changeSets.getDeleteItems().size()); ItemDTO updatedItem = changeSets.getUpdateItems().get(0); Assert.assertEquals(CONFIG_TEXT, updatedItem.getValue()); } } ================================================ FILE: apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/component/txtresolver/PropertyResolverTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.component.txtresolver; import com.ctrip.framework.apollo.common.dto.ItemChangeSets; import com.ctrip.framework.apollo.common.dto.ItemDTO; import com.ctrip.framework.apollo.common.exception.BadRequestException; import com.ctrip.framework.apollo.portal.AbstractUnitTest; import org.junit.Assert; import org.junit.Test; import org.mockito.InjectMocks; import java.util.Arrays; import java.util.Collections; import java.util.List; public class PropertyResolverTest extends AbstractUnitTest { @InjectMocks private PropertyResolver resolver; @Test public void testEmptyText() { try { resolver.resolve(0, "", null); } catch (Exception e) { Assert.assertTrue(e instanceof BadRequestException); } } @Test public void testRepeatKey() { try { resolver.resolve(1, "a=b\nb=c\nA=d\nB=e", Collections.emptyList()); } catch (Exception e) { Assert.assertTrue(e instanceof BadRequestException); } } @Test public void testAddItemBeforeNoItem() { ItemChangeSets changeSets = resolver.resolve(1, "a=b\nb=c", Collections.emptyList()); Assert.assertEquals(2, changeSets.getCreateItems().size()); } @Test public void testAddItemBeforeHasItem() { ItemChangeSets changeSets = resolver.resolve(1, "x=y\na=b\nb=c\nc=d", mockBaseItemHas3Key()); Assert.assertEquals("x", changeSets.getCreateItems().get(0).getKey()); Assert.assertEquals(1, changeSets.getCreateItems().size()); Assert.assertEquals(3, changeSets.getUpdateItems().size()); } @Test public void testAddCommentAndBlankItem() { ItemChangeSets changeSets = resolver.resolve(1, "#ddd\na=b\n\nb=c\nc=d", mockBaseItemHas3Key()); Assert.assertEquals(2, changeSets.getCreateItems().size()); Assert.assertEquals(3, changeSets.getUpdateItems().size()); } @Test public void testChangeItemNumLine() { ItemChangeSets changeSets = resolver.resolve(1, "b=c\nc=d\na=b", mockBaseItemHas3Key()); Assert.assertEquals(3, changeSets.getUpdateItems().size()); } @Test public void testDeleteItem() { ItemChangeSets changeSets = resolver.resolve(1, "a=b", mockBaseItemHas3Key()); Assert.assertEquals(2, changeSets.getDeleteItems().size()); } @Test public void testDeleteCommentItem() { ItemChangeSets changeSets = resolver.resolve(1, "a=b\n\nb=c", mockBaseItemWith2Key1Comment1Blank()); Assert.assertEquals(1, changeSets.getDeleteItems().size()); Assert.assertEquals(3, changeSets.getUpdateItems().size()); Assert.assertEquals(0, changeSets.getCreateItems().size()); } @Test public void testDeleteBlankItem() { ItemChangeSets changeSets = resolver.resolve(1, "#qqqq\na=b\nb=c", mockBaseItemWith2Key1Comment1Blank()); Assert.assertEquals(1, changeSets.getDeleteItems().size()); Assert.assertEquals(1, changeSets.getUpdateItems().size()); Assert.assertEquals(0, changeSets.getCreateItems().size()); } @Test public void testUpdateItem() { ItemChangeSets changeSets = resolver.resolve(1, "a=d", mockBaseItemHas3Key()); List updateItems = changeSets.getUpdateItems(); Assert.assertEquals(1, updateItems.size()); Assert.assertEquals("d", updateItems.get(0).getValue()); } @Test public void testUpdateCommentItem() { ItemChangeSets changeSets = resolver.resolve(1, "#ww\n" + "a=b\n" + "\n" + "b=c", mockBaseItemWith2Key1Comment1Blank()); Assert.assertEquals(0, changeSets.getDeleteItems().size()); Assert.assertEquals(1, changeSets.getUpdateItems().size()); Assert.assertEquals(0, changeSets.getCreateItems().size()); } @Test public void testAllSituation() { ItemChangeSets changeSets = resolver.resolve(1, "#ww\nd=e\nb=c\na=b\n\nq=w\n#eee", mockBaseItemWith2Key1Comment1Blank()); Assert.assertEquals(0, changeSets.getDeleteItems().size()); Assert.assertEquals(4, changeSets.getUpdateItems().size()); Assert.assertEquals(3, changeSets.getCreateItems().size()); } /** * a=b b=c c=d */ private List mockBaseItemHas3Key() { ItemDTO item1 = new ItemDTO("a", "b", "", 1); ItemDTO item2 = new ItemDTO("b", "c", "", 2); ItemDTO item3 = new ItemDTO("c", "d", "", 3); return Arrays.asList(item1, item2, item3); } /** * #qqqq * a=b * * b=c */ private List mockBaseItemWith2Key1Comment1Blank() { ItemDTO i1 = new ItemDTO("", "", "#qqqq", 1); ItemDTO i2 = new ItemDTO("a", "b", "", 2); ItemDTO i3 = new ItemDTO("", "", "", 3); ItemDTO i4 = new ItemDTO("b", "c", "", 4); i4.setLineNum(4); return Arrays.asList(i1, i2, i3, i4); } } ================================================ FILE: apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/config/ConfigTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.config; import com.ctrip.framework.apollo.portal.AbstractUnitTest; import com.ctrip.framework.apollo.portal.component.config.PortalConfig; import com.ctrip.framework.apollo.portal.service.PortalDBPropertySource; import org.junit.Before; import org.junit.Assert; import org.junit.Test; import org.mockito.Mock; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.core.env.ConfigurableEnvironment; import static org.mockito.Mockito.when; public class ConfigTest extends AbstractUnitTest { @Mock private ConfigurableEnvironment environment; @Mock private PortalDBPropertySource portalDBPropertySource; private PortalConfig config; @Before public void setUp() { config = new PortalConfig(portalDBPropertySource); ReflectionTestUtils.setField(config, "environment", environment, ConfigurableEnvironment.class); } @Test public void testGetNotExistValue() { String testKey = "key"; String testDefaultValue = "value"; when(environment.getProperty(testKey, testDefaultValue)).thenReturn(testDefaultValue); Assert.assertEquals(testDefaultValue, config.getValue(testKey, testDefaultValue)); } @Test public void testGetArrayProperty() { String testKey = "key"; String testValue = "a,b,c"; when(environment.getProperty(testKey)).thenReturn(testValue); String[] result = config.getArrayProperty(testKey, null); Assert.assertEquals(3, result.length); Assert.assertEquals("a", result[0]); Assert.assertEquals("b", result[1]); Assert.assertEquals("c", result[2]); } @Test public void testGetBooleanProperty() { String testKey = "key"; String testValue = "true"; when(environment.getProperty(testKey)).thenReturn(testValue); boolean result = config.getBooleanProperty(testKey, false); Assert.assertTrue(result); } @Test public void testGetIntProperty() { String testKey = "key"; String testValue = "1024"; when(environment.getProperty(testKey)).thenReturn(testValue); int result = config.getIntProperty(testKey, 0); Assert.assertEquals(1024, result); } } ================================================ FILE: apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/controller/ClusterControllerTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.controller; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertSame; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import com.ctrip.framework.apollo.common.dto.ClusterDTO; import com.ctrip.framework.apollo.portal.entity.bo.UserInfo; import com.ctrip.framework.apollo.portal.environment.Env; import com.ctrip.framework.apollo.portal.service.ClusterService; import com.ctrip.framework.apollo.portal.spi.UserInfoHolder; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import org.springframework.http.ResponseEntity; @RunWith(MockitoJUnitRunner.class) public class ClusterControllerTest { @Mock private ClusterService clusterService; @Mock private UserInfoHolder userInfoHolder; @InjectMocks private ClusterController clusterController; @Captor private ArgumentCaptor clusterCaptor; @Test public void shouldCreateClusterWithCurrentOperator() { ClusterDTO toCreate = new ClusterDTO(); toCreate.setAppId("SampleApp"); toCreate.setName("sampleCluster"); ClusterDTO created = new ClusterDTO(); created.setAppId("SampleApp"); created.setName("sampleCluster"); when(userInfoHolder.getUser()).thenReturn(new UserInfo("apollo")); when(clusterService.createCluster(eq(Env.DEV), clusterCaptor.capture())).thenReturn(created); ClusterDTO result = clusterController.createCluster("SampleApp", "DEV", toCreate); assertSame(created, result); ClusterDTO captured = clusterCaptor.getValue(); assertEquals("apollo", captured.getDataChangeCreatedBy()); assertEquals("apollo", captured.getDataChangeLastModifiedBy()); } @Test public void shouldDeleteClusterByEnvAndName() { ResponseEntity response = clusterController.deleteCluster("SampleApp", "DEV", "sampleCluster"); assertEquals(200, response.getStatusCodeValue()); verify(clusterService).deleteCluster(Env.DEV, "SampleApp", "sampleCluster"); } @Test public void shouldLoadClusterFromService() { ClusterDTO loaded = new ClusterDTO(); loaded.setName("sampleCluster"); when(clusterService.loadCluster("SampleApp", Env.DEV, "sampleCluster")).thenReturn(loaded); ClusterDTO result = clusterController.loadCluster("SampleApp", "DEV", "sampleCluster"); assertSame(loaded, result); verify(clusterService).loadCluster("SampleApp", Env.DEV, "sampleCluster"); } } ================================================ FILE: apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/controller/CommitControllerTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.controller; import com.ctrip.framework.apollo.portal.AbstractIntegrationTest; import org.junit.Test; import org.springframework.web.client.HttpClientErrorException; import java.util.List; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.core.StringContains.containsString; import static org.junit.Assert.fail; /** * Created by kezhenxu at 2019/1/14 12:49. * * @author kezhenxu (kezhenxu at lizhi dot fm) */ public class CommitControllerTest extends AbstractIntegrationTest { @Test public void shouldFailWhenPageOrSiseIsNegative() { try { restTemplate.getForEntity(url( "/apps/{appId}/envs/{env}/clusters/{clusterName}/namespaces/{namespaceName}/commits?page=-1"), List.class, "1", "env", "cl", "ns"); fail("should throw"); } catch (final HttpClientErrorException e) { assertThat(new String(e.getResponseBodyAsByteArray()), containsString("page should be positive or 0")); } try { restTemplate.getForEntity(url( "/apps/{appId}/envs/{env}/clusters/{clusterName}/namespaces/{namespaceName}/commits?size=0"), List.class, "1", "env", "cl", "ns"); fail("should throw"); } catch (final HttpClientErrorException e) { assertThat(new String(e.getResponseBodyAsByteArray()), containsString("size should be positive number")); } } } ================================================ FILE: apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/controller/ConfigsExportControllerTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.controller; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import com.ctrip.framework.apollo.common.dto.ItemDTO; import com.ctrip.framework.apollo.common.exception.ServiceException; import com.ctrip.framework.apollo.portal.entity.bo.ItemBO; import com.ctrip.framework.apollo.portal.entity.bo.NamespaceBO; import com.ctrip.framework.apollo.portal.environment.Env; import com.ctrip.framework.apollo.portal.service.ConfigsExportService; import com.ctrip.framework.apollo.portal.service.NamespaceService; import java.io.IOException; import java.io.OutputStream; import java.util.Arrays; import java.util.Collections; import jakarta.servlet.ServletOutputStream; import jakarta.servlet.WriteListener; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import org.springframework.http.HttpHeaders; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; @RunWith(MockitoJUnitRunner.class) public class ConfigsExportControllerTest { @Mock private ConfigsExportService configsExportService; @Mock private NamespaceService namespaceService; @InjectMocks private ConfigsExportController configsExportController; @Test public void shouldExportNamespaceWithPropertiesSuffixWhenMissing() { NamespaceBO namespace = new NamespaceBO(); namespace.setFormat("properties"); namespace.setItems(Collections.singletonList(itemBO("timeout", "100"))); when(namespaceService.loadNamespaceBO("SampleApp", Env.DEV, "default", "application", true, false)).thenReturn(namespace); MockHttpServletResponse response = new MockHttpServletResponse(); configsExportController.exportItems("SampleApp", "DEV", "default", "application", response); assertEquals("attachment;filename=application.properties", response.getHeader(HttpHeaders.CONTENT_DISPOSITION)); assertTrue(new String(response.getContentAsByteArray()).contains("\"key\":\"timeout\"")); } @Test public void shouldExportNamespaceKeepOriginalSuffixWhenFormatIsValid() { NamespaceBO namespace = new NamespaceBO(); namespace.setFormat("yml"); namespace.setItems(Collections.singletonList(itemBO("content", "a: b"))); when(namespaceService.loadNamespaceBO("SampleApp", Env.DEV, "default", "application.yml", true, false)).thenReturn(namespace); MockHttpServletResponse response = new MockHttpServletResponse(); configsExportController.exportItems("SampleApp", "DEV", "default", "application.yml", response); assertEquals("attachment;filename=application.yml", response.getHeader(HttpHeaders.CONTENT_DISPOSITION)); assertTrue(new String(response.getContentAsByteArray()).contains("\"key\":\"content\"")); } @Test(expected = ServiceException.class) public void shouldWrapExportNamespaceIOExceptionAsServiceException() throws IOException { NamespaceBO namespace = new NamespaceBO(); namespace.setFormat("properties"); namespace.setItems(Collections.singletonList(itemBO("timeout", "100"))); when(namespaceService.loadNamespaceBO("SampleApp", Env.DEV, "default", "application", true, false)).thenReturn(namespace); HttpServletResponse response = org.mockito.Mockito.mock(HttpServletResponse.class); when(response.getOutputStream()).thenReturn(new FailingServletOutputStream()); configsExportController.exportItems("SampleApp", "DEV", "default", "application", response); } @Test public void shouldExportAllConfigsWithParsedEnvs() throws IOException { MockHttpServletRequest request = new MockHttpServletRequest(); request.setRemoteAddr("127.0.0.1"); request.setRemoteHost("localhost"); MockHttpServletResponse response = new MockHttpServletResponse(); doAnswer(invocation -> { OutputStream outputStream = invocation.getArgument(0); outputStream.write("ok".getBytes()); return null; }).when(configsExportService).exportData(any(OutputStream.class), eq(Arrays.asList(Env.DEV, Env.FAT))); configsExportController.exportAll("DEV,FAT", request, response); verify(configsExportService).exportData(any(OutputStream.class), eq(Arrays.asList(Env.DEV, Env.FAT))); assertTrue(response.getHeader(HttpHeaders.CONTENT_DISPOSITION) .startsWith("attachment;filename=apollo_config_export_")); assertEquals("ok", response.getContentAsString()); } @Test public void shouldExportAppConfigByEnvAndCluster() throws IOException { HttpServletRequest request = new MockHttpServletRequest(); MockHttpServletResponse response = new MockHttpServletResponse(); doAnswer(invocation -> { OutputStream outputStream = invocation.getArgument(3); outputStream.write("app".getBytes()); return null; }).when(configsExportService).exportAppConfigByEnvAndCluster(eq("SampleApp"), eq(Env.DEV), eq("default"), any(OutputStream.class)); configsExportController.exportAppConfig("SampleApp", "DEV", "default", request, response); verify(configsExportService).exportAppConfigByEnvAndCluster(eq("SampleApp"), eq(Env.DEV), eq("default"), any(OutputStream.class)); assertTrue(response.getHeader(HttpHeaders.CONTENT_DISPOSITION) .startsWith("attachment;filename=SampleApp+DEV+default+")); assertEquals("app", response.getContentAsString()); } private ItemBO itemBO(String key, String value) { ItemDTO itemDTO = new ItemDTO(); itemDTO.setKey(key); itemDTO.setValue(value); ItemBO itemBO = new ItemBO(); itemBO.setItem(itemDTO); return itemBO; } private static class FailingServletOutputStream extends ServletOutputStream { @Override public void write(int b) throws IOException { throw new IOException("forced write failure"); } @Override public boolean isReady() { return true; } @Override public void setWriteListener(WriteListener writeListener) { // no-op } } } ================================================ FILE: apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/controller/ConfigsImportControllerTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.controller; import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.verify; import com.ctrip.framework.apollo.common.exception.BadRequestException; import com.ctrip.framework.apollo.portal.environment.Env; import com.ctrip.framework.apollo.portal.service.ConfigsImportService; import com.ctrip.framework.apollo.portal.util.ConfigFileUtils; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.Arrays; import java.util.Collections; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import org.springframework.mock.web.MockMultipartFile; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.test.web.servlet.setup.MockMvcBuilders; @RunWith(MockitoJUnitRunner.class) public class ConfigsImportControllerTest { @Mock private ConfigsImportService configsImportService; @InjectMocks private ConfigsImportController configsImportController; @Captor private ArgumentCaptor ignoreConflictCaptor; @Captor private ArgumentCaptor> envsCaptor; private MockMvc mockMvc; @Before public void setUp() { mockMvc = MockMvcBuilders.standaloneSetup(configsImportController).build(); } @Test public void shouldImportConfigFileWithStandardizedFilename() throws IOException { MockMultipartFile file = new MockMultipartFile("file", "application.yml", "application/yaml", "a: b".getBytes()); configsImportController.importConfigFile("SampleApp", "DEV", "default", "application", file); String expected = ConfigFileUtils.toFilename("SampleApp", "default", "application", com.ctrip.framework.apollo.core.enums.ConfigFileFormat.YML); verify(configsImportService).forceImportNamespaceFromFile(eq(Env.DEV), eq(expected), any(java.io.InputStream.class)); } @Test public void shouldUseDefaultIgnoreConflictActionWhenImportingAllConfigs() throws Exception { MockMultipartFile zip = new MockMultipartFile("file", "all.zip", "application/zip", zipBytes("apollo/demo.txt", "x")); mockMvc .perform( MockMvcRequestBuilders.multipart("/configs/import").file(zip).param("envs", "DEV,FAT")) .andExpect( org.springframework.test.web.servlet.result.MockMvcResultMatchers.status().isOk()); verify(configsImportService).importDataFromZipFile(envsCaptor.capture(), any(java.util.zip.ZipInputStream.class), ignoreConflictCaptor.capture()); assertEquals(Arrays.asList(Env.DEV, Env.FAT), envsCaptor.getValue()); assertEquals(Boolean.TRUE, ignoreConflictCaptor.getValue()); } @Test public void shouldParseCoverConflictActionWhenImportingAllConfigs() throws Exception { MockMultipartFile zip = new MockMultipartFile("file", "all.zip", "application/zip", zipBytes("apollo/demo.txt", "x")); mockMvc.perform(MockMvcRequestBuilders.multipart("/configs/import").file(zip) .param("envs", "DEV").param("conflictAction", "cover")).andExpect( org.springframework.test.web.servlet.result.MockMvcResultMatchers.status().isOk()); verify(configsImportService).importDataFromZipFile(eq(Collections.singletonList(Env.DEV)), any(java.util.zip.ZipInputStream.class), eq(false)); } @Test(expected = BadRequestException.class) public void shouldRejectInvalidConflictActionWhenImportingAllConfigs() throws IOException { MockMultipartFile zip = new MockMultipartFile("file", "all.zip", "application/zip", zipBytes("apollo/demo.txt", "x")); configsImportController.importConfigByZip("DEV", "invalid", zip); } @Test public void shouldUseDefaultIgnoreConflictActionWhenImportingAppConfigs() throws Exception { MockMultipartFile zip = new MockMultipartFile("file", "app.zip", "application/zip", zipBytes("SampleApp/DEV/demo", "x")); mockMvc .perform(MockMvcRequestBuilders .multipart("/apps/{appId}/envs/{env}/clusters/{clusterName}/import", "SampleApp", "DEV", "default") .file(zip)) .andExpect( org.springframework.test.web.servlet.result.MockMvcResultMatchers.status().isOk()); verify(configsImportService).importAppConfigFromZipFile(eq("SampleApp"), eq(Env.DEV), eq("default"), any(java.util.zip.ZipInputStream.class), eq(true)); } @Test(expected = BadRequestException.class) public void shouldRejectInvalidConflictActionWhenImportingAppConfigs() throws IOException { MockMultipartFile zip = new MockMultipartFile("file", "app.zip", "application/zip", zipBytes("SampleApp/DEV/demo", "x")); configsImportController.importAppConfigByZip("SampleApp", "DEV", "default", "bad", zip); } private byte[] zipBytes(String entryName, String content) throws IOException { ByteArrayOutputStream output = new ByteArrayOutputStream(); try (ZipOutputStream zipOutputStream = new ZipOutputStream(output)) { zipOutputStream.putNextEntry(new ZipEntry(entryName)); zipOutputStream.write(content.getBytes()); zipOutputStream.closeEntry(); } return output.toByteArray(); } } ================================================ FILE: apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/controller/ConsumerControllerTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.controller; import static org.junit.jupiter.api.Assertions.*; import com.ctrip.framework.apollo.common.exception.BadRequestException; import com.ctrip.framework.apollo.openapi.entity.ConsumerToken; import com.ctrip.framework.apollo.openapi.service.ConsumerService; import com.ctrip.framework.apollo.portal.entity.vo.consumer.ConsumerCreateRequestVO; import org.junit.jupiter.api.Test; import org.mockito.Mockito; class ConsumerControllerTest { @Test void createWithBadRequest() { ConsumerService consumerService = Mockito.mock(ConsumerService.class); ConsumerController consumerController = new ConsumerController(consumerService); ConsumerCreateRequestVO requestVO = new ConsumerCreateRequestVO(); // blank appId assertThrows(BadRequestException.class, () -> consumerController.create(requestVO, null)); requestVO.setAppId("appId1"); // blank name assertThrows(BadRequestException.class, () -> consumerController.create(requestVO, null)); requestVO.setName("app 1"); // blank ownerName assertThrows(BadRequestException.class, () -> consumerController.create(requestVO, null)); requestVO.setOwnerName("user1"); // blank orgId assertThrows(BadRequestException.class, () -> consumerController.create(requestVO, null)); requestVO.setOrgId("orgId1"); } @Test void createWithCompatibility() { ConsumerService consumerService = Mockito.mock(ConsumerService.class); ConsumerController consumerController = new ConsumerController(consumerService); ConsumerCreateRequestVO requestVO = new ConsumerCreateRequestVO(); requestVO.setAppId("appId1"); requestVO.setName("app 1"); requestVO.setOwnerName("user1"); requestVO.setOrgId("orgId1"); consumerController.create(requestVO, null); Mockito.verify(consumerService, Mockito.times(1)).createConsumer(Mockito.any()); Mockito.verify(consumerService, Mockito.times(1)).generateAndSaveConsumerToken(Mockito.any(), Mockito.any(), Mockito.any()); Mockito.verify(consumerService, Mockito.times(0)) .assignCreateApplicationRoleToConsumer(Mockito.any()); Mockito.verify(consumerService, Mockito.times(1)).getConsumerInfoByAppId(Mockito.any()); } @Test void createAndAssignCreateApplicationRoleToConsumer() { ConsumerService consumerService = Mockito.mock(ConsumerService.class); ConsumerController consumerController = new ConsumerController(consumerService); ConsumerCreateRequestVO requestVO = new ConsumerCreateRequestVO(); requestVO.setAppId("appId1"); requestVO.setName("app 1"); requestVO.setOwnerName("user1"); requestVO.setOrgId("orgId1"); requestVO.setAllowCreateApplication(true); final String token = "token-xxx"; { ConsumerToken ConsumerToken = new ConsumerToken(); ConsumerToken.setToken(token); Mockito.when( consumerService.generateAndSaveConsumerToken(Mockito.any(), Mockito.any(), Mockito.any())) .thenReturn(ConsumerToken); } consumerController.create(requestVO, null); Mockito.verify(consumerService, Mockito.times(1)).createConsumer(Mockito.any()); Mockito.verify(consumerService, Mockito.times(1)).generateAndSaveConsumerToken(Mockito.any(), Mockito.any(), Mockito.any()); Mockito.verify(consumerService, Mockito.times(1)) .assignCreateApplicationRoleToConsumer(Mockito.eq(token)); Mockito.verify(consumerService, Mockito.times(1)).getConsumerInfoByAppId(Mockito.any()); } } ================================================ FILE: apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/controller/EnvControllerTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.controller; import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.when; import com.ctrip.framework.apollo.portal.component.PortalSettings; import com.ctrip.framework.apollo.portal.environment.Env; import java.util.Arrays; import java.util.List; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; @RunWith(MockitoJUnitRunner.class) public class EnvControllerTest { @Mock private PortalSettings portalSettings; @InjectMocks private EnvController envController; @Test public void shouldReturnActiveEnvNames() { when(portalSettings.getActiveEnvs()).thenReturn(Arrays.asList(Env.DEV, Env.FAT, Env.UAT)); List result = envController.envs(); assertEquals(Arrays.asList("DEV", "FAT", "UAT"), result); } } ================================================ FILE: apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/controller/GlobalSearchControllerTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.controller; /** * @author hujiyuan 2024-08-10 */ import com.ctrip.framework.apollo.common.http.SearchResponseEntity; import com.ctrip.framework.apollo.portal.component.config.PortalConfig; import com.ctrip.framework.apollo.portal.entity.vo.ItemInfo; import com.ctrip.framework.apollo.portal.service.GlobalSearchService; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import java.util.*; import static org.mockito.Mockito.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @RunWith(MockitoJUnitRunner.class) public class GlobalSearchControllerTest { private MockMvc mockMvc; @Mock private PortalConfig portalConfig; @Mock private GlobalSearchService globalSearchService; @InjectMocks private GlobalSearchController globalSearchController; private final int perEnvSearchMaxResults = 200; @Before public void setUp() { when(portalConfig.getPerEnvSearchMaxResults()).thenReturn(perEnvSearchMaxResults); mockMvc = MockMvcBuilders.standaloneSetup(globalSearchController).build(); } @Test public void testGet_ItemInfo_BySearch_WithKeyAndValueAndActiveEnvs_ReturnEmptyItemInfos() throws Exception { when(globalSearchService.getAllEnvItemInfoBySearch(anyString(), anyString(),eq(0),eq(perEnvSearchMaxResults))).thenReturn(SearchResponseEntity.ok(new ArrayList<>())); mockMvc.perform(MockMvcRequestBuilders.get("/global-search/item-info/by-key-or-value") .contentType(MediaType.APPLICATION_JSON) .param("key", "query-key") .param("value", "query-value")) .andExpect(status().isOk()) .andExpect(content().json("{\"body\":[],\"hasMoreData\":false,\"message\":\"OK\",\"code\":200}")); verify(portalConfig,times(1)).getPerEnvSearchMaxResults(); verify(globalSearchService,times(1)).getAllEnvItemInfoBySearch(anyString(), anyString(),eq(0),eq(perEnvSearchMaxResults)); } @Test public void testGet_ItemInfo_BySearch_WithKeyAndValueAndActiveEnvs_ReturnExpectedItemInfos_ButOverPerEnvLimit() throws Exception { List allEnvMockItemInfos = new ArrayList<>(); allEnvMockItemInfos .add(new ItemInfo("appid1", "env1", "cluster1", "namespace1", "query-key", "query-value")); allEnvMockItemInfos .add(new ItemInfo("appid2", "env2", "cluster2", "namespace2", "query-key", "query-value")); when(globalSearchService.getAllEnvItemInfoBySearch(eq("query-key"), eq("query-value"), eq(0), eq(perEnvSearchMaxResults))) .thenReturn(SearchResponseEntity.okWithMessage(allEnvMockItemInfos, "In DEV , PRO , more than " + perEnvSearchMaxResults + " items found (Exceeded the maximum search quantity for a single environment). Please enter more precise criteria to narrow down the search scope.")); mockMvc .perform(MockMvcRequestBuilders.get("/global-search/item-info/by-key-or-value") .contentType(MediaType.APPLICATION_JSON).param("key", "query-key") .param("value", "query-value")) .andExpect(status().isOk()) .andExpect(content().json("{\"body\":[" + " { \"appId\": \"appid1\",\n" + " \"envName\": \"env1\",\n" + " \"clusterName\": \"cluster1\",\n" + " \"namespaceName\": \"namespace1\",\n" + " \"key\": \"query-key\",\n" + " \"value\": \"query-value\"}," + " { \"appId\": \"appid2\",\n" + " \"envName\": \"env2\",\n" + " \"clusterName\": \"cluster2\",\n" + " \"namespaceName\": \"namespace2\",\n" + " \"key\": \"query-key\",\n" + " \"value\": \"query-value\"}],\"hasMoreData\":true,\"message\":\"In DEV , PRO , more than 200 items found (Exceeded the maximum search quantity for a single environment). Please enter more precise criteria to narrow down the search scope.\",\"code\":200}")); verify(portalConfig, times(1)).getPerEnvSearchMaxResults(); verify(globalSearchService, times(1)).getAllEnvItemInfoBySearch(eq("query-key"), eq("query-value"), eq(0), eq(perEnvSearchMaxResults)); } @Test public void testGet_ItemInfo_BySearch_WithKeyAndValueAndActiveEnvs_ReturnExpectedItemInfos() throws Exception { List allEnvMockItemInfos = new ArrayList<>(); allEnvMockItemInfos .add(new ItemInfo("appid1", "env1", "cluster1", "namespace1", "query-key", "query-value")); allEnvMockItemInfos .add(new ItemInfo("appid2", "env2", "cluster2", "namespace2", "query-key", "query-value")); when(globalSearchService.getAllEnvItemInfoBySearch(eq("query-key"), eq("query-value"), eq(0), eq(perEnvSearchMaxResults))).thenReturn(SearchResponseEntity.ok(allEnvMockItemInfos)); mockMvc .perform(MockMvcRequestBuilders.get("/global-search/item-info/by-key-or-value") .contentType(MediaType.APPLICATION_JSON).param("key", "query-key") .param("value", "query-value")) .andExpect(status().isOk()) .andExpect(content().json("{\"body\":[" + " { \"appId\": \"appid1\",\n" + " \"envName\": \"env1\",\n" + " \"clusterName\": \"cluster1\",\n" + " \"namespaceName\": \"namespace1\",\n" + " \"key\": \"query-key\",\n" + " \"value\": \"query-value\"}," + " { \"appId\": \"appid2\",\n" + " \"envName\": \"env2\",\n" + " \"clusterName\": \"cluster2\",\n" + " \"namespaceName\": \"namespace2\",\n" + " \"key\": \"query-key\",\n" + " \"value\": \"query-value\"}],\"hasMoreData\":false,\"message\":\"OK\",\"code\":200}")); verify(globalSearchService, times(1)).getAllEnvItemInfoBySearch(eq("query-key"), eq("query-value"), eq(0), eq(perEnvSearchMaxResults)); } } ================================================ FILE: apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/controller/InstanceControllerTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.controller; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertSame; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import com.ctrip.framework.apollo.common.dto.InstanceDTO; import com.ctrip.framework.apollo.common.dto.PageDTO; import com.ctrip.framework.apollo.common.exception.BadRequestException; import com.ctrip.framework.apollo.portal.entity.vo.Number; import com.ctrip.framework.apollo.portal.environment.Env; import com.ctrip.framework.apollo.portal.service.InstanceService; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Set; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import org.springframework.data.domain.PageRequest; import org.springframework.http.ResponseEntity; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.test.web.servlet.setup.MockMvcBuilders; @RunWith(MockitoJUnitRunner.class) public class InstanceControllerTest { @Mock private InstanceService instanceService; @InjectMocks private InstanceController instanceController; @Captor private ArgumentCaptor> releaseIdsCaptor; private MockMvc mockMvc; @Before public void setUp() { mockMvc = MockMvcBuilders.standaloneSetup(instanceController).build(); } @Test public void shouldUseDefaultPageAndSizeForGetByRelease() throws Exception { PageDTO page = new PageDTO<>(Collections.emptyList(), PageRequest.of(0, 20), 0); when(instanceService.getByRelease(Env.DEV, 11L, 0, 20)).thenReturn(page); mockMvc.perform(MockMvcRequestBuilders.get("/envs/{env}/instances/by-release", "DEV") .param("releaseId", "11")).andExpect( org.springframework.test.web.servlet.result.MockMvcResultMatchers.status().isOk()); verify(instanceService).getByRelease(Env.DEV, 11L, 0, 20); } @Test public void shouldParseReleaseIdsAndDeduplicateWhenQueryingInstances() { List expected = Arrays.asList(new InstanceDTO(), new InstanceDTO()); when(instanceService.getByReleasesNotIn(eq(Env.DEV), eq("SampleApp"), eq("default"), eq("application"), releaseIdsCaptor.capture())).thenReturn(expected); List result = instanceController.getByReleasesNotIn("DEV", "SampleApp", "default", "application", "1,2,2,3"); assertSame(expected, result); Set releaseIds = releaseIdsCaptor.getValue(); assertEquals(3, releaseIds.size()); org.junit.Assert.assertTrue(releaseIds.contains(1L)); org.junit.Assert.assertTrue(releaseIds.contains(2L)); org.junit.Assert.assertTrue(releaseIds.contains(3L)); } @Test(expected = BadRequestException.class) public void shouldRejectEmptyReleaseIds() { instanceController.getByReleasesNotIn("DEV", "SampleApp", "default", "application", ""); } @Test public void shouldReturnInstanceCountByNamespace() { when(instanceService.getInstanceCountByNamespace("SampleApp", Env.DEV, "default", "application")) .thenReturn(8); ResponseEntity response = instanceController.getInstanceCountByNamespace("DEV", "SampleApp", "default", "application"); assertEquals(200, response.getStatusCodeValue()); assertEquals(8, response.getBody().getNum()); } } ================================================ FILE: apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/controller/ItemControllerAuthIntegrationTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.controller; import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.Mockito.any; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import com.ctrip.framework.apollo.ControllableAuthorizationConfiguration; import com.ctrip.framework.apollo.common.dto.ItemDTO; import com.ctrip.framework.apollo.portal.PortalApplication; import com.ctrip.framework.apollo.portal.entity.bo.UserInfo; import com.ctrip.framework.apollo.portal.environment.Env; import com.ctrip.framework.apollo.portal.service.ItemService; import com.ctrip.framework.apollo.portal.service.RolePermissionService; import com.ctrip.framework.apollo.portal.spi.UserInfoHolder; import com.google.gson.Gson; import jakarta.annotation.PostConstruct; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.boot.test.web.client.TestRestTemplate; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.test.context.jdbc.Sql; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.web.client.DefaultResponseErrorHandler; import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.RestTemplate; @RunWith(SpringJUnit4ClassRunner.class) @SpringBootTest(classes = {PortalApplication.class, ControllableAuthorizationConfiguration.class}, webEnvironment = WebEnvironment.RANDOM_PORT) public class ItemControllerAuthIntegrationTest { private final Gson GSON = new Gson(); private final String appId = "testApp"; private final String env = "LOCAL"; private final String clusterName = "default"; private final String namespaceName = "application"; private final ItemDTO itemDTO = new ItemDTO("testKey", "testValue", "testComment", 1); protected RestTemplate restTemplate = (new TestRestTemplate()).getRestTemplate(); @Value("${local.server.port}") int port; @Autowired private UserInfoHolder userInfoHolder; @Autowired private ItemService itemService; @Autowired private RolePermissionService rolePermissionService; @PostConstruct private void postConstruct() { System.setProperty("spring.profiles.active", "test"); restTemplate.setErrorHandler(new DefaultResponseErrorHandler()); } protected String url(String path) { return "http://localhost:" + port + path; } /** * Test cluster permission denied. */ @Test public void testCreateItemPermissionDenied() { setUserId("xxx"); try { HttpHeaders headers = new HttpHeaders(); headers.set("Content-Type", "application/json"); HttpEntity entity = new HttpEntity<>(GSON.toJson(itemDTO), headers); restTemplate.postForEntity( url("/apps/{appId}/envs/{env}/clusters/{clusterName}/namespaces/{namespaceName}/item"), entity, String.class, appId, env, clusterName, namespaceName); fail("should throw"); } catch (final HttpClientErrorException e) { assertEquals(HttpStatus.FORBIDDEN, e.getStatusCode()); } } @Test @Sql(scripts = "/sql/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testCreateItemPermissionAccessed() { setUserId("luke"); HttpHeaders headers = new HttpHeaders(); headers.set("Content-Type", "application/json"); HttpEntity entity = new HttpEntity<>(GSON.toJson(itemDTO), headers); when(rolePermissionService.hasAnyPermission(eq("luke"), anyList())).thenReturn(true); restTemplate.postForEntity( url("/apps/{appId}/envs/{env}/clusters/{clusterName}/namespaces/{namespaceName}/item"), entity, String.class, appId, env, clusterName, namespaceName); // Verify that the createItem method was called with the correct parameters verify(itemService).createItem(eq(appId), eq(Env.valueOf(env)), eq(clusterName), eq(namespaceName), any(ItemDTO.class)); } void setUserId(String userId) { UserInfo userInfo = new UserInfo(); userInfo.setUserId(userId); when(userInfoHolder.getUser()).thenReturn(userInfo); } } ================================================ FILE: apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/controller/ItemControllerTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.controller; import com.ctrip.framework.apollo.common.exception.BadRequestException; import com.ctrip.framework.apollo.core.enums.ConfigFileFormat; import com.ctrip.framework.apollo.portal.component.UnifiedPermissionValidator; import com.ctrip.framework.apollo.portal.entity.model.NamespaceTextModel; import com.ctrip.framework.apollo.portal.service.ItemService; import com.ctrip.framework.apollo.portal.service.NamespaceService; import com.ctrip.framework.apollo.portal.spi.UserInfoHolder; import com.google.common.base.Charsets; import com.google.common.io.Files; import java.io.File; import java.io.IOException; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; @RunWith(MockitoJUnitRunner.class) public class ItemControllerTest { @Mock private ItemService configService; @Mock private NamespaceService namespaceService; @Mock private UserInfoHolder userInfoHolder; @Mock private UnifiedPermissionValidator unifiedPermissionValidator; @InjectMocks private ItemController itemController; @Before public void setUp() throws Exception { itemController = new ItemController(configService, userInfoHolder, namespaceService, unifiedPermissionValidator); } @Test public void yamlSyntaxCheckOK() throws Exception { String yaml = loadYaml("case1.yaml"); itemController.doSyntaxCheck(assemble(ConfigFileFormat.YAML.getValue(), yaml)); } @Test(expected = BadRequestException.class) public void yamlSyntaxCheckWithDuplicatedValue() throws Exception { String yaml = loadYaml("case2.yaml"); itemController.doSyntaxCheck(assemble(ConfigFileFormat.YAML.getValue(), yaml)); } @Test(expected = BadRequestException.class) public void yamlSyntaxCheckWithUnsupportedType() throws Exception { String yaml = loadYaml("case3.yaml"); itemController.doSyntaxCheck(assemble(ConfigFileFormat.YAML.getValue(), yaml)); } private NamespaceTextModel assemble(String format, String content) { NamespaceTextModel model = new NamespaceTextModel(); model.setFormat(format); model.setConfigText(content); return model; } private String loadYaml(String caseName) throws IOException { File file = new File("src/test/resources/yaml/" + caseName); return Files.toString(file, Charsets.UTF_8); } } ================================================ FILE: apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/controller/NamespaceLockControllerTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.controller; import static org.junit.Assert.assertSame; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import com.ctrip.framework.apollo.common.dto.NamespaceLockDTO; import com.ctrip.framework.apollo.portal.entity.vo.LockInfo; import com.ctrip.framework.apollo.portal.environment.Env; import com.ctrip.framework.apollo.portal.service.NamespaceLockService; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; @RunWith(MockitoJUnitRunner.class) public class NamespaceLockControllerTest { @Mock private NamespaceLockService namespaceLockService; @InjectMocks private NamespaceLockController namespaceLockController; @Test public void shouldGetNamespaceLock() { NamespaceLockDTO expected = new NamespaceLockDTO(); when(namespaceLockService.getNamespaceLock("SampleApp", Env.DEV, "default", "application")) .thenReturn(expected); NamespaceLockDTO result = namespaceLockController.getNamespaceLock("SampleApp", "DEV", "default", "application"); assertSame(expected, result); verify(namespaceLockService).getNamespaceLock("SampleApp", Env.DEV, "default", "application"); } @Test public void shouldGetNamespaceLockInfo() { LockInfo expected = new LockInfo(); expected.setLockOwner("apollo"); when(namespaceLockService.getNamespaceLockInfo("SampleApp", Env.DEV, "default", "application")) .thenReturn(expected); LockInfo result = namespaceLockController.getNamespaceLockInfo("SampleApp", "DEV", "default", "application"); assertSame(expected, result); verify(namespaceLockService).getNamespaceLockInfo("SampleApp", Env.DEV, "default", "application"); } } ================================================ FILE: apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/controller/OrganizationControllerTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.controller; import static org.junit.Assert.assertSame; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import com.ctrip.framework.apollo.portal.component.config.PortalConfig; import com.ctrip.framework.apollo.portal.entity.vo.Organization; import java.util.Collections; import java.util.List; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; @RunWith(MockitoJUnitRunner.class) public class OrganizationControllerTest { @Mock private PortalConfig portalConfig; @InjectMocks private OrganizationController organizationController; @Test public void shouldLoadOrganizationsFromPortalConfig() { Organization org = new Organization(); org.setOrgId("TEST1"); org.setOrgName("Test Org"); List expected = Collections.singletonList(org); when(portalConfig.organizations()).thenReturn(expected); List result = organizationController.loadOrganization(); assertSame(expected, result); verify(portalConfig).organizations(); } } ================================================ FILE: apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/controller/PageSettingControllerTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.controller; import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.when; import com.ctrip.framework.apollo.portal.component.config.PortalConfig; import com.ctrip.framework.apollo.portal.entity.vo.PageSetting; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; @RunWith(MockitoJUnitRunner.class) public class PageSettingControllerTest { @Mock private PortalConfig portalConfig; @InjectMocks private PageSettingController pageSettingController; @Test public void shouldBuildPageSettingFromPortalConfig() { when(portalConfig.wikiAddress()).thenReturn("https://wiki.example.com"); when(portalConfig.canAppAdminCreatePrivateNamespace()).thenReturn(true); PageSetting result = pageSettingController.getPageSetting(); assertEquals("https://wiki.example.com", result.getWikiAddress()); assertEquals(true, result.isCanAppAdminCreatePrivateNamespace()); } } ================================================ FILE: apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/controller/PermissionControllerTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.controller; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import com.ctrip.framework.apollo.portal.AbstractIntegrationTest; import com.ctrip.framework.apollo.portal.entity.vo.ClusterNamespaceRolesAssignedUsers; import com.ctrip.framework.apollo.portal.entity.vo.PermissionCondition; import com.ctrip.framework.apollo.portal.service.RoleInitializationService; import com.ctrip.framework.apollo.portal.service.RolePermissionService; import com.ctrip.framework.apollo.portal.constant.PermissionType; import com.ctrip.framework.apollo.portal.constant.RoleType; import com.ctrip.framework.apollo.portal.util.RoleUtils; import com.google.common.collect.Sets; import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseEntity; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.jdbc.Sql; import java.util.Collections; @ActiveProfiles("skipAuthorization") public class PermissionControllerTest extends AbstractIntegrationTest { private final String appId = "testApp"; private final String env = "LOCAL"; private final String clusterName = "testCluster"; private final String namespaceName = "testNamespace"; private final String roleType = "ModifyNamespacesInCluster"; private final String user = "apollo"; @Autowired RoleInitializationService roleInitializationService; @Autowired RolePermissionService rolePermissionService; @Before public void setUp() { roleInitializationService.initClusterNamespaceRoles(appId, env, clusterName, "apollo"); Authentication auth = new UsernamePasswordAuthenticationToken("test-user", null, Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER"))); SecurityContextHolder.getContext().setAuthentication(auth); } @Test @Sql(scripts = "/sql/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testClusterNamespaceRoleLifeCycle() { HttpHeaders headers = new HttpHeaders(); headers.set("Content-Type", "application/json"); HttpEntity entity = new HttpEntity<>(user, headers); // check role not assigned ResponseEntity beforeAssign = restTemplate.getForEntity( url("/apps/{appId}/envs/{env}/clusters/{clusterName}/ns_role_users"), ClusterNamespaceRolesAssignedUsers.class, appId, env, clusterName); assertEquals(200, beforeAssign.getStatusCodeValue()); ClusterNamespaceRolesAssignedUsers body = beforeAssign.getBody(); assertNotNull(body); assertEquals(appId, body.getAppId()); assertEquals(env, body.getEnv()); assertEquals(clusterName, body.getCluster()); assertTrue(body.getModifyRoleUsers() == null || body.getModifyRoleUsers().isEmpty()); // assign role to user restTemplate.postForEntity( url("/apps/{appId}/envs/{env}/clusters/{clusterName}/ns_roles/{roleType}"), entity, Void.class, appId, env, clusterName, roleType); // check role assigned ResponseEntity afterAssign = restTemplate.getForEntity( url("/apps/{appId}/envs/{env}/clusters/{clusterName}/ns_role_users"), ClusterNamespaceRolesAssignedUsers.class, appId, env, clusterName); assertEquals(200, afterAssign.getStatusCodeValue()); body = afterAssign.getBody(); assertNotNull(body); assertEquals(appId, body.getAppId()); assertEquals(env, body.getEnv()); assertEquals(clusterName, body.getCluster()); assertTrue( body.getModifyRoleUsers().stream().anyMatch(userInfo -> userInfo.getUserId().equals(user))); // remove role from user restTemplate.delete( url("/apps/{appId}/envs/{env}/clusters/{clusterName}/ns_roles/{roleType}?user={user}"), appId, env, clusterName, roleType, user); // check role removed ResponseEntity afterRemove = restTemplate.getForEntity( url("/apps/{appId}/envs/{env}/clusters/{clusterName}/ns_role_users"), ClusterNamespaceRolesAssignedUsers.class, appId, env, clusterName); assertEquals(200, afterRemove.getStatusCodeValue()); body = afterRemove.getBody(); assertNotNull(body); assertEquals(appId, body.getAppId()); assertEquals(env, body.getEnv()); assertEquals(clusterName, body.getCluster()); assertTrue(body.getModifyRoleUsers() == null || body.getModifyRoleUsers().isEmpty()); } /** * Verify that env name aliases (e.g. "prod", "PROD") are normalized to the canonical form "PRO" * so that role lookup and permission check remain consistent. * * @see #5442 */ @Test @Sql(scripts = "/sql/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testEnvNameNormalizationForClusterRoles() { // Roles were initialized with env = "LOCAL" in setUp(). // Querying with lowercase "local" should still resolve to the same roles. ResponseEntity response = restTemplate.getForEntity( url("/apps/{appId}/envs/{env}/clusters/{clusterName}/ns_role_users"), ClusterNamespaceRolesAssignedUsers.class, appId, "local", clusterName); assertEquals(200, response.getStatusCodeValue()); ClusterNamespaceRolesAssignedUsers body = response.getBody(); assertNotNull(body); // The returned env should be the normalized form "LOCAL", not the raw input "local" assertEquals("LOCAL", body.getEnv()); } /** * Verify that "prod" is normalized to "PRO" (the canonical env name in Apollo) * when assigning and querying cluster namespace roles. * * @see #5442 */ @Test @Sql(scripts = "/sql/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testProdEnvNormalizationForClusterRoles() { // Initialize roles with canonical env name "PRO" roleInitializationService.initClusterNamespaceRoles(appId, "PRO", clusterName, "apollo"); HttpHeaders headers = new HttpHeaders(); headers.set("Content-Type", "application/json"); HttpEntity entity = new HttpEntity<>(user, headers); // Assign role using "prod" (alias for "PRO") restTemplate.postForEntity( url("/apps/{appId}/envs/{env}/clusters/{clusterName}/ns_roles/{roleType}"), entity, Void.class, appId, "prod", clusterName, roleType); // Query using "PROD" (another alias) — should still find the assigned role ResponseEntity response = restTemplate.getForEntity( url("/apps/{appId}/envs/{env}/clusters/{clusterName}/ns_role_users"), ClusterNamespaceRolesAssignedUsers.class, appId, "PROD", clusterName); assertEquals(200, response.getStatusCodeValue()); ClusterNamespaceRolesAssignedUsers body = response.getBody(); assertNotNull(body); assertEquals("PRO", body.getEnv()); assertTrue( body.getModifyRoleUsers().stream().anyMatch(userInfo -> userInfo.getUserId().equals(user))); } @Test @Sql(scripts = "/sql/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testEnvNormalizationForNamespacePermissionCheck() { roleInitializationService.initNamespaceSpecificEnvRoles(appId, namespaceName, "PRO", "apollo"); rolePermissionService.assignRoleToUsers( RoleUtils.buildNamespaceRoleName(appId, namespaceName, RoleType.MODIFY_NAMESPACE, "PRO"), Sets.newHashSet(user), "apollo"); String permissionType = PermissionType.MODIFY_NAMESPACE; ResponseEntity prodResponse = restTemplate.getForEntity( url("/apps/{appId}/envs/{env}/namespaces/{namespaceName}/permissions/{permissionType}"), PermissionCondition.class, appId, "prod", namespaceName, permissionType); assertEquals(200, prodResponse.getStatusCodeValue()); PermissionCondition prodBody = prodResponse.getBody(); assertNotNull(prodBody); assertTrue(prodBody.hasPermission()); ResponseEntity prodUpperResponse = restTemplate.getForEntity( url("/apps/{appId}/envs/{env}/namespaces/{namespaceName}/permissions/{permissionType}"), PermissionCondition.class, appId, "PROD", namespaceName, permissionType); assertEquals(200, prodUpperResponse.getStatusCodeValue()); PermissionCondition prodUpperBody = prodUpperResponse.getBody(); assertNotNull(prodUpperBody); assertTrue(prodUpperBody.hasPermission()); ResponseEntity proResponse = restTemplate.getForEntity( url("/apps/{appId}/envs/{env}/namespaces/{namespaceName}/permissions/{permissionType}"), PermissionCondition.class, appId, "PRO", namespaceName, permissionType); assertEquals(200, proResponse.getStatusCodeValue()); PermissionCondition proBody = proResponse.getBody(); assertNotNull(proBody); assertTrue(proBody.hasPermission()); } @Test @Sql(scripts = "/sql/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testEnvNormalizationForClusterNamespacePermissionCheck() { roleInitializationService.initClusterNamespaceRoles(appId, "LOCAL", clusterName, "apollo"); rolePermissionService.assignRoleToUsers(RoleUtils.buildClusterRoleName(appId, "LOCAL", clusterName, RoleType.RELEASE_NAMESPACES_IN_CLUSTER), Sets.newHashSet(user), "apollo"); String permissionType = PermissionType.RELEASE_NAMESPACES_IN_CLUSTER; ResponseEntity localResponse = restTemplate.getForEntity( url("/apps/{appId}/envs/{env}/clusters/{clusterName}/ns_permissions/{permissionType}"), PermissionCondition.class, appId, "local", clusterName, permissionType); assertEquals(200, localResponse.getStatusCodeValue()); PermissionCondition localBody = localResponse.getBody(); assertNotNull(localBody); assertTrue(localBody.hasPermission()); ResponseEntity localUpperResponse = restTemplate.getForEntity( url("/apps/{appId}/envs/{env}/clusters/{clusterName}/ns_permissions/{permissionType}"), PermissionCondition.class, appId, "LOCAL", clusterName, permissionType); assertEquals(200, localUpperResponse.getStatusCodeValue()); PermissionCondition localUpperBody = localUpperResponse.getBody(); assertNotNull(localUpperBody); assertTrue(localUpperBody.hasPermission()); } } ================================================ FILE: apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/controller/PrefixPathControllerTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.controller; import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.when; import jakarta.servlet.ServletContext; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import org.springframework.test.util.ReflectionTestUtils; @RunWith(MockitoJUnitRunner.class) public class PrefixPathControllerTest { @Mock private ServletContext servletContext; @InjectMocks private PrefixPathController prefixPathController; @Test public void shouldReturnConfiguredPrefixPath() { ReflectionTestUtils.setField(prefixPathController, "prefixPath", "/apollo"); String result = prefixPathController.getPrefixPath(); assertEquals("/apollo", result); } @Test public void shouldFallbackToServletContextPathWhenPrefixPathIsEmpty() { when(servletContext.getContextPath()).thenReturn("/portal"); ReflectionTestUtils.setField(prefixPathController, "prefixPath", ""); String result = prefixPathController.getPrefixPath(); assertEquals("/portal", result); } } ================================================ FILE: apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/controller/ReleaseHistoryControllerTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.controller; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import com.ctrip.framework.apollo.portal.component.UnifiedPermissionValidator; import com.ctrip.framework.apollo.portal.entity.bo.ReleaseHistoryBO; import com.ctrip.framework.apollo.portal.environment.Env; import com.ctrip.framework.apollo.portal.service.ReleaseHistoryService; import java.util.Collections; import java.util.List; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; @RunWith(MockitoJUnitRunner.class) public class ReleaseHistoryControllerTest { @Mock private ReleaseHistoryService releaseHistoryService; @Mock private UnifiedPermissionValidator unifiedPermissionValidator; @InjectMocks private ReleaseHistoryController releaseHistoryController; @Test public void shouldReturnEmptyListWhenConfigShouldBeHidden() { when(unifiedPermissionValidator.shouldHideConfigToCurrentUser("SampleApp", "DEV", "default", "application")).thenReturn(true); List result = releaseHistoryController.findReleaseHistoriesByNamespace( "SampleApp", "DEV", "default", "application", 0, 10); assertTrue(result.isEmpty()); verify(releaseHistoryService, never()) .findNamespaceReleaseHistory("SampleApp", Env.DEV, "default", "application", 0, 10); } @Test public void shouldDelegateToServiceWhenConfigIsVisible() { ReleaseHistoryBO releaseHistoryBO = new ReleaseHistoryBO(); List expected = Collections.singletonList(releaseHistoryBO); when(unifiedPermissionValidator.shouldHideConfigToCurrentUser("SampleApp", "DEV", "default", "application")).thenReturn(false); when(releaseHistoryService.findNamespaceReleaseHistory("SampleApp", Env.DEV, "default", "application", 1, 20)).thenReturn(expected); List result = releaseHistoryController .findReleaseHistoriesByNamespace("SampleApp", "DEV", "default", "application", 1, 20); assertSame(expected, result); verify(releaseHistoryService).findNamespaceReleaseHistory("SampleApp", Env.DEV, "default", "application", 1, 20); } } ================================================ FILE: apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/controller/SearchControllerTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.controller; import com.google.common.collect.Lists; import com.ctrip.framework.apollo.common.dto.NamespaceDTO; import com.ctrip.framework.apollo.common.dto.PageDTO; import com.ctrip.framework.apollo.common.entity.App; import com.ctrip.framework.apollo.portal.component.PortalSettings; import com.ctrip.framework.apollo.portal.component.config.PortalConfig; import com.ctrip.framework.apollo.portal.environment.Env; import com.ctrip.framework.apollo.portal.service.AppService; import com.ctrip.framework.apollo.portal.service.NamespaceService; import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import java.util.List; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; /** * @author lepdou 2021-09-13 */ @RunWith(MockitoJUnitRunner.class) public class SearchControllerTest { @Mock private AppService appService; @Mock private NamespaceService namespaceService; @Mock private PortalSettings portalSettings; @Mock private PortalConfig portalConfig; @InjectMocks private SearchController searchController; @Test public void testSearchByEmptyKey() { PageRequest request = PageRequest.of(0, 20); searchController.search("", request); verify(appService, times(1)).findAll(request); } @Test public void testSearchApp() { String query = "timeout"; PageRequest request = PageRequest.of(0, 20); PageDTO apps = genPageApp(10, request, 100); when(appService.searchByAppIdOrAppName(query, request)).thenReturn(apps); searchController.search(query, request); verify(appService, times(0)).findAll(request); verify(appService, times(1)).searchByAppIdOrAppName(query, request); } @Test public void testSearchItemSwitch() { String query = "timeout"; PageRequest request = PageRequest.of(0, 20); PageDTO apps = new PageDTO<>(Lists.newLinkedList(), request, 0); when(appService.searchByAppIdOrAppName(query, request)).thenReturn(apps); when(portalConfig.supportSearchByItem()).thenReturn(false); PageDTO result = searchController.search(query, request); Assert.assertFalse(result.hasContent()); verify(appService, times(0)).findAll(request); verify(appService, times(1)).searchByAppIdOrAppName(query, request); } @Test public void testSearchItem() { String query = "timeout"; PageRequest request = PageRequest.of(0, 20); PageDTO apps = new PageDTO<>(Lists.newLinkedList(), request, 0); PageDTO devNamespaces = genPageNamespace(10, request, 20); PageDTO fatNamespaces = genPageNamespace(15, request, 30); when(appService.searchByAppIdOrAppName(query, request)).thenReturn(apps); when(portalConfig.supportSearchByItem()).thenReturn(true); when(portalSettings.getActiveEnvs()).thenReturn(Lists.newArrayList(Env.DEV, Env.FAT)); when(namespaceService.findNamespacesByItem(Env.DEV, query, request)).thenReturn(devNamespaces); when(namespaceService.findNamespacesByItem(Env.FAT, query, request)).thenReturn(fatNamespaces); PageDTO result = searchController.search(query, request); Assert.assertTrue(result.hasContent()); Assert.assertEquals(25, result.getContent().size()); Assert.assertEquals(30, result.getTotal()); verify(appService, times(0)).findAll(request); verify(appService, times(1)).searchByAppIdOrAppName(query, request); verify(namespaceService).findNamespacesByItem(Env.DEV, query, request); verify(namespaceService).findNamespacesByItem(Env.FAT, query, request); } private PageDTO genPageApp(int size, Pageable pageable, int total) { List result = Lists.newLinkedList(); for (int i = 0; i < size; i++) { App app = new App(); result.add(app); } return new PageDTO<>(result, pageable, total); } private PageDTO genPageNamespace(int size, Pageable pageable, int total) { List result = Lists.newLinkedList(); for (int i = 0; i < size; i++) { NamespaceDTO namespaceDTO = new NamespaceDTO(); result.add(namespaceDTO); } return new PageDTO<>(result, pageable, total); } } ================================================ FILE: apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/controller/ServerConfigControllerTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.controller; import com.ctrip.framework.apollo.portal.AbstractIntegrationTest; import com.ctrip.framework.apollo.portal.entity.po.ServerConfig; import com.ctrip.framework.apollo.portal.environment.Env; import com.ctrip.framework.apollo.portal.service.ServerConfigService; import org.junit.Assert; import org.junit.Test; import org.mockito.InjectMocks; import org.mockito.Mock; import org.springframework.http.ResponseEntity; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.jdbc.Sql; import org.springframework.web.client.HttpClientErrorException; import java.util.*; import static org.hamcrest.core.StringContains.containsString; import static org.junit.Assert.assertEquals; import static org.hamcrest.MatcherAssert.assertThat; import static org.mockito.Mockito.when; /** * Created by kezhenxu at 2019/1/14 13:24. * * @author kezhenxu (kezhenxu at lizhi dot fm) */ @ActiveProfiles("skipAuthorization") public class ServerConfigControllerTest extends AbstractIntegrationTest { @Mock private ServerConfigService serverConfigService; @InjectMocks private ServerConfigController serverConfigController; @Test @Sql(scripts = "/sql/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void shouldSuccessWhenParameterValidForPortalDBConfig() { ServerConfig serverConfig = new ServerConfig(); serverConfig.setKey("validKey"); serverConfig.setValue("validValue"); ResponseEntity responseEntity = restTemplate .postForEntity(url("/server/portal-db/config"), serverConfig, ServerConfig.class); assertEquals(responseEntity.getBody().getKey(), serverConfig.getKey()); assertEquals(responseEntity.getBody().getValue(), serverConfig.getValue()); } @Test public void shouldFailWhenParameterInvalidForPortalDBConfig() { ServerConfig serverConfig = new ServerConfig(); serverConfig.setKey(" "); serverConfig.setValue("valid"); try { restTemplate.postForEntity(url("/server/portal-db/config"), serverConfig, ServerConfig.class); Assert.fail("Should throw"); } catch (final HttpClientErrorException e) { assertThat(new String(e.getResponseBodyAsByteArray()), containsString("ServerConfig.Key cannot be blank")); } serverConfig.setKey("valid"); serverConfig.setValue(" "); try { restTemplate.postForEntity(url("/server/portal-db/config"), serverConfig, ServerConfig.class); Assert.fail("Should throw"); } catch (final HttpClientErrorException e) { assertThat(new String(e.getResponseBodyAsByteArray()), containsString("ServerConfig.Value cannot be blank")); } } @Test public void testFindEmpty() { when(serverConfigService.findAllPortalDBConfig()).thenReturn(new ArrayList<>()); when(serverConfigService.findAllConfigDBConfig(Env.DEV)).thenReturn(new ArrayList<>()); List serverConfigList = serverConfigController.findAllPortalDBServerConfig(); Assert.assertNotNull(serverConfigList); Assert.assertEquals(0, serverConfigList.size()); serverConfigList = serverConfigController.findAllConfigDBServerConfig(Env.DEV.getName()); Assert.assertNotNull(serverConfigList); Assert.assertEquals(0, serverConfigList.size()); } } ================================================ FILE: apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/controller/SignInControllerTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.controller; import static org.junit.Assert.assertEquals; import org.junit.Test; public class SignInControllerTest { private final SignInController signInController = new SignInController(); @Test public void shouldAlwaysReturnLoginPage() { assertEquals("login.html", signInController.login(null, null)); assertEquals("login.html", signInController.login("error", null)); assertEquals("login.html", signInController.login(null, "logout")); } } ================================================ FILE: apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/controller/SsoHeartbeatControllerTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.controller; import static org.mockito.Mockito.verify; import com.ctrip.framework.apollo.portal.spi.SsoHeartbeatHandler; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; @RunWith(MockitoJUnitRunner.class) public class SsoHeartbeatControllerTest { @Mock private SsoHeartbeatHandler handler; @Mock private HttpServletRequest request; @Mock private HttpServletResponse response; @InjectMocks private SsoHeartbeatController ssoHeartbeatController; @Test public void shouldDelegateHeartbeatToHandler() { ssoHeartbeatController.heartbeat(request, response); verify(handler).doHeartbeat(request, response); } } ================================================ FILE: apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/controller/SystemInfoControllerTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.controller; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.when; import com.ctrip.framework.apollo.core.dto.ServiceDTO; import com.ctrip.framework.apollo.portal.component.PortalSettings; import com.ctrip.framework.apollo.portal.component.RestTemplateFactory; import com.ctrip.framework.apollo.portal.entity.vo.EnvironmentInfo; import com.ctrip.framework.apollo.portal.entity.vo.SystemInfo; import com.ctrip.framework.apollo.portal.environment.Env; import com.ctrip.framework.apollo.portal.environment.PortalMetaDomainService; import java.util.Collections; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import org.springframework.boot.actuate.health.Health; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.web.client.RestTemplate; @RunWith(MockitoJUnitRunner.class) public class SystemInfoControllerTest { @Mock private PortalSettings portalSettings; @Mock private RestTemplateFactory restTemplateFactory; @Mock private PortalMetaDomainService portalMetaDomainService; @Mock private RestTemplate restTemplate; @InjectMocks private SystemInfoController systemInfoController; @Before public void setUp() { when(restTemplateFactory.getObject()).thenReturn(restTemplate); ReflectionTestUtils.invokeMethod(systemInfoController, "init"); } @Test public void shouldBuildSystemInfoWithEnvironmentDetails() { when(portalSettings.getAllEnvs()).thenReturn(Collections.singletonList(Env.DEV)); when(portalSettings.isEnvActive(Env.DEV)).thenReturn(true); when(portalMetaDomainService.getMetaServerAddress(Env.DEV)).thenReturn("http://meta"); when(portalMetaDomainService.getDomain(Env.DEV)).thenReturn("http://meta"); ServiceDTO configService = service("config-1", "http://config-service"); ServiceDTO adminService = service("admin-1", "http://admin-service"); when(restTemplate.getForObject("http://meta/services/config", ServiceDTO[].class)) .thenReturn(new ServiceDTO[] {configService}); when(restTemplate.getForObject("http://meta/services/admin", ServiceDTO[].class)) .thenReturn(new ServiceDTO[] {adminService}); SystemInfo systemInfo = systemInfoController.getSystemInfo(); assertEquals(1, systemInfo.getEnvironments().size()); EnvironmentInfo envInfo = systemInfo.getEnvironments().get(0); assertEquals(Env.DEV, envInfo.getEnv()); assertTrue(envInfo.isActive()); assertEquals("http://meta", envInfo.getMetaServerAddress()); assertEquals(1, envInfo.getConfigServices().length); assertEquals(1, envInfo.getAdminServices().length); } @Test public void shouldRecordErrorMessageWhenLoadingServicesFails() { when(portalSettings.getAllEnvs()).thenReturn(Collections.singletonList(Env.DEV)); when(portalSettings.isEnvActive(Env.DEV)).thenReturn(false); when(portalMetaDomainService.getMetaServerAddress(Env.DEV)).thenReturn("http://meta"); when(portalMetaDomainService.getDomain(Env.DEV)).thenReturn("http://meta"); when(restTemplate.getForObject("http://meta/services/config", ServiceDTO[].class)) .thenThrow(new RuntimeException("boom")); SystemInfo systemInfo = systemInfoController.getSystemInfo(); EnvironmentInfo envInfo = systemInfo.getEnvironments().get(0); assertNotNull(envInfo.getErrorMessage()); assertTrue(envInfo.getErrorMessage().contains("failed")); assertTrue(envInfo.getErrorMessage().contains("boom")); } @Test public void shouldCheckHealthForMatchedInstance() { when(portalSettings.getAllEnvs()).thenReturn(Collections.singletonList(Env.DEV)); when(portalSettings.isEnvActive(Env.DEV)).thenReturn(true); when(portalMetaDomainService.getMetaServerAddress(Env.DEV)).thenReturn("http://meta"); when(portalMetaDomainService.getDomain(Env.DEV)).thenReturn("http://meta"); ServiceDTO configService = service("config-1", "http://config-service"); when(restTemplate.getForObject("http://meta/services/config", ServiceDTO[].class)) .thenReturn(new ServiceDTO[] {configService}); when(restTemplate.getForObject("http://meta/services/admin", ServiceDTO[].class)) .thenReturn(new ServiceDTO[0]); Health expected = Health.up().withDetail("status", "UP").build(); when(restTemplate.getForObject("http://config-service/health", Health.class)).thenReturn(expected); Health result = systemInfoController.checkHealth("config-1"); assertSame(expected, result); } @Test(expected = IllegalArgumentException.class) public void shouldThrowWhenInstanceIdDoesNotExist() { when(portalSettings.getAllEnvs()).thenReturn(Collections.singletonList(Env.DEV)); when(portalSettings.isEnvActive(Env.DEV)).thenReturn(true); when(portalMetaDomainService.getMetaServerAddress(Env.DEV)).thenReturn("http://meta"); when(portalMetaDomainService.getDomain(Env.DEV)).thenReturn("http://meta"); when(restTemplate.getForObject("http://meta/services/config", ServiceDTO[].class)) .thenReturn(new ServiceDTO[0]); when(restTemplate.getForObject("http://meta/services/admin", ServiceDTO[].class)) .thenReturn(new ServiceDTO[0]); systemInfoController.checkHealth("missing-instance"); } private ServiceDTO service(String instanceId, String homepageUrl) { ServiceDTO dto = new ServiceDTO(); dto.setInstanceId(instanceId); dto.setHomepageUrl(homepageUrl); return dto; } } ================================================ FILE: apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/controller/UserInfoControllerTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.controller; import com.ctrip.framework.apollo.common.exception.BadRequestException; import com.ctrip.framework.apollo.portal.component.UnifiedPermissionValidator; import com.ctrip.framework.apollo.portal.entity.bo.UserInfo; import com.ctrip.framework.apollo.portal.entity.po.UserPO; import com.ctrip.framework.apollo.portal.spi.UserInfoHolder; import com.ctrip.framework.apollo.portal.spi.springsecurity.SpringSecurityUserService; import com.ctrip.framework.apollo.portal.util.checker.AuthUserPasswordChecker; import com.ctrip.framework.apollo.portal.util.checker.CheckResult; import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; @RunWith(MockitoJUnitRunner.class) public class UserInfoControllerTest { @InjectMocks private UserInfoController userInfoController; @Mock private SpringSecurityUserService userService; @Mock private AuthUserPasswordChecker userPasswordChecker; @Mock private UnifiedPermissionValidator unifiedPermissionValidator; @Mock private UserInfoHolder userInfoHolder; @Test public void testCreateOrUpdateUserForAdmin() { UserPO user = new UserPO(); user.setUsername("username"); user.setPassword("password"); user.setEnabled(1); Mockito.when(unifiedPermissionValidator.isSuperAdmin()).thenReturn(true); Mockito.when(userPasswordChecker.checkWeakPassword(Mockito.anyString())) .thenReturn(new CheckResult(Boolean.TRUE, "")); userInfoController.createOrUpdateUser(true, user); } @Test public void testDisableUserForAdmin() { UserPO user = new UserPO(); user.setUsername("username"); user.setPassword("password"); user.setEnabled(0); Mockito.when(unifiedPermissionValidator.isSuperAdmin()).thenReturn(true); Mockito.when(userPasswordChecker.checkWeakPassword(Mockito.anyString())) .thenReturn(new CheckResult(Boolean.TRUE, "")); userInfoController.createOrUpdateUser(true, user); } @Test public void testUpdateUserForNoAdmin() { UserPO user = new UserPO(); user.setUsername("username"); user.setUserDisplayName("displayName"); user.setPassword("password"); user.setEnabled(1); UserInfo currentUserInfo = new UserInfo(); currentUserInfo.setUserId("username"); currentUserInfo.setName("displayName"); Mockito.when(unifiedPermissionValidator.isSuperAdmin()).thenReturn(false); Mockito.when(userInfoHolder.getUser()).thenReturn(currentUserInfo); Mockito.when(userPasswordChecker.checkWeakPassword(Mockito.anyString())) .thenReturn(new CheckResult(Boolean.TRUE, "")); userInfoController.createOrUpdateUser(true, user); } @Test(expected = UnsupportedOperationException.class) public void testUpdateOtherUserFailedForNoAdmin() { UserPO user = new UserPO(); user.setUsername("username"); user.setUserDisplayName("displayName"); user.setPassword("password"); UserInfo currentUserInfo = new UserInfo(); currentUserInfo.setUserId("username_other"); currentUserInfo.setName("displayName_other"); Mockito.when(unifiedPermissionValidator.isSuperAdmin()).thenReturn(false); Mockito.when(userInfoHolder.getUser()).thenReturn(currentUserInfo); userInfoController.createOrUpdateUser(true, user); } @Test(expected = UnsupportedOperationException.class) public void testDisableUserFailedForNoAdmin() { UserPO user = new UserPO(); user.setUsername("username"); user.setUserDisplayName("displayName"); user.setPassword("password"); user.setEnabled(0); UserInfo currentUserInfo = new UserInfo(); currentUserInfo.setUserId("username"); currentUserInfo.setName("displayName"); Mockito.when(unifiedPermissionValidator.isSuperAdmin()).thenReturn(false); Mockito.when(userInfoHolder.getUser()).thenReturn(currentUserInfo); userInfoController.createOrUpdateUser(true, user); } @Test(expected = UnsupportedOperationException.class) public void testDisableOtherUserFailedForNoAdmin() { UserPO user = new UserPO(); user.setUsername("username"); user.setUserDisplayName("displayName"); user.setPassword("password"); user.setEnabled(0); UserInfo currentUserInfo = new UserInfo(); currentUserInfo.setUserId("username_other"); currentUserInfo.setName("displayName_other"); Mockito.when(unifiedPermissionValidator.isSuperAdmin()).thenReturn(false); Mockito.when(userInfoHolder.getUser()).thenReturn(currentUserInfo); userInfoController.createOrUpdateUser(true, user); } @Test(expected = BadRequestException.class) public void testCreateOrUpdateUserFailed() { UserPO user = new UserPO(); user.setUsername("username"); user.setPassword("password"); String msg = "fake error message"; Mockito.when(unifiedPermissionValidator.isSuperAdmin()).thenReturn(true); Mockito.when(userPasswordChecker.checkWeakPassword(Mockito.anyString())) .thenReturn(new CheckResult(Boolean.FALSE, msg)); try { userInfoController.createOrUpdateUser(true, user); } catch (BadRequestException e) { Assert.assertEquals(msg, e.getMessage()); throw e; } } } ================================================ FILE: apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/environment/BaseIntegrationTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.environment; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Response; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.handler.ContextHandler; import org.eclipse.jetty.server.handler.ContextHandlerCollection; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.Callback; import org.junit.After; import java.io.IOException; import java.net.ServerSocket; import java.nio.charset.StandardCharsets; public abstract class BaseIntegrationTest { protected static final int PORT = findFreePort(); private Server server; /** * init and start a jetty server, remember to call server.stop when the task is finished */ protected Server startServerWithHandlers(ContextHandler... handlers) throws Exception { server = new Server(PORT); ContextHandlerCollection contexts = new ContextHandlerCollection(); contexts.setHandlers(handlers); server.setHandler(contexts); server.start(); return server; } @After public void tearDown() throws Exception { if (server != null && server.isStarted()) { server.stop(); } } ContextHandler mockServerHandler(final int statusCode, final String responseBody) { ContextHandler context = new ContextHandler("/"); context.setHandler(new Handler.Abstract() { @Override public boolean handle(Request request, Response response, Callback callback) throws Exception { response.getHeaders().put(HttpHeader.CONTENT_TYPE, "text/plain;charset=UTF-8"); response.setStatus(statusCode); response.write(true, BufferUtil.toBuffer(responseBody, StandardCharsets.UTF_8), callback); return true; } }); return context; } /** * Returns a free port number on localhost. * * Heavily inspired from org.eclipse.jdt.launching.SocketUtil (to avoid a dependency to JDT just because of this). * Slightly improved with close() missing in JDT. And throws exception instead of returning -1. * * @return a free port number on localhost * @throws IllegalStateException if unable to find a free port */ static int findFreePort() { ServerSocket socket = null; try { socket = new ServerSocket(0); socket.setReuseAddress(true); int port = socket.getLocalPort(); try { socket.close(); } catch (IOException e) { // Ignore IOException on close() } return port; } catch (IOException e) { } finally { if (socket != null) { try { socket.close(); } catch (IOException e) { } } } throw new IllegalStateException( "Could not find a free TCP/IP port to start embedded Jetty HTTP Server on"); } } ================================================ FILE: apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/environment/DatabasePortalMetaServerProviderTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.environment; import static org.junit.Assert.*; import com.ctrip.framework.apollo.portal.component.config.PortalConfig; import java.util.HashMap; import java.util.Map; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import org.mockito.junit.MockitoJUnitRunner; @RunWith(MockitoJUnitRunner.class) public class DatabasePortalMetaServerProviderTest { private DatabasePortalMetaServerProvider databasePortalMetaServerProvider; @Mock private PortalConfig portalConfig; private Map metaServiceMap; @Before public void setUp() { MockitoAnnotations.initMocks(this); metaServiceMap = new HashMap<>(); metaServiceMap.put("nothing", "http://unknown.com"); metaServiceMap.put("dev", "http://server.com:8080"); Mockito.when(portalConfig.getMetaServers()).thenReturn(metaServiceMap); // use mocked object to construct databasePortalMetaServerProvider = new DatabasePortalMetaServerProvider(portalConfig); } @Test public void testGetMetaServerAddress() { String address = databasePortalMetaServerProvider.getMetaServerAddress(Env.DEV); assertEquals("http://server.com:8080", address); String newMetaServerAddress = "http://another-server.com:8080"; metaServiceMap.put("dev", newMetaServerAddress); databasePortalMetaServerProvider.reload(); assertEquals(newMetaServerAddress, databasePortalMetaServerProvider.getMetaServerAddress(Env.DEV)); } @Test public void testExists() { assertTrue(databasePortalMetaServerProvider.exists(Env.DEV)); assertFalse(databasePortalMetaServerProvider.exists(Env.PRO)); assertTrue(databasePortalMetaServerProvider.exists(Env.addEnvironment("nothing"))); } } ================================================ FILE: apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/environment/DefaultPortalMetaServerProviderTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.environment; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import com.ctrip.framework.apollo.portal.AbstractUnitTest; import org.junit.After; import org.junit.Before; import org.junit.Test; public class DefaultPortalMetaServerProviderTest extends AbstractUnitTest { private DefaultPortalMetaServerProvider defaultPortalMetaServerProvider; @Before public void setUp() throws Exception { defaultPortalMetaServerProvider = new DefaultPortalMetaServerProvider(); } @After public void tearDown() throws Exception { System.clearProperty("dev_meta"); System.clearProperty("fat_meta"); } @Test public void testFromPropertyFile() { assertEquals("http://localhost:8080", defaultPortalMetaServerProvider.getMetaServerAddress(Env.LOCAL)); assertEquals("${dev_meta}", defaultPortalMetaServerProvider.getMetaServerAddress(Env.DEV)); assertEquals("${pro_meta}", defaultPortalMetaServerProvider.getMetaServerAddress(Env.PRO)); } /** * testing the environment dynamic added from system property */ @Test public void testDynamicEnvironmentFromSystemProperty() { String someDevMetaAddress = "someMetaAddress"; String someFatMetaAddress = "someFatMetaAddress"; System.setProperty("dev_meta", someDevMetaAddress); System.setProperty("fat_meta", someFatMetaAddress); // reload above added defaultPortalMetaServerProvider.reload(); assertEquals(someDevMetaAddress, defaultPortalMetaServerProvider.getMetaServerAddress(Env.DEV)); assertEquals(someFatMetaAddress, defaultPortalMetaServerProvider.getMetaServerAddress(Env.FAT)); String randomAddress = "randomAddress"; String randomEnvironment = "randomEnvironment"; System.setProperty(randomEnvironment + "_meta", randomAddress); assertFalse(defaultPortalMetaServerProvider.exists(Env.addEnvironment(randomEnvironment))); // reload above added defaultPortalMetaServerProvider.reload(); assertEquals(randomAddress, defaultPortalMetaServerProvider.getMetaServerAddress(Env.valueOf(randomEnvironment))); assertTrue(defaultPortalMetaServerProvider.exists(Env.addEnvironment(randomEnvironment))); // clear the property System.clearProperty(randomEnvironment + "_meta"); } } ================================================ FILE: apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/environment/EnvTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.environment; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertTrue; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import org.junit.Test; public class EnvTest { @Test public void exist() { assertFalse(Env.exists("xxxyyy234")); assertTrue(Env.exists("local")); assertTrue(Env.exists("dev")); } @Test public void addEnv() { String name = "someEEEE"; assertFalse(Env.exists(name)); Env.addEnvironment(name); assertTrue(Env.exists(name)); } @Test(expected = IllegalArgumentException.class) public void valueOf() { String name = "notexist"; assertFalse(Env.exists(name)); assertEquals(Env.valueOf(name), Env.UNKNOWN); assertEquals(Env.valueOf("dev"), Env.DEV); assertEquals(Env.valueOf("UAT"), Env.UAT); } @Test public void testEquals() { assertEquals(Env.DEV, Env.valueOf("dEv")); String name = "someEEEE"; Env.addEnvironment(name); assertNotEquals(Env.valueOf(name), Env.DEV); } @Test(expected = RuntimeException.class) public void testEqualsWithRuntimeException() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { // get private constructor Constructor envConstructor = Env.class.getDeclaredConstructor(String.class); // make private constructor accessible envConstructor.setAccessible(true); // make a fake Env Env fakeDevEnv = envConstructor.newInstance(Env.DEV.toString()); // compare, then a RuntimeException will invoke fakeDevEnv.equals(Env.DEV); } @Test public void testEqualWithoutException() { assertEquals(Env.DEV, Env.DEV); assertEquals(Env.DEV, Env.valueOf("dEV")); assertNotEquals(Env.PRO, Env.DEV); assertNotEquals(Env.DEV, Env.valueOf("uaT")); } @Test public void testToString() { assertEquals("DEV", Env.DEV.toString()); } @Test public void name() { assertEquals("DEV", Env.DEV.getName()); } @Test public void getName() { String name = "getName"; Env.addEnvironment(name); assertEquals(name.trim().toUpperCase(), Env.valueOf(name).toString()); } @Test public void testTransformEnvBlank() { assertEquals(Env.UNKNOWN, Env.transformEnv("")); assertEquals(Env.UNKNOWN, Env.transformEnv(null)); assertEquals(Env.UNKNOWN, Env.transformEnv(" ")); } @Test public void testTransformEnvSpecialCase() { // Prod/Pro assertEquals(Env.PRO, Env.transformEnv("prod")); assertEquals(Env.PRO, Env.transformEnv("PROD")); // FAT/FWS assertEquals(Env.FAT, Env.transformEnv("FWS")); assertEquals(Env.FAT, Env.transformEnv("fws")); } @Test public void testTransformEnvNotExist() { assertEquals(Env.UNKNOWN, Env.transformEnv("notexisting")); assertEquals(Env.LOCAL, Env.transformEnv("LOCAL")); } @Test public void testTransformEnvValid() { assertEquals(Env.UNKNOWN, Env.transformEnv("UNKNOWN")); assertEquals(Env.LOCAL, Env.transformEnv("LOCAL")); assertEquals(Env.FAT, Env.transformEnv("FAT")); assertEquals(Env.FAT, Env.transformEnv("FWS")); assertEquals(Env.PRO, Env.transformEnv("PRO")); assertEquals(Env.PRO, Env.transformEnv("PROD")); assertEquals(Env.DEV, Env.transformEnv("DEV")); assertEquals(Env.LPT, Env.transformEnv("LPT")); assertEquals(Env.TOOLS, Env.transformEnv("TOOLS")); assertEquals(Env.UAT, Env.transformEnv("UAT")); String testEnvName = "testEnv"; Env.addEnvironment(testEnvName); Env expected = Env.valueOf(testEnvName); assertEquals(expected, Env.transformEnv(testEnvName)); } @Test public void testTransformEnvWithTrailingAndLeadingBlankValid() { ArrayList specialChars = new ArrayList<>(); specialChars.add(" "); specialChars.add("\t"); specialChars.add(" \t"); specialChars.add(" \t "); specialChars.add("\t "); for (String specialChar : specialChars) { assertEquals(Env.UNKNOWN, Env.transformEnv(specialChar + "UNKNOWN")); assertEquals(Env.LOCAL, Env.transformEnv(specialChar + "LOCAL" + specialChar)); assertEquals(Env.FAT, Env.transformEnv(specialChar + "FAT" + specialChar)); assertEquals(Env.FAT, Env.transformEnv(specialChar + "FWS" + specialChar)); assertEquals(Env.PRO, Env.transformEnv(specialChar + "PRO" + specialChar)); assertEquals(Env.PRO, Env.transformEnv(specialChar + "PROD" + specialChar)); assertEquals(Env.DEV, Env.transformEnv(specialChar + "DEV" + specialChar)); assertEquals(Env.LPT, Env.transformEnv(specialChar + "LPT" + specialChar)); assertEquals(Env.TOOLS, Env.transformEnv(specialChar + "TOOLS" + specialChar)); assertEquals(Env.UAT, Env.transformEnv(specialChar + "UAT" + specialChar)); String testEnvName = "testEnv"; Env.addEnvironment(testEnvName); Env expected = Env.valueOf(testEnvName); assertEquals(expected, Env.transformEnv(specialChar + testEnvName + specialChar)); } } @Test(expected = RuntimeException.class) public void testAddEnvironmentBlankString() { Env.addEnvironment(""); } @Test(expected = RuntimeException.class) public void testAddEnvironmentNullString() { Env.addEnvironment(null); } @Test(expected = RuntimeException.class) public void testAddEnvironmentSpacesString() { Env.addEnvironment(" "); } @Test public void testExistsForBlankName() { assertFalse(Env.exists("")); assertFalse(Env.exists(" ")); assertFalse(Env.exists(null)); } } ================================================ FILE: apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/environment/PortalMetaDomainServiceTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.environment; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import com.ctrip.framework.apollo.portal.component.config.PortalConfig; import java.util.HashMap; import java.util.Map; import jakarta.servlet.http.HttpServletResponse; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; @RunWith(MockitoJUnitRunner.class) public class PortalMetaDomainServiceTest extends BaseIntegrationTest { private PortalMetaDomainService portalMetaDomainService; @Mock private PortalConfig portalConfig; @Before public void init() { final Map map = new HashMap<>(); map.put("nothing", "http://unknown.com"); Mockito.when(portalConfig.getMetaServers()).thenReturn(map); portalMetaDomainService = new PortalMetaDomainService(portalConfig); } @Test public void testGetMetaDomain() { // local String localMetaServerAddress = "http://localhost:8080"; mockMetaServerAddress(Env.LOCAL, localMetaServerAddress); assertEquals(localMetaServerAddress, portalMetaDomainService.getDomain(Env.LOCAL)); // add this environment without meta server address String randomEnvironment = "randomEnvironment"; Env.addEnvironment(randomEnvironment); assertEquals(PortalMetaDomainService.DEFAULT_META_URL, portalMetaDomainService.getDomain(Env.valueOf(randomEnvironment))); } @Test public void testGetValidAddress() throws Exception { String someResponse = "some response"; startServerWithHandlers(mockServerHandler(HttpServletResponse.SC_OK, someResponse)); String validServer = " http://localhost:" + PORT + " "; String invalidServer = "http://localhost:" + findFreePort(); mockMetaServerAddress(Env.FAT, validServer + "," + invalidServer); mockMetaServerAddress(Env.UAT, invalidServer + "," + validServer); portalMetaDomainService.reload(); assertEquals(validServer.trim(), portalMetaDomainService.getDomain(Env.FAT)); assertEquals(validServer.trim(), portalMetaDomainService.getDomain(Env.UAT)); } @Test public void testInvalidAddress() { String invalidServer = "http://localhost:" + findFreePort() + " "; String anotherInvalidServer = "http://localhost:" + findFreePort() + " "; mockMetaServerAddress(Env.LPT, invalidServer + "," + anotherInvalidServer); portalMetaDomainService.reload(); String metaServer = portalMetaDomainService.getDomain(Env.LPT); assertTrue( metaServer.equals(invalidServer.trim()) || metaServer.equals(anotherInvalidServer.trim())); } private void mockMetaServerAddress(Env env, String metaServerAddress) { // add it to system's property System.setProperty(env.getName() + "_meta", metaServerAddress); } } ================================================ FILE: apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/filter/PortalOpenApiAuthenticationScenariosTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.filter; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.when; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import com.ctrip.framework.apollo.openapi.entity.ConsumerToken; import com.ctrip.framework.apollo.openapi.util.ConsumerAuditUtil; import com.ctrip.framework.apollo.openapi.util.ConsumerAuthUtil; import com.ctrip.framework.apollo.portal.spi.configuration.AuthFilterConfiguration; import java.util.Date; import jakarta.servlet.FilterChain; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.junit.After; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; import org.springframework.core.annotation.Order; import org.springframework.http.ResponseEntity; import org.springframework.mock.env.MockEnvironment; import org.springframework.mock.web.MockFilterChain; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.provisioning.InMemoryUserDetailsManager; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; @RunWith(org.springframework.test.context.junit4.SpringRunner.class) @SpringBootTest(classes = PortalOpenApiAuthenticationScenariosTest.TestApplication.class) @AutoConfigureMockMvc // Restrict helper beans (controllers + security config) to a synthetic profile so other tests // scanning the same base package do not accidentally pick them up. @ActiveProfiles({"auth", "portal-scenarios-test"}) public class PortalOpenApiAuthenticationScenariosTest { private static final String PORTAL_URI = "/apps/test/envs/DEV/clusters/default"; private static final String OPEN_API_URI = "/openapi/v1/envs/DEV/apps/test/clusters/default"; @SpringBootApplication @Import({AuthFilterConfiguration.class, TestSecurityConfiguration.class, TestControllerConfiguration.class}) static class TestApplication { } @Configuration @EnableWebSecurity // Keep this test-only WebSecurityConfigurer from leaking into other SpringBootTests. @Profile("portal-scenarios-test") static class TestSecurityConfiguration { @Bean @Order(0) public SecurityFilterChain testSecurityFilterChain(HttpSecurity http) throws Exception { http.securityMatcher("/signin", "/apps/**", "/openapi/**"); http.csrf(csrf -> csrf.disable()); http.authorizeHttpRequests( authorizeHttpRequests -> authorizeHttpRequests.requestMatchers("/signin").permitAll() .requestMatchers("/openapi/**").permitAll().anyRequest().hasRole("user")); http.formLogin(formLogin -> formLogin.loginPage("/signin")); http.exceptionHandling(exceptionHandling -> exceptionHandling .authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/signin"))); http.httpBasic(Customizer.withDefaults()); return http.build(); } @Bean public UserDetailsService userDetailsService() { return new InMemoryUserDetailsManager( User.withUsername("apollo").password("{noop}password").roles("user").build()); } } @Configuration // Controllers under test live behind the same synthetic profile for the same reason as above. @Profile("portal-scenarios-test") static class TestControllerConfiguration { @RestController @Profile("portal-scenarios-test") static class PortalTestController { @GetMapping("/apps/{appId}/envs/{env}/clusters/{clusterName}") public ResponseEntity loadPortalCluster(@PathVariable String appId, @PathVariable String env, @PathVariable String clusterName) { return ResponseEntity.ok("portal-ok"); } } @RestController @Profile("portal-scenarios-test") static class OpenApiTestController { @GetMapping("/openapi/v1/envs/{env}/apps/{appId}/clusters/{clusterName}") public ResponseEntity loadOpenApiCluster(@PathVariable String env, @PathVariable String appId, @PathVariable String clusterName) { return ResponseEntity.ok("openapi-ok"); } } } @Autowired private MockMvc mockMvc; @MockBean private ConsumerAuthUtil consumerAuthUtil; @MockBean private ConsumerAuditUtil consumerAuditUtil; @After public void tearDown() { reset(consumerAuthUtil, consumerAuditUtil); } // Scenario 2.1-1: Portal endpoint with valid session returns 200 OK. @Test public void portalRequestWithValidSession_shouldReturnOk() throws Exception { mockMvc.perform(get(PORTAL_URI).with(user("apollo").roles("user"))).andExpect(status().isOk()); } // Scenario 2.1-2: Portal endpoint with expired session redirects to /signin (auth/ldap) or // returns 401 (oidc). @Test public void portalRequestWithExpiredSession_shouldRedirectToSignin() throws Exception { // auth/ldap path is handled via Spring Security entry point mockMvc.perform(get(PORTAL_URI).cookie(new Cookie("SESSION", "expired"))) .andExpect(status().is3xxRedirection()).andExpect(redirectedUrl("http://localhost/signin")); // oidc path is handled by PortalUserSessionFilter assertOidcExpiredSessionIsUnauthorized(PORTAL_URI); } // Scenario 2.2-1: Portal user hitting OpenAPI with valid session returns 200 OK. @Test public void openApiRequestWithPortalSession_shouldReturnOk() throws Exception { mockMvc.perform(get(OPEN_API_URI).with(user("apollo").roles("user"))) .andExpect(status().isOk()); } // Scenario 2.2-2: OpenAPI with expired portal session redirects (auth/ldap) or returns 401 // (oidc). @Test public void openApiRequestWithExpiredSession_shouldFollowProfileSpecificHandling() throws Exception { // auth/ldap mockMvc.perform(get(OPEN_API_URI).cookie(new Cookie("SESSION", "expired"))) .andExpect(status().is3xxRedirection()).andExpect(redirectedUrl("http://localhost/signin")); // should // be // aligned // with // 2.1-2 // portal // calling // portal // oidc assertOidcExpiredSessionIsUnauthorized(OPEN_API_URI); } // Scenario 2.2-3: External system with valid token gets 200 OK. @Test public void openApiRequestWithValidToken_shouldReturnOk() throws Exception { ConsumerToken token = new ConsumerToken(); token.setConsumerId(1L); token.setToken("valid-token"); token.setRateLimit(0); token.setExpires(new Date(System.currentTimeMillis() + 60_000)); when(consumerAuthUtil.getConsumerToken("valid-token")).thenReturn(token); when(consumerAuditUtil.audit(any(HttpServletRequest.class), eq(1L))).thenReturn(true); mockMvc.perform(get(OPEN_API_URI).header("Authorization", "valid-token")) .andExpect(status().isOk()); } // Scenario 2.2-4: Unauthenticated call without token gets 401 Unauthorized. @Test public void openApiRequestWithoutLoginOrToken_shouldReturn401() throws Exception { when(consumerAuthUtil.getConsumerToken(null)).thenReturn(null); mockMvc.perform(get(OPEN_API_URI)) .andExpect(status().isUnauthorized()); } private void assertOidcExpiredSessionIsUnauthorized(String uri) throws Exception { MockEnvironment oidcEnvironment = new MockEnvironment(); oidcEnvironment.setActiveProfiles("oidc"); PortalUserSessionFilter oidcFilter = new PortalUserSessionFilter(oidcEnvironment); MockHttpServletRequest request = new MockHttpServletRequest("GET", uri); request.setCookies(new Cookie("SESSION", "expired")); MockHttpServletResponse response = new MockHttpServletResponse(); FilterChain chain = new MockFilterChain(); oidcFilter.doFilter(request, response, chain); org.junit.Assert.assertEquals(HttpServletResponse.SC_UNAUTHORIZED, response.getStatus()); } } ================================================ FILE: apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/filter/UserTypeResolverFilter.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.filter; import com.ctrip.framework.apollo.portal.component.UserIdentityContextHolder; import com.ctrip.framework.apollo.portal.constant.UserIdentityConstants; import org.springframework.web.filter.OncePerRequestFilter; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; public class UserTypeResolverFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { if (request.getRequestURI().contains("openapi")) { UserIdentityContextHolder.setAuthType(UserIdentityConstants.CONSUMER); } else { UserIdentityContextHolder.setAuthType(UserIdentityConstants.USER); } filterChain.doFilter(request, response); } } ================================================ FILE: apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/service/AppNamespaceServiceTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.service; import com.ctrip.framework.apollo.common.entity.AppNamespace; import com.ctrip.framework.apollo.common.exception.BadRequestException; import com.ctrip.framework.apollo.core.ConfigConsts; import com.ctrip.framework.apollo.core.enums.ConfigFileFormat; import com.ctrip.framework.apollo.portal.AbstractIntegrationTest; import org.junit.Assert; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.jdbc.Sql; import java.util.List; public class AppNamespaceServiceTest extends AbstractIntegrationTest { @Autowired private AppNamespaceService appNamespaceService; private final String APP = "app-test"; @Test @Sql(scripts = "/sql/appnamespaceservice/init-appnamespace.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/sql/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testFindPublicAppNamespace() { List appNamespaceList = appNamespaceService.findPublicAppNamespaces(); Assert.assertNotNull(appNamespaceList); Assert.assertEquals(5, appNamespaceList.size()); } @Test @Sql(scripts = "/sql/appnamespaceservice/init-appnamespace.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/sql/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testFindPublicAppNamespaceByName() { Assert.assertNotNull(appNamespaceService.findPublicAppNamespace("datasourcexml")); Assert.assertNull(appNamespaceService.findPublicAppNamespace("TFF.song0711-02")); } @Test @Sql(scripts = "/sql/appnamespaceservice/init-appnamespace.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/sql/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testFindPublicAppNamespaceByAppAndName() { Assert.assertNotNull(appNamespaceService.findByAppIdAndName("100003173", "datasourcexml")); Assert.assertNull(appNamespaceService.findByAppIdAndName("100003173", "TFF.song0711-02")); } @Test @Sql(scripts = "/sql/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testCreateDefaultAppNamespace() { appNamespaceService.createDefaultAppNamespace(APP); AppNamespace appNamespace = appNamespaceService.findByAppIdAndName(APP, ConfigConsts.NAMESPACE_APPLICATION); Assert.assertNotNull(appNamespace); Assert.assertEquals(ConfigFileFormat.Properties.getValue(), appNamespace.getFormat()); } @Test(expected = BadRequestException.class) @Sql(scripts = "/sql/appnamespaceservice/init-appnamespace.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/sql/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testCreatePublicAppNamespaceExisted() { AppNamespace appNamespace = assembleBaseAppNamespace(); appNamespace.setPublic(true); appNamespace.setName("old"); appNamespace.setFormat(ConfigFileFormat.Properties.getValue()); appNamespaceService.createAppNamespaceInLocal(appNamespace); } @Test(expected = BadRequestException.class) @Sql(scripts = "/sql/appnamespaceservice/init-appnamespace.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/sql/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testCreatePublicAppNamespaceExistedAsPrivateAppNamespace() { AppNamespace appNamespace = assembleBaseAppNamespace(); appNamespace.setPublic(true); appNamespace.setName("private-01"); appNamespace.setFormat(ConfigFileFormat.Properties.getValue()); appNamespaceService.createAppNamespaceInLocal(appNamespace); } @Test @Sql(scripts = "/sql/appnamespaceservice/init-appnamespace.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/sql/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testCreatePublicAppNamespaceNotExistedWithNoAppendnamespacePrefix() { AppNamespace appNamespace = assembleBaseAppNamespace(); appNamespace.setPublic(true); appNamespace.setName("old"); appNamespace.setFormat(ConfigFileFormat.Properties.getValue()); AppNamespace createdAppNamespace = appNamespaceService.createAppNamespaceInLocal(appNamespace, false); Assert.assertNotNull(createdAppNamespace); Assert.assertEquals(appNamespace.getName(), createdAppNamespace.getName()); } @Test(expected = BadRequestException.class) @Sql(scripts = "/sql/appnamespaceservice/init-appnamespace.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/sql/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testCreatePublicAppNamespaceExistedWithNoAppendnamespacePrefix() { AppNamespace appNamespace = assembleBaseAppNamespace(); appNamespace.setPublic(true); appNamespace.setName("datasource"); appNamespace.setFormat(ConfigFileFormat.Properties.getValue()); appNamespaceService.createAppNamespaceInLocal(appNamespace, false); } @Test @Sql(scripts = "/sql/appnamespaceservice/init-appnamespace.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/sql/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testCreatePublicAppNamespaceNotExisted() { AppNamespace appNamespace = assembleBaseAppNamespace(); appNamespace.setPublic(true); appNamespaceService.createAppNamespaceInLocal(appNamespace); AppNamespace createdAppNamespace = appNamespaceService.findPublicAppNamespace(appNamespace.getName()); Assert.assertNotNull(createdAppNamespace); Assert.assertEquals(appNamespace.getName(), createdAppNamespace.getName()); } @Test @Sql(scripts = "/sql/appnamespaceservice/init-appnamespace.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/sql/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testCreatePublicAppNamespaceWithWrongFormatNotExisted() { AppNamespace appNamespace = assembleBaseAppNamespace(); appNamespace.setPublic(true); appNamespace.setFormat(ConfigFileFormat.YAML.getValue()); appNamespaceService.createAppNamespaceInLocal(appNamespace); AppNamespace createdAppNamespace = appNamespaceService.findPublicAppNamespace(appNamespace.getName()); Assert.assertNotNull(createdAppNamespace); Assert.assertEquals(appNamespace.getName(), createdAppNamespace.getName()); Assert.assertEquals(ConfigFileFormat.YAML.getValue(), createdAppNamespace.getFormat()); } @Test(expected = BadRequestException.class) @Sql(scripts = "/sql/appnamespaceservice/init-appnamespace.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/sql/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testCreatePrivateAppNamespaceExisted() { AppNamespace appNamespace = assembleBaseAppNamespace(); appNamespace.setPublic(false); appNamespace.setName("datasource"); appNamespace.setAppId("100003173"); appNamespaceService.createAppNamespaceInLocal(appNamespace); } @Test @Sql(scripts = "/sql/appnamespaceservice/init-appnamespace.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/sql/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testCreatePrivateAppNamespaceExistedInAnotherAppId() { AppNamespace appNamespace = assembleBaseAppNamespace(); appNamespace.setPublic(false); appNamespace.setName("datasource"); appNamespace.setAppId("song0711-01"); appNamespaceService.createAppNamespaceInLocal(appNamespace); AppNamespace createdAppNamespace = appNamespaceService.findByAppIdAndName(appNamespace.getAppId(), appNamespace.getName()); Assert.assertNotNull(createdAppNamespace); Assert.assertEquals(appNamespace.getName(), createdAppNamespace.getName()); } @Test(expected = BadRequestException.class) @Sql(scripts = "/sql/appnamespaceservice/init-appnamespace.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/sql/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testCreatePrivateAppNamespaceExistedInAnotherAppIdAsPublic() { AppNamespace appNamespace = assembleBaseAppNamespace(); appNamespace.setPublic(false); appNamespace.setName("SCC.song0711-03"); appNamespace.setAppId("100003173"); appNamespace.setFormat(ConfigFileFormat.Properties.getValue()); appNamespaceService.createAppNamespaceInLocal(appNamespace); } @Test @Sql(scripts = "/sql/appnamespaceservice/init-appnamespace.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/sql/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testCreatePrivateAppNamespaceNotExisted() { AppNamespace appNamespace = assembleBaseAppNamespace(); appNamespace.setPublic(false); appNamespaceService.createAppNamespaceInLocal(appNamespace); AppNamespace createdAppNamespace = appNamespaceService.findByAppIdAndName(appNamespace.getAppId(), appNamespace.getName()); Assert.assertNotNull(createdAppNamespace); Assert.assertEquals(appNamespace.getName(), createdAppNamespace.getName()); } private AppNamespace assembleBaseAppNamespace() { AppNamespace appNamespace = new AppNamespace(); appNamespace.setName("appNamespace"); appNamespace.setAppId("1000"); appNamespace.setFormat(ConfigFileFormat.XML.getValue()); return appNamespace; } } ================================================ FILE: apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/service/AppServiceTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.service; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import com.ctrip.framework.apollo.audit.api.ApolloAuditLogApi; import com.ctrip.framework.apollo.common.entity.App; import com.ctrip.framework.apollo.common.exception.BadRequestException; import com.ctrip.framework.apollo.portal.api.AdminServiceAPI; import com.ctrip.framework.apollo.portal.component.PortalSettings; import com.ctrip.framework.apollo.portal.entity.bo.UserInfo; import com.ctrip.framework.apollo.portal.repository.AppRepository; import com.ctrip.framework.apollo.portal.spi.UserInfoHolder; import com.ctrip.framework.apollo.portal.spi.UserService; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.Set; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.parallel.Execution; import org.junit.jupiter.api.parallel.ExecutionMode; import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.ApplicationEventPublisher; import org.springframework.test.context.ContextConfiguration; /** * @author wxq */ @Execution(ExecutionMode.SAME_THREAD) @SpringBootTest @ContextConfiguration(classes = AppService.class) class AppServiceTest { private static final String OPERATOR_USER_ID = "userId-operator"; @Autowired AppService appService; @MockBean UserInfoHolder userInfoHolder; @MockBean AdminServiceAPI.AppAPI appAPI; @MockBean AppRepository appRepository; @MockBean ClusterService clusterService; @MockBean AppNamespaceService appNamespaceService; @MockBean RoleInitializationService roleInitializationService; @MockBean RolePermissionService rolePermissionService; @MockBean FavoriteService favoriteService; @MockBean UserService userService; @MockBean ApplicationEventPublisher publisher; @MockBean ApolloAuditLogApi apolloAuditLogApi; @MockBean PortalSettings portalSettings; @BeforeEach void beforeEach() { // reset the mock after each test Mockito.reset(userInfoHolder, appAPI, appRepository, clusterService, appNamespaceService, roleInitializationService, rolePermissionService, favoriteService, userService, publisher, apolloAuditLogApi); UserInfo userInfo = new UserInfo(); userInfo.setUserId(OPERATOR_USER_ID); Mockito.when(userInfoHolder.getUser()).thenReturn(userInfo); } @Test void createAppAndAddRolePermissionButAppAlreadyExists() { Mockito.when(appRepository.findByAppId(Mockito.any())).thenReturn(new App()); assertThrows(BadRequestException.class, () -> appService.createAppAndAddRolePermission(new App(), Collections.emptySet())); } @Test void createAppAndAddRolePermissionButOwnerNotExists() { Mockito.when(userService.findByUserId(Mockito.any())).thenReturn(null); assertThrows(BadRequestException.class, () -> appService.createAppAndAddRolePermission(new App(), Collections.emptySet())); } @Test void createAppAndAddRolePermission() { final String userId = "user100"; final String appId = "appId100"; { UserInfo userInfo = new UserInfo(); userInfo.setUserId(userId); userInfo.setEmail("xxx@xxx.com"); Mockito.when(userService.findByUserId(Mockito.eq(userId))).thenReturn(userInfo); } final App app = new App(); app.setAppId(appId); app.setOwnerName(userId); Set admins = new HashSet<>(Arrays.asList("user1", "user2")); final App createdApp = new App(); createdApp.setAppId(appId); createdApp.setOwnerName(userId); { Mockito.when(appRepository.save(Mockito.eq(app))).thenReturn(createdApp); } appService.createAppAndAddRolePermission(app, admins); Mockito.verify(appRepository, Mockito.times(1)).findByAppId(Mockito.eq(appId)); Mockito.verify(userService, Mockito.times(1)).findByUserId(Mockito.eq(userId)); Mockito.verify(userInfoHolder, Mockito.times(2)).getUser(); Mockito.verify(appRepository, Mockito.times(1)).save(Mockito.eq(app)); Mockito.verify(appNamespaceService, Mockito.times(1)) .createDefaultAppNamespace(Mockito.eq(appId)); Mockito.verify(roleInitializationService, Mockito.times(1)) .initAppRoles(Mockito.eq(createdApp)); Mockito.verify(rolePermissionService, Mockito.times(1)).assignRoleToUsers(Mockito.any(), Mockito.eq(admins), Mockito.eq(OPERATOR_USER_ID)); } @Test void testDeleteAppInLocal() { final String appId = "appId100"; { App app = new App(); app.setAppId(appId); Mockito.when(appRepository.findByAppId(Mockito.eq(appId))).thenReturn(app); } { Mockito.when(appRepository.deleteApp(Mockito.eq(appId), Mockito.eq(OPERATOR_USER_ID))) .thenReturn(1); } App deletedApp = appService.deleteAppInLocal(appId); Mockito.verify(appRepository, Mockito.times(1)).deleteApp(Mockito.eq(appId), Mockito.eq(OPERATOR_USER_ID)); Mockito.verify(userInfoHolder, Mockito.times(1)).getUser(); Mockito.verify(apolloAuditLogApi, Mockito.times(1)).appendDataInfluences( Mockito.eq(Collections.singletonList(deletedApp)), Mockito.eq(App.class)); Mockito.verify(appNamespaceService, Mockito.times(1)).batchDeleteByAppId(Mockito.eq(appId), Mockito.eq(OPERATOR_USER_ID)); Mockito.verify(favoriteService, Mockito.times(1)).batchDeleteByAppId(Mockito.eq(appId), Mockito.eq(OPERATOR_USER_ID)); Mockito.verify(rolePermissionService, Mockito.times(1)) .deleteRolePermissionsByAppId(Mockito.eq(appId), Mockito.eq(OPERATOR_USER_ID)); assertEquals(OPERATOR_USER_ID, deletedApp.getDataChangeLastModifiedBy()); } } ================================================ FILE: apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/service/ConfigServiceTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.service; import com.ctrip.framework.apollo.common.dto.ItemChangeSets; import com.ctrip.framework.apollo.common.dto.ItemDTO; import com.ctrip.framework.apollo.common.dto.NamespaceDTO; import com.ctrip.framework.apollo.common.exception.BadRequestException; import com.ctrip.framework.apollo.core.ConfigConsts; import com.ctrip.framework.apollo.core.enums.ConfigFileFormat; import com.ctrip.framework.apollo.portal.environment.Env; import com.ctrip.framework.apollo.portal.AbstractUnitTest; import com.ctrip.framework.apollo.portal.api.AdminServiceAPI; import com.ctrip.framework.apollo.portal.spi.UserInfoHolder; import com.ctrip.framework.apollo.portal.entity.bo.UserInfo; import com.ctrip.framework.apollo.portal.component.txtresolver.PropertyResolver; import com.ctrip.framework.apollo.portal.entity.model.NamespaceTextModel; import com.ctrip.framework.apollo.portal.entity.vo.ItemDiffs; import com.ctrip.framework.apollo.portal.entity.vo.NamespaceIdentifier; import java.util.Collections; import org.junit.Before; import org.junit.Test; import org.mockito.InjectMocks; import org.mockito.Mock; import org.springframework.test.util.ReflectionTestUtils; import java.util.Arrays; import java.util.List; import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; public class ConfigServiceTest extends AbstractUnitTest { @Mock private AdminServiceAPI.NamespaceAPI namespaceAPI; @Mock private AdminServiceAPI.ReleaseAPI releaseAPI; @Mock private AdminServiceAPI.ItemAPI itemAPI; @Mock private PropertyResolver resolver; @Mock private UserInfoHolder userInfoHolder; @InjectMocks private ItemService configService; @Before public void setup() { ReflectionTestUtils.setField(configService, "propertyResolver", resolver); } @Test public void testUpdateConfigByText() { String appId = "6666"; String clusterName = "default"; String namespaceName = "application"; long someNamespaceId = 123L; NamespaceTextModel model = mockNamespaceModel(appId, clusterName, namespaceName, someNamespaceId); List itemDTOs = mockBaseItemHas3Key(); ItemChangeSets changeSets = new ItemChangeSets(); changeSets.addCreateItem(new ItemDTO("d", "c", "", 4)); NamespaceDTO someNamespaceDto = mock(NamespaceDTO.class); when(someNamespaceDto.getId()).thenReturn(someNamespaceId); when(namespaceAPI.loadNamespace(appId, model.getEnv(), clusterName, namespaceName)) .thenReturn(someNamespaceDto); when(itemAPI.findItems(appId, Env.DEV, clusterName, namespaceName)).thenReturn(itemDTOs); when(resolver.resolve(someNamespaceId, model.getConfigText(), itemDTOs)).thenReturn(changeSets); UserInfo userInfo = new UserInfo(); userInfo.setUserId("test"); when(userInfoHolder.getUser()).thenReturn(userInfo); configService.updateConfigItemByText(model); } private NamespaceTextModel mockNamespaceModel(String appId, String clusterName, String namespaceName, long someNamespaceId) { NamespaceTextModel model = new NamespaceTextModel(); model.setEnv("DEV"); model.setNamespaceName(namespaceName); model.setClusterName(clusterName); model.setAppId(appId); model.setConfigText("a=b\nb=c\nc=d\nd=e"); model.setFormat(ConfigFileFormat.Properties.getValue()); model.setNamespaceId(someNamespaceId); return model; } @Test(expected = BadRequestException.class) public void testUpdateConfigByTextWithInvalidNamespaceId() { String appId = "6666"; String clusterName = "default"; String namespaceName = "application"; long someNamespaceId = 123L; long anotherNamespaceId = 321L; NamespaceTextModel model = mockNamespaceModel(appId, clusterName, namespaceName, anotherNamespaceId); List itemDTOs = mockBaseItemHas3Key(); ItemChangeSets changeSets = new ItemChangeSets(); changeSets.addCreateItem(new ItemDTO("d", "c", "", 4)); NamespaceDTO someNamespaceDto = mock(NamespaceDTO.class); when(someNamespaceDto.getId()).thenReturn(someNamespaceId); when(namespaceAPI.loadNamespace(appId, model.getEnv(), clusterName, namespaceName)) .thenReturn(someNamespaceDto); when(itemAPI.findItems(appId, Env.DEV, clusterName, namespaceName)).thenReturn(itemDTOs); when(resolver.resolve(someNamespaceId, model.getConfigText(), itemDTOs)).thenReturn(changeSets); UserInfo userInfo = new UserInfo(); userInfo.setUserId("test"); when(userInfoHolder.getUser()).thenReturn(userInfo); configService.updateConfigItemByText(model); } /** * a=b b=c c=d */ private List mockBaseItemHas3Key() { ItemDTO item1 = new ItemDTO("a", "b", "", 1); ItemDTO item2 = new ItemDTO("b", "c", "", 2); ItemDTO item3 = new ItemDTO("c", "d", "", 3); return Arrays.asList(item1, item2, item3); } @Test public void testCompareTargetNamespaceHasNoItems() { ItemDTO sourceItem1 = new ItemDTO("a", "b", "comment", 1); List sourceItems = Collections.singletonList(sourceItem1); String appId = "6666", env = "LOCAL", clusterName = ConfigConsts.CLUSTER_NAME_DEFAULT, namespaceName = ConfigConsts.NAMESPACE_APPLICATION; List namespaceIdentifiers = generateNamespaceIdentifier(appId, env, clusterName, namespaceName); NamespaceDTO namespaceDTO = generateNamespaceDTO(appId, clusterName, namespaceName); when(namespaceAPI.loadNamespace(appId, Env.valueOf(env), clusterName, namespaceName)) .thenReturn(namespaceDTO); when(itemAPI.findItems(appId, Env.valueOf(env), clusterName, namespaceName)).thenReturn(null); UserInfo userInfo = new UserInfo(); userInfo.setUserId("test"); when(userInfoHolder.getUser()).thenReturn(userInfo); List itemDiffses = configService.compare(namespaceIdentifiers, sourceItems); assertEquals(1, itemDiffses.size()); ItemDiffs itemDiffs = itemDiffses.get(0); ItemChangeSets changeSets = itemDiffs.getDiffs(); assertEquals(0, changeSets.getUpdateItems().size()); assertEquals(0, changeSets.getDeleteItems().size()); List createItems = changeSets.getCreateItems(); ItemDTO createItem = createItems.get(0); assertEquals(1, createItem.getLineNum()); assertEquals("a", createItem.getKey()); assertEquals("b", createItem.getValue()); assertEquals("comment", createItem.getComment()); } @Test public void testCompare() { ItemDTO sourceItem1 = new ItemDTO("a", "b", "comment", 1);// not modified ItemDTO sourceItem2 = new ItemDTO("newKey", "c", "comment", 2);// new item ItemDTO sourceItem3 = new ItemDTO("c", "newValue", "comment", 3);// update value ItemDTO sourceItem4 = new ItemDTO("d", "b", "newComment", 4);// update comment List sourceItems = Arrays.asList(sourceItem1, sourceItem2, sourceItem3, sourceItem4); ItemDTO targetItem1 = new ItemDTO("a", "b", "comment", 1); ItemDTO targetItem2 = new ItemDTO("c", "oldValue", "comment", 2); ItemDTO targetItem3 = new ItemDTO("d", "b", "oldComment", 3); List targetItems = Arrays.asList(targetItem1, targetItem2, targetItem3); String appId = "6666", env = "LOCAL", clusterName = ConfigConsts.CLUSTER_NAME_DEFAULT, namespaceName = ConfigConsts.NAMESPACE_APPLICATION; List namespaceIdentifiers = generateNamespaceIdentifier(appId, env, clusterName, namespaceName); NamespaceDTO namespaceDTO = generateNamespaceDTO(appId, clusterName, namespaceName); when(namespaceAPI.loadNamespace(appId, Env.valueOf(env), clusterName, namespaceName)) .thenReturn(namespaceDTO); when(itemAPI.findItems(appId, Env.valueOf(env), clusterName, namespaceName)) .thenReturn(targetItems); UserInfo userInfo = new UserInfo(); userInfo.setUserId("test"); when(userInfoHolder.getUser()).thenReturn(userInfo); List itemDiffses = configService.compare(namespaceIdentifiers, sourceItems); assertEquals(1, itemDiffses.size()); ItemDiffs itemDiffs = itemDiffses.get(0); ItemChangeSets changeSets = itemDiffs.getDiffs(); assertEquals(0, changeSets.getDeleteItems().size()); assertEquals(2, changeSets.getUpdateItems().size()); assertEquals(1, changeSets.getCreateItems().size()); NamespaceIdentifier namespaceIdentifier = itemDiffs.getNamespace(); assertEquals(appId, namespaceIdentifier.getAppId()); assertEquals(Env.valueOf("LOCAL"), namespaceIdentifier.getEnv()); assertEquals(clusterName, namespaceIdentifier.getClusterName()); assertEquals(namespaceName, namespaceIdentifier.getNamespaceName()); ItemDTO createdItem = changeSets.getCreateItems().get(0); assertEquals("newKey", createdItem.getKey()); assertEquals("c", createdItem.getValue()); assertEquals("comment", createdItem.getComment()); assertEquals(4, createdItem.getLineNum()); List updateItems = changeSets.getUpdateItems(); ItemDTO updateItem1 = updateItems.get(0); ItemDTO updateItem2 = updateItems.get(1); assertEquals("c", updateItem1.getKey()); assertEquals("newValue", updateItem1.getValue()); assertEquals("comment", updateItem1.getComment()); assertEquals(2, updateItem1.getLineNum()); assertEquals("d", updateItem2.getKey()); assertEquals("b", updateItem2.getValue()); assertEquals("newComment", updateItem2.getComment()); assertEquals(3, updateItem2.getLineNum()); } private NamespaceDTO generateNamespaceDTO(String appId, String clusterName, String namespaceName) { NamespaceDTO namespaceDTO = new NamespaceDTO(); namespaceDTO.setAppId(appId); namespaceDTO.setId(1); namespaceDTO.setClusterName(clusterName); namespaceDTO.setNamespaceName(namespaceName); return namespaceDTO; } private List generateNamespaceIdentifier(String appId, String env, String clusterName, String namespaceName) { NamespaceIdentifier targetNamespace = new NamespaceIdentifier(); targetNamespace.setAppId(appId); targetNamespace.setEnv(env); targetNamespace.setClusterName(clusterName); targetNamespace.setNamespaceName(namespaceName); return Collections.singletonList(targetNamespace); } } ================================================ FILE: apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/service/ConfigsExportServiceTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.service; import com.ctrip.framework.apollo.common.dto.ClusterDTO; import com.ctrip.framework.apollo.common.dto.ItemDTO; import com.ctrip.framework.apollo.common.dto.NamespaceDTO; import com.ctrip.framework.apollo.common.entity.App; import com.ctrip.framework.apollo.common.entity.AppNamespace; import com.ctrip.framework.apollo.common.exception.BadRequestException; import com.ctrip.framework.apollo.core.enums.ConfigFileFormat; import com.ctrip.framework.apollo.portal.AbstractUnitTest; import com.ctrip.framework.apollo.portal.component.UnifiedPermissionValidator; import com.ctrip.framework.apollo.portal.component.UserPermissionValidator; import com.ctrip.framework.apollo.portal.entity.bo.ItemBO; import com.ctrip.framework.apollo.portal.entity.bo.NamespaceBO; import com.ctrip.framework.apollo.portal.entity.bo.UserInfo; import com.ctrip.framework.apollo.portal.environment.Env; import com.ctrip.framework.apollo.portal.spi.UserInfoHolder; import org.assertj.core.util.Files; import org.assertj.core.util.Lists; import org.junit.Assert; import org.junit.Test; import org.mockito.InjectMocks; import org.mockito.Mock; import org.springframework.context.ApplicationEventPublisher; import org.springframework.http.HttpStatus; import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.HttpStatusCodeException; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.util.List; import java.util.zip.ZipInputStream; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; /** * @author lepdou 2021-08-30 */ public class ConfigsExportServiceTest extends AbstractUnitTest { @Mock private AppService appService; @Mock private ClusterService clusterService; @Mock private NamespaceService namespaceService; @Mock private UserPermissionValidator userPermissionValidator; @Mock private UserInfoHolder userInfoHolder; @Mock private AppNamespaceService appNamespaceService; @InjectMocks private ConfigsExportService configsExportService; @Mock private ItemService itemService; @Mock private ApplicationEventPublisher applicationEventPublisher; @Mock private RoleInitializationService roleInitializationService; @InjectMocks private ConfigsImportService configsImportService; @Mock private UnifiedPermissionValidator unifiedPermissionValidator; @Test public void testNamespaceExportImport() throws FileNotFoundException { // Test with fillItemDetail = true testExportImportScenario(true); } @Test public void testNamespaceExportImportWithFillItemDetail() throws FileNotFoundException { // Test with fillItemDetail = false testExportImportScenario(false); } @Test public void testAppConfigExportImportWithFillItemDetail() throws FileNotFoundException { testAppConfigExportImportScenario(); } private void testExportImportScenario(boolean fillItemDetail) throws FileNotFoundException { File temporaryFolder = Files.newTemporaryFolder(); temporaryFolder.deleteOnExit(); String filePath = temporaryFolder + File.separator + "export.zip"; // export config UserInfo userInfo = genUser(); when(userInfoHolder.getUser()).thenReturn(userInfo); Env env = Env.DEV; String appId1 = "app1"; String appId2 = "app2"; App app1 = genApp(appId1, appId1, "org1", "org2"); App app2 = genApp(appId2, appId2, "org1", "org2"); List exportApps = Lists.newArrayList(app1, app2); String appNamespaceName1 = "ns1"; String appNamespaceName2 = "ns2"; AppNamespace app1Namespace1 = genAppNamespace(appId1, appNamespaceName1, false); AppNamespace app1Namespace2 = genAppNamespace(appId1, appNamespaceName2, true); AppNamespace app2Namespace1 = genAppNamespace(appId2, appNamespaceName1, false); List appNamespaces = Lists.newArrayList(app1Namespace1, app1Namespace2, app2Namespace1); String clusterName1 = "c1"; String clusterName2 = "c2"; ClusterDTO app1Cluster1 = genCluster(clusterName1, appId1); ClusterDTO app1Cluster2 = genCluster(clusterName2, appId1); ClusterDTO app2Cluster1 = genCluster(clusterName1, appId2); ClusterDTO app2Cluster2 = genCluster(clusterName2, appId2); List app1Clusters = Lists.newArrayList(app1Cluster1, app1Cluster2); List app2Clusters = Lists.newArrayList(app2Cluster1, app2Cluster2); ItemBO item1 = genItem("k1", "v1"); ItemBO item2 = genItem("k2", "v2"); List items = Lists.newArrayList(item1, item2); String namespaceName1 = "namespace1"; String namespaceName2 = "namespace2"; NamespaceBO app1Cluster1Namespace1 = genNamespace(app1, app1Cluster1, items, namespaceName1); NamespaceBO app1Cluster1Namespace2 = genNamespace(app1, app1Cluster1, items, namespaceName2); List app1Cluster1Namespace = Lists.newArrayList(app1Cluster1Namespace1, app1Cluster1Namespace2); NamespaceBO app1Cluster2Namespace1 = genNamespace(app1, app1Cluster2, items, namespaceName1); List app1Cluster2Namespace = Lists.newArrayList(app1Cluster2Namespace1); NamespaceBO app2Cluster1Namespace1 = genNamespace(app2, app1Cluster1, items, namespaceName1); List app2Cluster1Namespace = Lists.newArrayList(app2Cluster1Namespace1); NamespaceBO app2Cluster2Namespace1 = genNamespace(app2, app1Cluster2, items, namespaceName1); NamespaceBO app2Cluster2Namespace2 = genNamespace(app2, app1Cluster2, items, namespaceName2); List app2Cluster2Namespace = Lists.newArrayList(app2Cluster2Namespace1, app2Cluster2Namespace2); when(appService.findAll()).thenReturn(exportApps); when(appNamespaceService.findAll()).thenReturn(appNamespaces); when(userPermissionValidator.isAppAdmin(any())).thenReturn(true); when(unifiedPermissionValidator.isAppAdmin(any())).thenReturn(true); when(unifiedPermissionValidator.hasAssignRolePermission(anyString())).thenReturn(true); when(unifiedPermissionValidator.isSuperAdmin()).thenReturn(true); when(clusterService.findClusters(env, appId1)).thenReturn(app1Clusters); when(clusterService.findClusters(env, appId2)).thenReturn(app2Clusters); when(namespaceService.findNamespaceBOs(appId1, Env.DEV, clusterName1, fillItemDetail, false)) .thenReturn(app1Cluster1Namespace); when(namespaceService.findNamespaceBOs(appId1, Env.DEV, clusterName2, fillItemDetail, false)) .thenReturn(app1Cluster2Namespace); when(namespaceService.findNamespaceBOs(appId2, Env.DEV, clusterName1, fillItemDetail, false)) .thenReturn(app2Cluster1Namespace); when(namespaceService.findNamespaceBOs(appId2, Env.DEV, clusterName2, fillItemDetail, false)) .thenReturn(app2Cluster2Namespace); FileOutputStream fileOutputStream = new FileOutputStream(filePath); configsExportService.exportData(fileOutputStream, Lists.newArrayList(Env.DEV)); // import config when(appNamespaceService.findByAppIdAndName(any(), any())).thenReturn(null); when(appNamespaceService.importAppNamespaceInLocal(any())).thenReturn(app1Namespace1); when(appService.load(any())).thenReturn(null); when(appService.load(any(), any())).thenThrow(new RuntimeException()); when(clusterService.loadCluster(any(), any(), any())).thenThrow(new RuntimeException()); when(namespaceService.loadNamespaceBaseInfo(any(), any(), any(), any())) .thenThrow(new RuntimeException()); when(namespaceService.createNamespace(any(), any())).thenReturn(genNamespaceDTO(1)); when(itemService.findItems(any(), any(), any(), any())).thenReturn(Lists.newArrayList()); HttpStatusCodeException itemNotFoundException = new HttpClientErrorException(HttpStatus.NOT_FOUND); when(itemService.loadItem(any(), any(), any(), any(), anyString())) .thenThrow(itemNotFoundException); FileInputStream fileInputStream = new FileInputStream(filePath); ZipInputStream zipInputStream = new ZipInputStream(fileInputStream); try { configsImportService.importDataFromZipFile(Lists.newArrayList(Env.DEV), zipInputStream, false); } catch (Exception e) { e.printStackTrace(); } verify(appNamespaceService, times(3)).importAppNamespaceInLocal(any()); verify(applicationEventPublisher, times(3)).publishEvent(any()); verify(appService, times(2)).createAppInRemote(any(), any()); verify(clusterService, times(4)).createCluster(any(), any()); if (fillItemDetail) { verify(namespaceService, times(6)).createNamespace(any(), any()); verify(roleInitializationService, times(6)).initNamespaceRoles(any(), any(), anyString()); verify(roleInitializationService, times(6)).initNamespaceEnvRoles(any(), any(), anyString()); verify(itemService, times(12)).createItem(any(), any(), any(), any(), any()); } } private void testAppConfigExportImportScenario() throws FileNotFoundException { File temporaryFolder = Files.newTemporaryFolder(); temporaryFolder.deleteOnExit(); String filePath = temporaryFolder + File.separator + "export.zip"; // export config UserInfo userInfo = genUser(); when(userInfoHolder.getUser()).thenReturn(userInfo); Env env = Env.DEV; String appId1 = "app1"; String appId2 = "app2"; App app1 = genApp(appId1, appId1, "org1", "org2"); String clusterName1 = "c1"; String clusterName2 = "c2"; ClusterDTO app1Cluster1 = genCluster(clusterName1, appId1); ClusterDTO app1Cluster2 = genCluster(clusterName2, appId1); ItemBO item1 = genItem("k1", "v1"); ItemBO item2 = genItem("k2", "v2"); List items = Lists.newArrayList(item1, item2); String namespaceName1 = "namespace1"; String namespaceName2 = "namespace2"; NamespaceBO app1Cluster1Namespace1 = genNamespace(app1, app1Cluster1, items, namespaceName1); NamespaceBO app1Cluster1Namespace2 = genNamespace(app1, app1Cluster1, items, namespaceName2); List app1Cluster1Namespace = Lists.newArrayList(app1Cluster1Namespace1, app1Cluster1Namespace2); when(appService.load(appId1)).thenReturn(app1); when(userPermissionValidator.isAppAdmin(any())).thenReturn(true); when(unifiedPermissionValidator.isAppAdmin(any())).thenReturn(true); when(unifiedPermissionValidator.hasAssignRolePermission(anyString())).thenReturn(true); when(clusterService.loadCluster(appId1, env, clusterName1)).thenReturn(app1Cluster1); when(namespaceService.findNamespaceBOs(appId1, Env.DEV, clusterName1, true, false)) .thenReturn(app1Cluster1Namespace); FileOutputStream fileOutputStream = new FileOutputStream(filePath); try { configsExportService.exportAppConfigByEnvAndCluster(appId2, env, clusterName1, fileOutputStream); } catch (BadRequestException e) { Assert.assertEquals("App not found: " + appId2, e.getMessage()); } try { configsExportService.exportAppConfigByEnvAndCluster(appId1, env, clusterName2, fileOutputStream); } catch (BadRequestException e) { Assert.assertEquals("The app does not exist in the specified environment and cluster.", e.getMessage()); } configsExportService.exportAppConfigByEnvAndCluster(appId1, env, clusterName1, fileOutputStream); // import config when(clusterService.loadCluster(appId1, env, clusterName2)).thenReturn(app1Cluster2); when(namespaceService.loadNamespaceBaseInfo(any(), any(), any(), any())) .thenThrow(new RuntimeException()); when(namespaceService.createNamespace(any(), any())).thenReturn(genNamespaceDTO(1)); when(itemService.findItems(any(), any(), any(), any())).thenReturn(Lists.newArrayList()); HttpStatusCodeException itemNotFoundException = new HttpClientErrorException(HttpStatus.NOT_FOUND); when(itemService.loadItem(any(), any(), any(), any(), anyString())) .thenThrow(itemNotFoundException); FileInputStream fileInputStream = new FileInputStream(filePath); ZipInputStream zipInputStream = new ZipInputStream(fileInputStream); try { configsImportService.importAppConfigFromZipFile(appId2, env, clusterName1, zipInputStream, false); } catch (Exception e) { Assert.assertEquals("The app does not exist in the specified environment and cluster.", e.getMessage()); } fileInputStream = new FileInputStream(filePath); zipInputStream = new ZipInputStream(fileInputStream); try { configsImportService.importAppConfigFromZipFile(appId1, env, clusterName2, zipInputStream, false); } catch (Exception e) { Assert.assertEquals("The content of the file to be imported is incorrect.", e.getMessage()); } fileInputStream = new FileInputStream(filePath); zipInputStream = new ZipInputStream(fileInputStream); try { configsImportService.importAppConfigFromZipFile(appId1, env, clusterName1, zipInputStream, false); } catch (Exception e) { e.printStackTrace(); } verify(namespaceService, times(2)).createNamespace(any(), any()); verify(roleInitializationService, times(2)).initNamespaceRoles(any(), any(), anyString()); verify(roleInitializationService, times(2)).initNamespaceEnvRoles(any(), any(), anyString()); verify(itemService, times(4)).createItem(any(), any(), any(), any(), any()); } private App genApp(String name, String appId, String orgId, String orgName) { App app = new App(); app.setAppId(appId); app.setName(name); app.setOrgName("apollo"); app.setOrgId(orgId); app.setOrgName(orgName); return app; } private ClusterDTO genCluster(String name, String appId) { ClusterDTO clusterDTO = new ClusterDTO(); clusterDTO.setAppId(appId); clusterDTO.setName(name); return clusterDTO; } private AppNamespace genAppNamespace(String appId, String name, boolean isPublic) { AppNamespace appNamespace = new AppNamespace(); appNamespace.setAppId(appId); appNamespace.setPublic(isPublic); appNamespace.setName(name); appNamespace.setFormat(ConfigFileFormat.Properties.getValue()); return appNamespace; } private NamespaceBO genNamespace(App app, ClusterDTO clusterDTO, List itemBOS, String namespaceName) { NamespaceBO namespaceBO = new NamespaceBO(); NamespaceDTO baseInfo = new NamespaceDTO(); baseInfo.setNamespaceName(namespaceName); baseInfo.setAppId(app.getAppId()); baseInfo.setClusterName(clusterDTO.getName()); namespaceBO.setBaseInfo(baseInfo); namespaceBO.setFormat(ConfigFileFormat.Properties.getValue()); namespaceBO.setItems(itemBOS); return namespaceBO; } private ItemBO genItem(String key, String value) { ItemBO itemBO = new ItemBO(); ItemDTO itemDTO = new ItemDTO(); itemDTO.setKey(key); itemDTO.setValue(value); itemBO.setItem(itemDTO); return itemBO; } private NamespaceDTO genNamespaceDTO(long id) { NamespaceDTO dto = new NamespaceDTO(); dto.setId(id); return dto; } private UserInfo genUser() { UserInfo userInfo = new UserInfo(); userInfo.setUserId("apollo"); userInfo.setName("apollo"); userInfo.setEmail("apollo@apollo.com"); return userInfo; } } ================================================ FILE: apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/service/FavoriteServiceTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.service; import com.ctrip.framework.apollo.common.exception.BadRequestException; import com.ctrip.framework.apollo.portal.AbstractIntegrationTest; import com.ctrip.framework.apollo.portal.entity.po.Favorite; import com.ctrip.framework.apollo.portal.repository.FavoriteRepository; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.PageRequest; import org.springframework.test.context.jdbc.Sql; import java.util.List; public class FavoriteServiceTest extends AbstractIntegrationTest { @Autowired private FavoriteService favoriteService; @Autowired private FavoriteRepository favoriteRepository; private String testUser = "apollo"; @Before public void before() { } @Test @Sql(scripts = "/sql/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testAddNormalFavorite() { String testApp = "testApp"; Favorite favorite = instanceOfFavorite(testUser, testApp); favoriteService.addFavorite(favorite); List createdFavorites = favoriteService.search(testUser, testApp, PageRequest.of(0, 10)); Assert.assertEquals(1, createdFavorites.size()); Assert.assertEquals(FavoriteService.POSITION_DEFAULT, createdFavorites.get(0).getPosition()); Assert.assertEquals(testUser, createdFavorites.get(0).getUserId()); Assert.assertEquals(testApp, createdFavorites.get(0).getAppId()); } @Test(expected = BadRequestException.class) @Sql(scripts = "/sql/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testAddFavoriteErrorUser() { String testApp = "testApp"; Favorite favorite = instanceOfFavorite("errorUser", testApp); favoriteService.addFavorite(favorite); } @Test @Sql(scripts = "/sql/favorites/favorites.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/sql/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testSearchByUserId() { List favorites = favoriteService.search(testUser, null, PageRequest.of(0, 10)); Assert.assertEquals(4, favorites.size()); } @Test @Sql(scripts = "/sql/favorites/favorites.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/sql/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testSearchByAppId() { List favorites = favoriteService.search(null, "test0621-04", PageRequest.of(0, 10)); Assert.assertEquals(3, favorites.size()); } @Test @Sql(scripts = "/sql/favorites/favorites.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/sql/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testSearchByAppIdAndUserId() { List favorites = favoriteService.search(testUser, "test0621-04", PageRequest.of(0, 10)); Assert.assertEquals(1, favorites.size()); } @Test(expected = BadRequestException.class) @Sql(scripts = "/sql/favorites/favorites.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/sql/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testSearchWithErrorParams() { favoriteService.search(null, null, PageRequest.of(0, 10)); } @Test @Sql(scripts = "/sql/favorites/favorites.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/sql/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testDeleteFavorite() { long legalFavoriteId = 21L; favoriteService.deleteFavorite(legalFavoriteId); Assert.assertNull(favoriteRepository.findById(legalFavoriteId).orElse(null)); } @Test(expected = BadRequestException.class) @Sql(scripts = "/sql/favorites/favorites.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/sql/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testDeleteFavoriteFail() { long anotherPersonFavoriteId = 23L; favoriteService.deleteFavorite(anotherPersonFavoriteId); Assert.assertNull(favoriteRepository.findById(anotherPersonFavoriteId).orElse(null)); } @Test(expected = BadRequestException.class) @Sql(scripts = "/sql/favorites/favorites.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/sql/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testAdjustFavoriteError() { long anotherPersonFavoriteId = 23; favoriteService.adjustFavoriteToFirst(anotherPersonFavoriteId); } @Test @Sql(scripts = "/sql/favorites/favorites.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/sql/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testAdjustFavorite() { long toAdjustFavoriteId = 20; favoriteService.adjustFavoriteToFirst(toAdjustFavoriteId); List favorites = favoriteService.search(testUser, null, PageRequest.of(0, 10)); Favorite firstFavorite = favorites.get(0); Favorite secondFavorite = favorites.get(1); Assert.assertEquals(toAdjustFavoriteId, firstFavorite.getId()); Assert.assertEquals(firstFavorite.getPosition() + 1, secondFavorite.getPosition()); } private Favorite instanceOfFavorite(String userId, String appId) { Favorite favorite = new Favorite(); favorite.setAppId(appId); favorite.setUserId(userId); favorite.setDataChangeCreatedBy(userId); favorite.setDataChangeLastModifiedBy(userId); return favorite; } } ================================================ FILE: apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/service/GlobalSearchServiceTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.service; /** * @author hujiyuan 2024-08-10 */ import com.ctrip.framework.apollo.common.dto.ItemInfoDTO; import com.ctrip.framework.apollo.common.dto.PageDTO; import com.ctrip.framework.apollo.common.http.SearchResponseEntity; import com.ctrip.framework.apollo.portal.api.AdminServiceAPI; import com.ctrip.framework.apollo.portal.component.PortalSettings; import com.ctrip.framework.apollo.portal.entity.vo.ItemInfo; import com.ctrip.framework.apollo.portal.environment.Env; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; import org.springframework.data.domain.PageRequest; import java.util.ArrayList; import java.util.List; import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @RunWith(MockitoJUnitRunner.class) public class GlobalSearchServiceTest { @Mock private AdminServiceAPI.ItemAPI itemAPI; @Mock private PortalSettings portalSettings; @InjectMocks private GlobalSearchService globalSearchService; private final List activeEnvs = new ArrayList<>(); @Before public void setUp() { when(portalSettings.getActiveEnvs()).thenReturn(activeEnvs); } @Test public void testGet_PerEnv_ItemInfo_BySearch_withKeyAndValue_ReturnExpectedItemInfos() { activeEnvs.add(Env.DEV); activeEnvs.add(Env.PRO); ItemInfoDTO itemInfoDTO = new ItemInfoDTO("TestApp", "TestCluster", "TestNamespace", "TestKey", "TestValue"); List mockItemInfoDTOs = new ArrayList<>(); mockItemInfoDTOs.add(itemInfoDTO); Mockito.when(itemAPI.getPerEnvItemInfoBySearch(any(Env.class), eq("TestKey"), eq("TestValue"), eq(0), eq(1))).thenReturn(new PageDTO<>(mockItemInfoDTOs, PageRequest.of(0, 1), 1L)); SearchResponseEntity> mockItemInfos = globalSearchService.getAllEnvItemInfoBySearch("TestKey", "TestValue", 0, 1); assertEquals(2, mockItemInfos.getBody().size()); List devMockItemInfos = new ArrayList<>(); List proMockItemInfos = new ArrayList<>(); List allEnvMockItemInfos = new ArrayList<>(); devMockItemInfos.add(new ItemInfo("TestApp", Env.DEV.getName(), "TestCluster", "TestNamespace", "TestKey", "TestValue")); proMockItemInfos.add(new ItemInfo("TestApp", Env.PRO.getName(), "TestCluster", "TestNamespace", "TestKey", "TestValue")); allEnvMockItemInfos.addAll(devMockItemInfos); allEnvMockItemInfos.addAll(proMockItemInfos); verify(itemAPI, times(2)).getPerEnvItemInfoBySearch(any(Env.class), eq("TestKey"), eq("TestValue"), eq(0), eq(1)); verify(portalSettings, times(1)).getActiveEnvs(); assertEquals(allEnvMockItemInfos.toString(), mockItemInfos.getBody().toString()); } @Test public void testGet_PerEnv_ItemInfo_withKeyAndValue_BySearch_ReturnEmptyItemInfos() { activeEnvs.add(Env.DEV); activeEnvs.add(Env.PRO); Mockito.when( itemAPI.getPerEnvItemInfoBySearch(any(Env.class), anyString(), anyString(), eq(0), eq(1))) .thenReturn(new PageDTO<>(new ArrayList<>(), PageRequest.of(0, 1), 0L)); SearchResponseEntity> result = globalSearchService.getAllEnvItemInfoBySearch("NonExistentKey", "NonExistentValue", 0, 1); assertEquals(0, result.getBody().size()); } @Test public void testGet_PerEnv_ItemInfo_BySearch_withKeyAndValue_ReturnExpectedItemInfos_ButOverPerEnvLimit() { activeEnvs.add(Env.DEV); activeEnvs.add(Env.PRO); ItemInfoDTO itemInfoDTO = new ItemInfoDTO("TestApp", "TestCluster", "TestNamespace", "TestKey", "TestValue"); List mockItemInfoDTOs = new ArrayList<>(); mockItemInfoDTOs.add(itemInfoDTO); Mockito.when(itemAPI.getPerEnvItemInfoBySearch(any(Env.class), eq("TestKey"), eq("TestValue"), eq(0), eq(1))).thenReturn(new PageDTO<>(mockItemInfoDTOs, PageRequest.of(0, 1), 2L)); SearchResponseEntity> mockItemInfos = globalSearchService.getAllEnvItemInfoBySearch("TestKey", "TestValue", 0, 1); assertEquals(2, mockItemInfos.getBody().size()); List devMockItemInfos = new ArrayList<>(); List proMockItemInfos = new ArrayList<>(); List allEnvMockItemInfos = new ArrayList<>(); devMockItemInfos.add(new ItemInfo("TestApp", Env.DEV.getName(), "TestCluster", "TestNamespace", "TestKey", "TestValue")); proMockItemInfos.add(new ItemInfo("TestApp", Env.PRO.getName(), "TestCluster", "TestNamespace", "TestKey", "TestValue")); allEnvMockItemInfos.addAll(devMockItemInfos); allEnvMockItemInfos.addAll(proMockItemInfos); String message = "In DEV , PRO , more than 1 items found (Exceeded the maximum search quantity for a single environment). Please enter more precise criteria to narrow down the search scope."; verify(itemAPI, times(2)).getPerEnvItemInfoBySearch(any(Env.class), eq("TestKey"), eq("TestValue"), eq(0), eq(1)); verify(portalSettings, times(1)).getActiveEnvs(); assertEquals(allEnvMockItemInfos.toString(), mockItemInfos.getBody().toString()); assertEquals(message, mockItemInfos.getMessage()); } } ================================================ FILE: apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/service/NamespaceServiceTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.service; import com.ctrip.framework.apollo.common.dto.ClusterDTO; import com.ctrip.framework.apollo.common.dto.ItemDTO; import com.ctrip.framework.apollo.common.dto.NamespaceDTO; import com.ctrip.framework.apollo.common.dto.ReleaseDTO; import com.ctrip.framework.apollo.common.entity.AppNamespace; import com.ctrip.framework.apollo.core.enums.ConfigFileFormat; import com.ctrip.framework.apollo.portal.component.PortalSettings; import com.ctrip.framework.apollo.portal.entity.vo.NamespaceUsage; import com.ctrip.framework.apollo.portal.environment.Env; import com.ctrip.framework.apollo.portal.AbstractUnitTest; import com.ctrip.framework.apollo.portal.api.AdminServiceAPI; import com.ctrip.framework.apollo.portal.component.txtresolver.PropertyResolver; import com.ctrip.framework.apollo.portal.entity.bo.NamespaceBO; import com.ctrip.framework.apollo.portal.entity.bo.UserInfo; import com.ctrip.framework.apollo.portal.spi.UserInfoHolder; import org.assertj.core.util.Lists; import org.junit.Before; import org.junit.Test; import org.mockito.InjectMocks; import org.mockito.Mock; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.stream.Collectors; import com.ctrip.framework.apollo.common.exception.BadRequestException; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; public class NamespaceServiceTest extends AbstractUnitTest { @Mock private AdminServiceAPI.NamespaceAPI namespaceAPI; @Mock private ReleaseService releaseService; @Mock private ItemService itemService; @Mock private PropertyResolver resolver; @Mock private AppNamespaceService appNamespaceService; @Mock private InstanceService instanceService; @Mock private NamespaceBranchService branchService; @Mock private UserInfoHolder userInfoHolder; @Mock private AdditionalUserInfoEnrichService additionalUserInfoEnrichService; @Mock private PortalSettings portalSettings; @Mock private ClusterService clusterService; @InjectMocks private NamespaceService namespaceService; private String testAppId = "6666"; private String testClusterName = "default"; private String testNamespaceName = "application"; private Env testEnv = Env.DEV; @Before public void setup() {} @Test public void testFindNamespace() { AppNamespace applicationAppNamespace = mock(AppNamespace.class); AppNamespace hermesAppNamespace = mock(AppNamespace.class); NamespaceDTO application = new NamespaceDTO(); application.setId(1); application.setClusterName(testClusterName); application.setAppId(testAppId); application.setNamespaceName(testNamespaceName); NamespaceDTO hermes = new NamespaceDTO(); hermes.setId(2); hermes.setClusterName("default"); hermes.setAppId(testAppId); hermes.setNamespaceName("hermes"); List namespaces = Arrays.asList(application, hermes); ReleaseDTO someRelease = new ReleaseDTO(); someRelease.setConfigurations("{\"a\":\"123\",\"b\":\"123\"}"); ItemDTO i1 = new ItemDTO("a", "123", "", 1); ItemDTO i2 = new ItemDTO("b", "1", "", 2); ItemDTO i3 = new ItemDTO("", "", "#dddd", 3); ItemDTO i4 = new ItemDTO("c", "1", "", 4); List someItems = Arrays.asList(i1, i2, i3, i4); when(applicationAppNamespace.getFormat()).thenReturn(ConfigFileFormat.Properties.getValue()); when(hermesAppNamespace.getFormat()).thenReturn(ConfigFileFormat.XML.getValue()); when(appNamespaceService.findByAppIdAndName(testAppId, testNamespaceName)) .thenReturn(applicationAppNamespace); when(appNamespaceService.findPublicAppNamespace("hermes")).thenReturn(hermesAppNamespace); when(namespaceAPI.findNamespaceByCluster(testAppId, Env.DEV, testClusterName)) .thenReturn(namespaces); when(releaseService.loadLatestRelease(testAppId, Env.DEV, testClusterName, testNamespaceName)) .thenReturn(someRelease); when(releaseService.loadLatestRelease(testAppId, Env.DEV, testClusterName, "hermes")) .thenReturn(someRelease); when(itemService.findItems(testAppId, Env.DEV, testClusterName, testNamespaceName)) .thenReturn(someItems); List namespaceVOs = namespaceService.findNamespaceBOs(testAppId, Env.DEV, testClusterName); assertEquals(2, namespaceVOs.size()); when(namespaceAPI.findNamespaceByCluster(testAppId, Env.DEV, testClusterName)) .thenReturn(Lists.list(application)); namespaceVOs = namespaceService.findNamespaceBOs(testAppId, Env.DEV, testClusterName); assertEquals(1, namespaceVOs.size()); NamespaceBO namespaceVO = namespaceVOs.get(0); assertEquals(4, namespaceVO.getItems().size()); assertEquals("a", namespaceVO.getItems().get(0).getItem().getKey()); assertEquals(2, namespaceVO.getItemModifiedCnt()); assertEquals(testAppId, namespaceVO.getBaseInfo().getAppId()); assertEquals(testClusterName, namespaceVO.getBaseInfo().getClusterName()); assertEquals(testNamespaceName, namespaceVO.getBaseInfo().getNamespaceName()); ReleaseDTO errorRelease = new ReleaseDTO(); errorRelease.setConfigurations("\"a\":\"123\",\"b\":\"123\""); when(releaseService.loadLatestRelease(testAppId, Env.DEV, testClusterName, testNamespaceName)) .thenReturn(errorRelease); assertThatExceptionOfType(RuntimeException.class) .isThrownBy(() -> namespaceService.findNamespaceBOs(testAppId, Env.DEV, testClusterName)) .withMessageStartingWith( "Parse namespaces error, expected: 1, but actual: 0, cannot get those namespaces: [application]"); } @Test public void testDeletePrivateNamespace() { String operator = "user"; AppNamespace privateNamespace = createAppNamespace(testAppId, testNamespaceName, false); when(appNamespaceService.findByAppIdAndName(testAppId, testNamespaceName)) .thenReturn(privateNamespace); when(userInfoHolder.getUser()).thenReturn(createUser(operator)); namespaceService.deleteNamespace(testAppId, testEnv, testClusterName, testNamespaceName); verify(namespaceAPI, times(1)).deleteNamespace(testEnv, testAppId, testClusterName, testNamespaceName, operator); } @Test public void testGetNamespaceUsage() { AppNamespace publicNamespace = createAppNamespace(testAppId, testNamespaceName, true); String branchName = "branch"; NamespaceDTO branch = createNamespace(testAppId, branchName, testNamespaceName); when(portalSettings.getActiveEnvs()).thenReturn(Lists.newArrayList(testEnv)); ClusterDTO cluster = new ClusterDTO(); cluster.setName(testClusterName); cluster.setAppId(testAppId); when(clusterService.findClusters(testEnv, testAppId)).thenReturn(Lists.newArrayList(cluster)); when(appNamespaceService.findByAppIdAndName(testAppId, testNamespaceName)) .thenReturn(publicNamespace); when(instanceService.getInstanceCountByNamespace(testAppId, testEnv, testClusterName, testNamespaceName)).thenReturn(8); when(branchService.findBranchBaseInfo(testAppId, testEnv, testClusterName, testNamespaceName)) .thenReturn(branch); when(instanceService.getInstanceCountByNamespace(testAppId, testEnv, branchName, testNamespaceName)).thenReturn(9); when(appNamespaceService.findPublicAppNamespace(testNamespaceName)).thenReturn(publicNamespace); when(namespaceAPI.countPublicAppNamespaceAssociatedNamespaces(testEnv, testNamespaceName)) .thenReturn(10); List usages = namespaceService.getNamespaceUsageByAppId(testAppId, testNamespaceName); assertThat(usages).asList().hasSize(1); assertThat(usages.get(0).getInstanceCount()).isEqualTo(8); assertThat(usages.get(0).getBranchInstanceCount()).isEqualTo(9); assertThat(usages.get(0).getLinkedNamespaceCount()).isEqualTo(10); NamespaceUsage usage = namespaceService.getNamespaceUsageByEnv(testAppId, testNamespaceName, testEnv, testClusterName); assertThat(usage).isNotNull(); assertThat(usage.getInstanceCount()).isEqualTo(8); assertThat(usage.getBranchInstanceCount()).isEqualTo(9); assertThat(usage.getLinkedNamespaceCount()).isEqualTo(0); } @Test public void testDeleteEmptyNamespace() { String branchName = "branch"; String operator = "user"; AppNamespace publicNamespace = createAppNamespace(testAppId, testNamespaceName, true); NamespaceDTO branch = createNamespace(testAppId, branchName, testNamespaceName); when(appNamespaceService.findByAppIdAndName(testAppId, testNamespaceName)) .thenReturn(publicNamespace); when(instanceService.getInstanceCountByNamespace(testAppId, testEnv, testClusterName, testNamespaceName)).thenReturn(0); when(branchService.findBranchBaseInfo(testAppId, testEnv, testClusterName, testNamespaceName)) .thenReturn(branch); when(instanceService.getInstanceCountByNamespace(testAppId, testEnv, branchName, testNamespaceName)).thenReturn(0); when(appNamespaceService.findPublicAppNamespace(testNamespaceName)).thenReturn(publicNamespace); NamespaceDTO namespace = createNamespace(testAppId, testClusterName, testNamespaceName); when(namespaceAPI.getPublicAppNamespaceAllNamespaces(testEnv, testNamespaceName, 0, 10)) .thenReturn(Collections.singletonList(namespace)); when(userInfoHolder.getUser()).thenReturn(createUser(operator)); namespaceService.deleteNamespace(testAppId, testEnv, testClusterName, testNamespaceName); verify(namespaceAPI, times(1)).deleteNamespace(testEnv, testAppId, testClusterName, testNamespaceName, operator); } @Test public void testLoadNamespaceBO() { boolean fillItemDetail = true; NamespaceBO namespaceBO = loadNamespaceBO(fillItemDetail); List namespaceKey2 = namespaceBO.getItems().stream().map(s -> s.getItem().getKey()).collect(Collectors.toList()); assertThat(namespaceBO.getItemModifiedCnt()).isEqualTo(2); assertThat(namespaceBO.getItems().size()).isEqualTo(2); assertThat(namespaceKey2).isEqualTo(Arrays.asList("k1", "k2")); } @Test public void testLoadNamespaceBOWithoutItemDetail() { boolean fillItemDetail = false; NamespaceBO namespaceBO = loadNamespaceBO(fillItemDetail); assertThat(namespaceBO.getItems().size()).isEqualTo(0); } private NamespaceBO loadNamespaceBO(boolean fillItemDetail) { when(namespaceAPI.loadNamespace(any(), any(), any(), any())).thenReturn(createNamespace(testAppId, "branch", testNamespaceName)); when(releaseService.loadLatestRelease(any(), any(), any(), any())).thenReturn(createReleaseDTO()); when(itemService.findItems(any(), any(), any(), any())).thenReturn(createItems()); when(itemService.findDeletedItems(any(), any(), any(), any())).thenReturn(createDeletedItems()); NamespaceBO namespaceBO1 = namespaceService.loadNamespaceBO(testAppId, testEnv, testClusterName, testNamespaceName); List namespaceKey1 = namespaceBO1.getItems().stream().map(s -> s.getItem().getKey()).collect(Collectors.toList()); assertThat(namespaceBO1.getItemModifiedCnt()).isEqualTo(3); assertThat(namespaceBO1.getItems().size()).isEqualTo(3); assertThat(namespaceKey1).isEqualTo(Arrays.asList("k1", "k2", "k3")); return namespaceService.loadNamespaceBO(testAppId, testEnv, testClusterName, testNamespaceName, fillItemDetail, false); } @Test public void testFindPublicNamespaceForAssociatedNamespace() { when(namespaceAPI.findPublicNamespaceForAssociatedNamespace(any(), any(), any(), any())).thenReturn(createNamespace(testAppId, "branch", testNamespaceName)); when(releaseService.loadLatestRelease(any(), any(), any(), any())).thenReturn(createReleaseDTO()); when(itemService.findItems(any(), any(), any(), any())).thenReturn(createItems()); when(itemService.findDeletedItems(any(), any(), any(), any())).thenReturn(createDeletedItems()); NamespaceBO namespaceBO = namespaceService.findPublicNamespaceForAssociatedNamespace(testEnv, testAppId, testClusterName, testNamespaceName); List namespaceKey2 = namespaceBO.getItems().stream().map(s -> s.getItem().getKey()).collect(Collectors.toList()); assertThat(namespaceBO.getItemModifiedCnt()).isEqualTo(3); assertThat(namespaceBO.getItems().size()).isEqualTo(3); assertThat(namespaceKey2).isEqualTo(Arrays.asList("k1", "k2", "k3")); } @Test public void testLoadNamespaceBOWithDeletedItems() { ReleaseDTO releaseDTO = createReleaseDTO(); when(releaseService.loadLatestRelease(testAppId, testEnv, testClusterName, testNamespaceName)) .thenReturn(releaseDTO); List itemDTOList = createItems(); when(itemService.findItems(testAppId, testEnv, testClusterName, testNamespaceName)) .thenReturn(itemDTOList); List deletedItemDTOList = Lists.newArrayList(); ItemDTO deletedItemDTO = new ItemDTO(); deletedItemDTO.setKey("deleted-key"); deletedItemDTOList.add(deletedItemDTO); when(itemService.findDeletedItems(testAppId, testEnv, testClusterName, testNamespaceName)) .thenReturn(deletedItemDTOList); when(namespaceAPI.loadNamespace(testAppId, testEnv, testClusterName, testNamespaceName)) .thenReturn(createNamespace(testAppId, testClusterName, testNamespaceName)); when(appNamespaceService.findByAppIdAndName(testAppId, testNamespaceName)) .thenReturn(createAppNamespace(testAppId, testNamespaceName, false)); NamespaceBO namespaceBO = namespaceService.loadNamespaceBO(testAppId, testEnv, testClusterName, testNamespaceName, true, true); assertNotNull(namespaceBO); assertEquals(testAppId, namespaceBO.getBaseInfo().getAppId()); assertEquals(testClusterName, namespaceBO.getBaseInfo().getClusterName()); assertEquals(testNamespaceName, namespaceBO.getBaseInfo().getNamespaceName()); assertEquals(3, namespaceBO.getItems().size()); verify(itemService, times(1)).findDeletedItems(testAppId, testEnv, testClusterName, testNamespaceName); verify(additionalUserInfoEnrichService, times(1)).enrichAdditionalUserInfo(any(), any()); } @Test public void testLoadNamespaceBOWithoutDeletedItems() { ReleaseDTO releaseDTO = createReleaseDTO(); when(releaseService.loadLatestRelease(testAppId, testEnv, testClusterName, testNamespaceName)) .thenReturn(releaseDTO); List itemDTOList = createItems(); when(itemService.findItems(testAppId, testEnv, testClusterName, testNamespaceName)) .thenReturn(itemDTOList); when(namespaceAPI.loadNamespace(testAppId, testEnv, testClusterName, testNamespaceName)) .thenReturn(createNamespace(testAppId, testClusterName, testNamespaceName)); when(appNamespaceService.findByAppIdAndName(testAppId, testNamespaceName)) .thenReturn(createAppNamespace(testAppId, testNamespaceName, false)); NamespaceBO namespaceBO = namespaceService.loadNamespaceBO(testAppId, testEnv, testClusterName, testNamespaceName, true, false); assertNotNull(namespaceBO); assertEquals(testAppId, namespaceBO.getBaseInfo().getAppId()); assertEquals(testClusterName, namespaceBO.getBaseInfo().getClusterName()); assertEquals(testNamespaceName, namespaceBO.getBaseInfo().getNamespaceName()); assertEquals(2, namespaceBO.getItems().size()); verify(itemService, times(0)).findDeletedItems(any(), any(), any(), any()); verify(additionalUserInfoEnrichService, times(1)).enrichAdditionalUserInfo(any(), any()); } @Test public void testLoadNamespaceBONamespaceNotFound() { when(namespaceAPI.loadNamespace(testAppId, testEnv, testClusterName, testNamespaceName)).thenReturn(null); assertThatExceptionOfType(BadRequestException.class) .isThrownBy(() -> namespaceService.loadNamespaceBO(testAppId, testEnv, testClusterName, testNamespaceName)); } @Test public void testLoadNamespaceBONoLatestRelease() { when(namespaceAPI.loadNamespace(testAppId, testEnv, testClusterName, testNamespaceName)).thenReturn(createNamespace(testAppId, testClusterName, testNamespaceName)); when(releaseService.loadLatestRelease(testAppId, testEnv, testClusterName, testNamespaceName)).thenReturn(null); when(appNamespaceService.findByAppIdAndName(testAppId, testNamespaceName)).thenReturn(createAppNamespace(testAppId, testNamespaceName, false)); when(itemService.findItems(testAppId, testEnv, testClusterName, testNamespaceName)).thenReturn(createItems()); NamespaceBO namespaceBO = namespaceService.loadNamespaceBO(testAppId, testEnv, testClusterName, testNamespaceName); assertNotNull(namespaceBO); assertEquals(2, namespaceBO.getItems().size()); assertTrue(namespaceBO.getItems().get(0).isModified()); assertTrue(namespaceBO.getItems().get(1).isModified()); } @Test public void testLoadNamespaceBONoItems() { ReleaseDTO releaseDTO = createReleaseDTO(); when(releaseService.loadLatestRelease(testAppId, testEnv, testClusterName, testNamespaceName)) .thenReturn(releaseDTO); when(namespaceAPI.loadNamespace(testAppId, testEnv, testClusterName, testNamespaceName)) .thenReturn(createNamespace(testAppId, testClusterName, testNamespaceName)); when(itemService.findItems(testAppId, testEnv, testClusterName, testNamespaceName)) .thenReturn(Lists.newArrayList()); when(appNamespaceService.findByAppIdAndName(testAppId, testNamespaceName)) .thenReturn(createAppNamespace(testAppId, testNamespaceName, false)); ItemDTO deletedItemDTO = new ItemDTO(); deletedItemDTO.setKey("deleted-key"); when(itemService.findDeletedItems(testAppId, testEnv, testClusterName, testNamespaceName)) .thenReturn(Lists.newArrayList(deletedItemDTO)); NamespaceBO namespaceBO = namespaceService.loadNamespaceBO(testAppId, testEnv, testClusterName, testNamespaceName); assertNotNull(namespaceBO); assertEquals(3, namespaceBO.getItems().size()); assertTrue(namespaceBO.getItems().get(0).isDeleted()); assertEquals("k1", namespaceBO.getItems().get(0).getItem().getKey()); } @Test public void testLoadNamespaceBOWithPublicNamespace() { AppNamespace publicAppNamespace = createAppNamespace("public-app", testNamespaceName, true); when(namespaceAPI.loadNamespace(testAppId, testEnv, testClusterName, testNamespaceName)) .thenReturn(createNamespace(testAppId, testClusterName, testNamespaceName)); when(appNamespaceService.findByAppIdAndName(testAppId, testNamespaceName)).thenReturn(null); when(appNamespaceService.findPublicAppNamespace(testNamespaceName)) .thenReturn(publicAppNamespace); ReleaseDTO releaseDTO = createReleaseDTO(); when(releaseService.loadLatestRelease(testAppId, testEnv, testClusterName, testNamespaceName)) .thenReturn(releaseDTO); when(itemService.findItems(testAppId, testEnv, testClusterName, testNamespaceName)) .thenReturn(createItems()); NamespaceBO namespaceBO = namespaceService.loadNamespaceBO(testAppId, testEnv, testClusterName, testNamespaceName); assertNotNull(namespaceBO); assertTrue(namespaceBO.isPublic()); assertEquals("public-app", namespaceBO.getParentAppId()); verify(appNamespaceService, times(1)).findPublicAppNamespace(testNamespaceName); } @Test public void testLoadNamespaceBOWithPrivateNamespace() { AppNamespace privateAppNamespace = createAppNamespace(testAppId, testNamespaceName, false); when(namespaceAPI.loadNamespace(testAppId, testEnv, testClusterName, testNamespaceName)) .thenReturn(createNamespace(testAppId, testClusterName, testNamespaceName)); when(appNamespaceService.findByAppIdAndName(testAppId, testNamespaceName)) .thenReturn(privateAppNamespace); ReleaseDTO releaseDTO = createReleaseDTO(); when(releaseService.loadLatestRelease(testAppId, testEnv, testClusterName, testNamespaceName)) .thenReturn(releaseDTO); when(itemService.findItems(testAppId, testEnv, testClusterName, testNamespaceName)) .thenReturn(createItems()); NamespaceBO namespaceBO = namespaceService.loadNamespaceBO(testAppId, testEnv, testClusterName, testNamespaceName); assertNotNull(namespaceBO); assertEquals(testAppId, namespaceBO.getParentAppId()); } @Test public void testLoadNamespaceBOWithDirtyAppNamespace() { when(namespaceAPI.loadNamespace(testAppId, testEnv, testClusterName, testNamespaceName)).thenReturn(createNamespace(testAppId, testClusterName, testNamespaceName)); when(appNamespaceService.findByAppIdAndName(testAppId, testNamespaceName)).thenReturn(null); when(appNamespaceService.findPublicAppNamespace(testNamespaceName)).thenReturn(null); ReleaseDTO releaseDTO = createReleaseDTO(); when(releaseService.loadLatestRelease(testAppId, testEnv, testClusterName, testNamespaceName)).thenReturn(releaseDTO); when(itemService.findItems(testAppId, testEnv, testClusterName, testNamespaceName)).thenReturn(createItems()); NamespaceBO namespaceBO = namespaceService.loadNamespaceBO(testAppId, testEnv, testClusterName, testNamespaceName); assertNotNull(namespaceBO); assertTrue(namespaceBO.isPublic()); } @Test public void testLoadNamespaceBOItemModifiedCountCalculation() { ReleaseDTO releaseDTO = createReleaseDTO(); when(namespaceAPI.loadNamespace(testAppId, testEnv, testClusterName, testNamespaceName)) .thenReturn(createNamespace(testAppId, testClusterName, testNamespaceName)); when(releaseService.loadLatestRelease(testAppId, testEnv, testClusterName, testNamespaceName)) .thenReturn(releaseDTO); when(appNamespaceService.findByAppIdAndName(testAppId, testNamespaceName)) .thenReturn(createAppNamespace(testAppId, testNamespaceName, false)); List itemDTOList = createItems(); when(itemService.findItems(testAppId, testEnv, testClusterName, testNamespaceName)) .thenReturn(itemDTOList); ItemDTO deletedItemDTO = new ItemDTO(); deletedItemDTO.setKey("deleted-key"); when(itemService.findDeletedItems(testAppId, testEnv, testClusterName, testNamespaceName)) .thenReturn(Lists.newArrayList(deletedItemDTO)); NamespaceBO namespaceBO = namespaceService.loadNamespaceBO(testAppId, testEnv, testClusterName, testNamespaceName, true, true); assertNotNull(namespaceBO); assertEquals(3, namespaceBO.getItemModifiedCnt()); } private ReleaseDTO createReleaseDTO() { ReleaseDTO releaseDTO = new ReleaseDTO(); releaseDTO.setConfigurations("{\"k1\":\"k1\",\"k2\":\"k2\", \"k3\":\"\"}"); return releaseDTO; } private List createItems() { List itemDTOList = Lists.newArrayList(); ItemDTO itemDTO1 = new ItemDTO(); itemDTO1.setId(1); itemDTO1.setNamespaceId(1); itemDTO1.setKey("k1"); itemDTO1.setValue(String.valueOf(1)); itemDTOList.add(itemDTO1); ItemDTO itemDTO2 = new ItemDTO(); itemDTO2.setId(2); itemDTO2.setNamespaceId(2); itemDTO2.setKey("k2"); itemDTO2.setValue(String.valueOf(2)); itemDTOList.add(itemDTO2); return itemDTOList; } private List createDeletedItems() { List deletedItemDTOList = Lists.newArrayList(); ItemDTO deletedItemDTO = new ItemDTO(); deletedItemDTO.setId(3); deletedItemDTO.setNamespaceId(3); deletedItemDTO.setKey("k3"); deletedItemDTOList.add(deletedItemDTO); return deletedItemDTOList; } private AppNamespace createAppNamespace(String appId, String name, boolean isPublic) { AppNamespace instance = new AppNamespace(); instance.setAppId(appId); instance.setName(name); instance.setPublic(isPublic); return instance; } private NamespaceDTO createNamespace(String appId, String clusterName, String namespaceName) { NamespaceDTO instance = new NamespaceDTO(); instance.setAppId(appId); instance.setClusterName(clusterName); instance.setNamespaceName(namespaceName); return instance; } private UserInfo createUser(String userId) { UserInfo instance = new UserInfo(); instance.setUserId(userId); return instance; } } ================================================ FILE: apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/spi/defaultImpl/RoleInitializationServiceTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.spi.defaultImpl; import com.ctrip.framework.apollo.common.entity.App; import com.ctrip.framework.apollo.portal.environment.Env; import com.ctrip.framework.apollo.portal.AbstractUnitTest; import com.ctrip.framework.apollo.portal.component.config.PortalConfig; import com.ctrip.framework.apollo.portal.constant.PermissionType; import com.ctrip.framework.apollo.portal.entity.bo.UserInfo; import com.ctrip.framework.apollo.portal.entity.po.Permission; import com.ctrip.framework.apollo.portal.entity.po.Role; import com.ctrip.framework.apollo.portal.service.RolePermissionService; import com.ctrip.framework.apollo.portal.spi.UserInfoHolder; import com.ctrip.framework.apollo.portal.spi.defaultimpl.DefaultRoleInitializationService; import com.ctrip.framework.apollo.portal.util.RoleUtils; import com.google.common.collect.Sets; import org.junit.Test; import org.mockito.InjectMocks; import org.mockito.Mock; import java.util.ArrayList; import java.util.List; import static org.mockito.Mockito.*; public class RoleInitializationServiceTest extends AbstractUnitTest { private final String APP_ID = "1000"; private final String APP_NAME = "app-test"; private final String ENV = "DEV"; private final String CLUSTER = "cluster-test"; private final String NAMESPACE = "namespace-test"; private final String CURRENT_USER = "user"; @Mock private RolePermissionService rolePermissionService; @Mock private UserInfoHolder userInfoHolder; @Mock private PortalConfig portalConfig; @InjectMocks private DefaultRoleInitializationService roleInitializationService; @Test public void testInitAppRoleHasInitBefore(){ when(rolePermissionService.findRoleByRoleName(anyString())).thenReturn(mockRole(RoleUtils.buildAppMasterRoleName(APP_ID))); roleInitializationService.initAppRoles(mockApp()); verify(rolePermissionService, times(1)).findRoleByRoleName(RoleUtils.buildAppMasterRoleName(APP_ID)); verify(rolePermissionService, times(0)).assignRoleToUsers(anyString(), anySet(), anyString()); } @Test public void testInitAppRole(){ when(rolePermissionService.findRoleByRoleName(anyString())).thenReturn(null); when(userInfoHolder.getUser()).thenReturn(mockUser()); when(rolePermissionService.createPermission(any())).thenReturn(mockPermission()); when(portalConfig.portalSupportedEnvs()).thenReturn(mockPortalSupportedEnvs()); roleInitializationService.initAppRoles(mockApp()); verify(rolePermissionService, times(7)).findRoleByRoleName(anyString()); verify(rolePermissionService, times(1)).assignRoleToUsers( RoleUtils.buildAppMasterRoleName(APP_ID), Sets.newHashSet(CURRENT_USER), CURRENT_USER); verify(rolePermissionService, times(7)).createPermission(any()); verify(rolePermissionService, times(8)).createRoleWithPermissions(any(), anySet()); } @Test public void testInitNamespaceRoleHasExisted() { String modifyNamespaceRoleName = RoleUtils.buildModifyNamespaceRoleName(APP_ID, NAMESPACE); when(rolePermissionService.findRoleByRoleName(modifyNamespaceRoleName)) .thenReturn(mockRole(modifyNamespaceRoleName)); String releaseNamespaceRoleName = RoleUtils.buildReleaseNamespaceRoleName(APP_ID, NAMESPACE); when(rolePermissionService.findRoleByRoleName(releaseNamespaceRoleName)) .thenReturn(mockRole(releaseNamespaceRoleName)); roleInitializationService.initNamespaceRoles(APP_ID, NAMESPACE, CURRENT_USER); verify(rolePermissionService, times(2)).findRoleByRoleName(anyString()); verify(rolePermissionService, times(0)).createPermission(any()); verify(rolePermissionService, times(0)).createRoleWithPermissions(any(), anySet()); } @Test public void testInitNamespaceRoleNotExisted() { String modifyNamespaceRoleName = RoleUtils.buildModifyNamespaceRoleName(APP_ID, NAMESPACE); when(rolePermissionService.findRoleByRoleName(modifyNamespaceRoleName)).thenReturn(null); String releaseNamespaceRoleName = RoleUtils.buildReleaseNamespaceRoleName(APP_ID, NAMESPACE); when(rolePermissionService.findRoleByRoleName(releaseNamespaceRoleName)).thenReturn(null); when(userInfoHolder.getUser()).thenReturn(mockUser()); when(rolePermissionService.createPermission(any())).thenReturn(mockPermission()); roleInitializationService.initNamespaceRoles(APP_ID, NAMESPACE, CURRENT_USER); verify(rolePermissionService, times(2)).findRoleByRoleName(anyString()); verify(rolePermissionService, times(2)).createPermission(any()); verify(rolePermissionService, times(2)).createRoleWithPermissions(any(), anySet()); } @Test public void testInitNamespaceRoleModifyNSExisted() { String modifyNamespaceRoleName = RoleUtils.buildModifyNamespaceRoleName(APP_ID, NAMESPACE); when(rolePermissionService.findRoleByRoleName(modifyNamespaceRoleName)) .thenReturn(mockRole(modifyNamespaceRoleName)); String releaseNamespaceRoleName = RoleUtils.buildReleaseNamespaceRoleName(APP_ID, NAMESPACE); when(rolePermissionService.findRoleByRoleName(releaseNamespaceRoleName)).thenReturn(null); when(userInfoHolder.getUser()).thenReturn(mockUser()); when(rolePermissionService.createPermission(any())).thenReturn(mockPermission()); roleInitializationService.initNamespaceRoles(APP_ID, NAMESPACE, CURRENT_USER); verify(rolePermissionService, times(2)).findRoleByRoleName(anyString()); verify(rolePermissionService, times(1)).createPermission(any()); verify(rolePermissionService, times(1)).createRoleWithPermissions(any(), anySet()); } @Test public void testInitClusterNsRole() { String modifyNamespacesInClusterRoleName = RoleUtils.buildModifyNamespacesInClusterRoleName(APP_ID, ENV, CLUSTER); when(rolePermissionService.findRoleByRoleName(modifyNamespacesInClusterRoleName)) .thenReturn(null); String releaseNamespacesInClusterRoleName = RoleUtils.buildReleaseNamespacesInClusterRoleName(APP_ID, ENV, CLUSTER); when(rolePermissionService.findRoleByRoleName(releaseNamespacesInClusterRoleName)) .thenReturn(null); when(userInfoHolder.getUser()).thenReturn(mockUser()); when(rolePermissionService.createPermission(any())).thenReturn(mockPermission()); roleInitializationService.initClusterNamespaceRoles(APP_ID, ENV, CLUSTER, CURRENT_USER); verify(rolePermissionService, times(2)).findRoleByRoleName(anyString()); verify(rolePermissionService, times(2)).createPermission(any()); verify(rolePermissionService, times(2)).createRoleWithPermissions(any(), anySet()); } @Test public void testInitClusterNsRoleHasExisted() { String modifyNamespacesInClusterRoleName = RoleUtils.buildModifyNamespacesInClusterRoleName(APP_ID, ENV, CLUSTER); when(rolePermissionService.findRoleByRoleName(modifyNamespacesInClusterRoleName)) .thenReturn(mockRole(modifyNamespacesInClusterRoleName)); String releaseNamespacesInClusterRoleName = RoleUtils.buildReleaseNamespacesInClusterRoleName(APP_ID, ENV, CLUSTER); when(rolePermissionService.findRoleByRoleName(releaseNamespacesInClusterRoleName)) .thenReturn(mockRole(releaseNamespacesInClusterRoleName)); roleInitializationService.initClusterNamespaceRoles(APP_ID, ENV, CLUSTER, CURRENT_USER); verify(rolePermissionService, times(2)).findRoleByRoleName(anyString()); verify(rolePermissionService, times(0)).createPermission(any()); verify(rolePermissionService, times(0)).createRoleWithPermissions(any(), anySet()); } @Test public void testInitClusterNsRoleModifyNamespacesInClusterExisted() { String modifyNamespacesInClusterRoleName = RoleUtils.buildModifyNamespacesInClusterRoleName(APP_ID, ENV, CLUSTER); when(rolePermissionService.findRoleByRoleName(modifyNamespacesInClusterRoleName)) .thenReturn(mockRole(modifyNamespacesInClusterRoleName)); String releaseNamespacesInClusterRoleName = RoleUtils.buildReleaseNamespacesInClusterRoleName(APP_ID, ENV, CLUSTER); when(rolePermissionService.findRoleByRoleName(releaseNamespacesInClusterRoleName)) .thenReturn(null); when(userInfoHolder.getUser()).thenReturn(mockUser()); when(rolePermissionService.createPermission(any())).thenReturn(mockPermission()); roleInitializationService.initClusterNamespaceRoles(APP_ID, ENV, CLUSTER, CURRENT_USER); verify(rolePermissionService, times(2)).findRoleByRoleName(anyString()); verify(rolePermissionService, times(1)).createPermission(any()); verify(rolePermissionService, times(1)).createRoleWithPermissions(any(), anySet()); } @Test public void testInitClusterNsRoleReleaseNamespacesInClusterExisted() { String modifyNamespacesInClusterRoleName = RoleUtils.buildModifyNamespacesInClusterRoleName(APP_ID, ENV, CLUSTER); when(rolePermissionService.findRoleByRoleName(modifyNamespacesInClusterRoleName)) .thenReturn(null); String releaseNamespacesInClusterRoleName = RoleUtils.buildReleaseNamespacesInClusterRoleName(APP_ID, ENV, CLUSTER); when(rolePermissionService.findRoleByRoleName(releaseNamespacesInClusterRoleName)) .thenReturn(mockRole(releaseNamespacesInClusterRoleName)); when(userInfoHolder.getUser()).thenReturn(mockUser()); when(rolePermissionService.createPermission(any())).thenReturn(mockPermission()); roleInitializationService.initClusterNamespaceRoles(APP_ID, ENV, CLUSTER, CURRENT_USER); verify(rolePermissionService, times(2)).findRoleByRoleName(anyString()); verify(rolePermissionService, times(1)).createPermission(any()); verify(rolePermissionService, times(1)).createRoleWithPermissions(any(), anySet()); } private App mockApp() { App app = new App(); app.setAppId(APP_ID); app.setName(APP_NAME); app.setOrgName("xx"); app.setOrgId("1"); app.setOwnerName(CURRENT_USER); app.setDataChangeCreatedBy(CURRENT_USER); return app; } private Role mockRole(String roleName) { Role role = new Role(); role.setRoleName(roleName); return role; } private UserInfo mockUser() { UserInfo userInfo = new UserInfo(); userInfo.setUserId(CURRENT_USER); return userInfo; } private Permission mockPermission() { Permission permission = new Permission(); permission.setPermissionType(PermissionType.MODIFY_NAMESPACE); permission.setTargetId(RoleUtils.buildNamespaceTargetId(APP_ID, NAMESPACE)); return permission; } private List mockPortalSupportedEnvs() { List envArray = new ArrayList<>(); envArray.add(Env.DEV); envArray.add(Env.FAT); return envArray; } } ================================================ FILE: apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/spi/defaultImpl/RolePermissionServiceTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.spi.defaultImpl; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.junit.jupiter.api.Assertions.assertNull; import com.ctrip.framework.apollo.common.entity.BaseEntity; import com.ctrip.framework.apollo.portal.AbstractIntegrationTest; import com.ctrip.framework.apollo.portal.entity.bo.UserInfo; import com.ctrip.framework.apollo.portal.entity.po.Permission; import com.ctrip.framework.apollo.portal.entity.po.Role; import com.ctrip.framework.apollo.portal.entity.po.RolePermission; import com.ctrip.framework.apollo.portal.entity.po.UserRole; import com.ctrip.framework.apollo.portal.repository.PermissionRepository; import com.ctrip.framework.apollo.portal.repository.RolePermissionRepository; import com.ctrip.framework.apollo.portal.repository.RoleRepository; import com.ctrip.framework.apollo.portal.repository.UserRoleRepository; import com.ctrip.framework.apollo.portal.service.RolePermissionService; import com.ctrip.framework.apollo.portal.util.RoleUtils; import com.google.common.collect.Sets; import java.util.Collections; import java.util.List; import java.util.Set; import java.util.stream.Collectors; import org.assertj.core.api.Assertions; import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.jdbc.Sql; /** * @author Jason Song(song_s@ctrip.com) */ public class RolePermissionServiceTest extends AbstractIntegrationTest { @Autowired private RolePermissionService rolePermissionService; @Autowired private RoleRepository roleRepository; @Autowired private RolePermissionRepository rolePermissionRepository; @Autowired private UserRoleRepository userRoleRepository; @Autowired private PermissionRepository permissionRepository; private String someCreatedBy; private String someLastModifiedBy; @Before public void setUp() throws Exception { someCreatedBy = "someCreatedBy"; someLastModifiedBy = "someLastModifiedBy"; } @Test @Sql(scripts = "/sql/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testCreatePermission() throws Exception { String someTargetId = "someTargetId"; String somePermissionType = "somePermissionType"; Permission somePermission = assemblePermission(somePermissionType, someTargetId); Permission created = rolePermissionService.createPermission(somePermission); Permission createdFromDB = permissionRepository.findById(created.getId()).orElse(null); assertEquals(somePermissionType, createdFromDB.getPermissionType()); assertEquals(someTargetId, createdFromDB.getTargetId()); } @Test(expected = IllegalStateException.class) @Sql(scripts = "/sql/permission/insert-test-permissions.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/sql/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testCreatePermissionWithPermissionExisted() throws Exception { String someTargetId = "someTargetId"; String somePermissionType = "somePermissionType"; Permission somePermission = assemblePermission(somePermissionType, someTargetId); rolePermissionService.createPermission(somePermission); } @Test @Sql(scripts = "/sql/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testCreatePermissions() throws Exception { String someTargetId = "someTargetId"; String anotherTargetId = "anotherTargetId"; String somePermissionType = "somePermissionType"; String anotherPermissionType = "anotherPermissionType"; Permission somePermission = assemblePermission(somePermissionType, someTargetId); Permission anotherPermission = assemblePermission(anotherPermissionType, anotherTargetId); Set created = rolePermissionService.createPermissions(Sets.newHashSet(somePermission, anotherPermission)); Set permissionIds = created.stream().map(BaseEntity::getId).collect(Collectors.toSet()); Iterable permissionsFromDB = permissionRepository.findAllById(permissionIds); Set targetIds = Sets.newHashSet(); Set permissionTypes = Sets.newHashSet(); for (Permission permission : permissionsFromDB) { targetIds.add(permission.getTargetId()); permissionTypes.add(permission.getPermissionType()); } assertEquals(2, targetIds.size()); assertEquals(2, permissionTypes.size()); assertTrue(targetIds.containsAll(Sets.newHashSet(someTargetId, anotherTargetId))); assertTrue( permissionTypes.containsAll(Sets.newHashSet(somePermissionType, anotherPermissionType))); } @Test(expected = IllegalStateException.class) @Sql(scripts = "/sql/permission/insert-test-permissions.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/sql/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testCreatePermissionsWithPermissionsExisted() throws Exception { String someTargetId = "someTargetId"; String anotherTargetId = "anotherTargetId"; String somePermissionType = "somePermissionType"; String anotherPermissionType = "anotherPermissionType"; Permission somePermission = assemblePermission(somePermissionType, someTargetId); Permission anotherPermission = assemblePermission(anotherPermissionType, anotherTargetId); rolePermissionService.createPermissions(Sets.newHashSet(somePermission, anotherPermission)); } @Test @Sql(scripts = "/sql/permission/insert-test-permissions.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/sql/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testCreateRoleWithPermissions() throws Exception { String someRoleName = "someRoleName"; Role role = assembleRole(someRoleName); Set permissionIds = Sets.newHashSet(990L, 991L); Role created = rolePermissionService.createRoleWithPermissions(role, permissionIds); Role createdFromDB = roleRepository.findById(created.getId()).orElse(null); List rolePermissions = rolePermissionRepository.findByRoleIdIn(Sets.newHashSet(createdFromDB.getId())); Set rolePermissionIds = rolePermissions.stream().map(RolePermission::getPermissionId).collect(Collectors.toSet()); assertEquals(someRoleName, createdFromDB.getRoleName()); assertEquals(2, rolePermissionIds.size()); assertTrue(rolePermissionIds.containsAll(permissionIds)); } @Test(expected = IllegalStateException.class) @Sql(scripts = "/sql/permission/insert-test-roles.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/sql/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testCreateRoleWithPermissionsWithRoleExisted() throws Exception { String someRoleName = "someRoleName"; Role role = assembleRole(someRoleName); rolePermissionService.createRoleWithPermissions(role, null); } @Test @Sql(scripts = "/sql/permission/insert-test-roles.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/sql/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testAssignRoleToUsers() throws Exception { String someRoleName = "someRoleName"; String someUser = "someUser"; String anotherUser = "anotherUser"; String operator = "operator"; Set users = Sets.newHashSet(someUser, anotherUser); rolePermissionService.assignRoleToUsers(someRoleName, users, operator); List userRoles = userRoleRepository.findByRoleId(990); Set usersWithRole = Sets.newHashSet(); for (UserRole userRole : userRoles) { assertEquals(operator, userRole.getDataChangeCreatedBy()); assertEquals(operator, userRole.getDataChangeLastModifiedBy()); usersWithRole.add(userRole.getUserId()); } assertEquals(2, usersWithRole.size()); assertTrue(usersWithRole.containsAll(users)); } @Test(expected = IllegalStateException.class) @Sql(scripts = "/sql/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testAssignRoleToUsersWithRoleNotExists() throws Exception { String someRoleName = "someRoleName"; String someUser = "someUser"; String operator = "operator"; Set users = Sets.newHashSet(someUser); rolePermissionService.assignRoleToUsers(someRoleName, users, operator); } @Test @Sql(scripts = "/sql/permission/insert-test-roles.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/sql/permission/insert-test-userroles.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/sql/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testAssignRoleToUsersWithUserRolesExisted() throws Exception { String someRoleName = "someRoleName"; String someUser = "someUser"; String anotherUser = "anotherUser"; String operator = "operator"; Set users = Sets.newHashSet(someUser, anotherUser); rolePermissionService.assignRoleToUsers(someRoleName, users, operator); List userRoles = userRoleRepository.findByRoleId(990); Set usersWithRole = Sets.newHashSet(); for (UserRole userRole : userRoles) { assertEquals("someOperator", userRole.getDataChangeCreatedBy()); assertEquals("someOperator", userRole.getDataChangeLastModifiedBy()); usersWithRole.add(userRole.getUserId()); } assertEquals(2, usersWithRole.size()); assertTrue(usersWithRole.containsAll(users)); } @Test @Sql(scripts = "/sql/permission/insert-test-roles.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/sql/permission/insert-test-userroles.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/sql/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testRemoveRoleFromUsers() throws Exception { String someRoleName = "someRoleName"; String someUser = "someUser"; String anotherUser = "anotherUser"; String operator = "operator"; Set users = Sets.newHashSet(someUser, anotherUser); List userRoles = userRoleRepository.findByRoleId(990); assertFalse(userRoles.isEmpty()); rolePermissionService.removeRoleFromUsers(someRoleName, users, operator); List userRolesAfterRemoval = userRoleRepository.findByRoleId(990); assertTrue(userRolesAfterRemoval.isEmpty()); } @Test(expected = IllegalStateException.class) @Sql(scripts = "/sql/permission/insert-test-userroles.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/sql/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testRemoveRoleFromUsersWithRoleNotExisted() throws Exception { String someRoleName = "someRoleName"; String someUser = "someUser"; String operator = "operator"; Set users = Sets.newHashSet(someUser); rolePermissionService.removeRoleFromUsers(someRoleName, users, operator); } @Test @Sql(scripts = "/sql/permission/insert-test-roles.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/sql/permission/insert-test-userroles.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/sql/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testQueryUsersWithRole() throws Exception { String roleName = "someRoleName"; Set users = rolePermissionService.queryUsersWithRole(roleName); Assertions.assertThat(users).isEmpty(); roleName = "apolloRoleName"; users = rolePermissionService.queryUsersWithRole(roleName); Set userIds = users.stream().map(UserInfo::getUserId).collect(Collectors.toSet()); assertTrue(userIds.containsAll(Sets.newHashSet("apollo"))); } @Test @Sql(scripts = "/sql/permission/insert-test-roles.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/sql/permission/insert-test-permissions.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/sql/permission/insert-test-userroles.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/sql/permission/insert-test-rolepermissions.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/sql/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testUserHasPermission() throws Exception { String someTargetId = "someTargetId"; String anotherTargetId = "anotherTargetId"; String somePermissionType = "somePermissionType"; String anotherPermissionType = "anotherPermissionType"; String someUser = "someUser"; String anotherUser = "anotherUser"; String someUserWithNoPermission = "someUserWithNoPermission"; assertTrue(rolePermissionService.userHasPermission(someUser, somePermissionType, someTargetId)); assertTrue( rolePermissionService.userHasPermission(someUser, anotherPermissionType, anotherTargetId)); assertTrue( rolePermissionService.userHasPermission(anotherUser, somePermissionType, someTargetId)); assertTrue(rolePermissionService.userHasPermission(anotherUser, anotherPermissionType, anotherTargetId)); assertFalse(rolePermissionService.userHasPermission(someUserWithNoPermission, somePermissionType, someTargetId)); assertFalse(rolePermissionService.userHasPermission(someUserWithNoPermission, anotherPermissionType, anotherTargetId)); } @Test @Sql(scripts = "/sql/permission/insert-test-permissions.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/sql/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testHasAnyPermissionIncludesSuperAdmin() { List requiredPermissions = Collections.singletonList(new Permission("somePermissionType", "someTargetId")); String previousSuperAdmin = System.getProperty("superAdmin"); try { System.setProperty("superAdmin", "apollo"); assertTrue(rolePermissionService.hasAnyPermission("apollo", requiredPermissions)); assertFalse( rolePermissionService.hasAnyPermission("someUserWithNoPermission", requiredPermissions)); } finally { if (previousSuperAdmin == null) { System.clearProperty("superAdmin"); } else { System.setProperty("superAdmin", previousSuperAdmin); } } } @Test @Sql( scripts = "/sql/permission/RolePermissionServiceTest.deleteRolePermissionsByAppIdWithClusterRoles.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/sql/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testDeleteRolePermissionsByAppIdWithClusterRoles() { String appId = "clusterApp"; String operator = "test"; rolePermissionService.deleteRolePermissionsByAppId(appId, operator); String modifyRoleName = RoleUtils.buildModifyNamespacesInClusterRoleName(appId, "DEV", "default"); String releaseRoleName = RoleUtils.buildReleaseNamespacesInClusterRoleName(appId, "DEV", "default"); assertNull(roleRepository.findTopByRoleName(modifyRoleName)); assertNull(roleRepository.findTopByRoleName(releaseRoleName)); } private Role assembleRole(String roleName) { Role role = new Role(); role.setRoleName(roleName); role.setDataChangeCreatedBy(someCreatedBy); role.setDataChangeLastModifiedBy(someLastModifiedBy); return role; } private Permission assemblePermission(String permissionType, String targetId) { Permission permission = new Permission(); permission.setPermissionType(permissionType); permission.setTargetId(targetId); permission.setDataChangeCreatedBy(someCreatedBy); permission.setDataChangeLastModifiedBy(someCreatedBy); return permission; } } ================================================ FILE: apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/util/AuthUserPasswordCheckerTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.util; import com.ctrip.framework.apollo.portal.component.config.PortalConfig; import com.ctrip.framework.apollo.portal.service.PortalDBPropertySource; import com.ctrip.framework.apollo.portal.util.checker.AuthUserPasswordChecker; import com.ctrip.framework.apollo.portal.util.checker.CheckResult; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import org.junit.Assert; import org.junit.Test; import org.mockito.Mockito; public class AuthUserPasswordCheckerTest { @Test public void testRegexMatch() { PortalConfig mock = Mockito.mock(PortalConfig.class); AuthUserPasswordChecker checker = new AuthUserPasswordChecker(mock); List unMatchList = Arrays.asList("11111111", "oibjdiel", "oso87b6", "0vb9xibowkd8bz9dsxbef", "", null); String exceptedErrMsg = "Password needs a number and letter and between 8~20 characters"; for (String p : unMatchList) { CheckResult res = checker.checkWeakPassword(p); Assert.assertFalse(res.isSuccess()); Assert.assertEquals(exceptedErrMsg, res.getMessage()); } List matchList = Arrays.asList("pziv0g87", "8f7zjpf8sci93", "Upz4jF8u2yjV3wn8zp6c"); for (String p : matchList) { CheckResult res = checker.checkWeakPassword(p); Assert.assertTrue(res.isSuccess()); } } @Test public void testIsWeakPassword() { // use default PortalDBPropertySource propertySource = Mockito.mock(PortalDBPropertySource.class); PortalConfig mock = new PortalConfig(propertySource); AuthUserPasswordChecker checker = new AuthUserPasswordChecker(mock); Map cases = new HashMap<>(); cases.put("a1234567", false); cases.put("b98765432", false); cases.put("c11111111", false); cases.put("d2222222", false); cases.put("e3333333", false); cases.put("f4444444", false); cases.put("g5555555", false); cases.put("h6666666", false); cases.put("i7777777", false); cases.put("j8888888", false); cases.put("k9999999", false); cases.put("l0000000", false); cases.put("1q2w3e4r", false); cases.put("qwertyuiop1", false); cases.put("asdfghjkl2", false); cases.put("asdfghjkl3", false); cases.put("abcd1234", false); cases.put("1s39gvisk", true); String exceptedErrMsg = "Passwords cannot be consecutive, regular letters or numbers. And cannot be commonly used."; for (Entry c : cases.entrySet()) { CheckResult res = checker.checkWeakPassword(c.getKey()); Assert.assertEquals(res.isSuccess(), c.getValue()); if (!c.getValue()) { Assert.assertTrue(res.getMessage().startsWith(exceptedErrMsg)); } } } @Test public void testIsWeakPassword2() { // use custom PortalConfig mock = Mockito.mock(PortalConfig.class); Mockito.when(mock.getUserPasswordNotAllowList()).thenReturn(Arrays.asList("1111", "2222")); AuthUserPasswordChecker checker = new AuthUserPasswordChecker(mock); Map cases = new HashMap<>(); cases.put("a1234567", true); cases.put("b98765432", true); cases.put("c11111111", false); cases.put("d2222222", false); cases.put("e3333333", true); String exceptedErrMsg = "Passwords cannot be consecutive, regular letters or numbers. And cannot be commonly used."; for (Entry c : cases.entrySet()) { CheckResult res = checker.checkWeakPassword(c.getKey()); Assert.assertEquals(res.isSuccess(), c.getValue()); if (!c.getValue()) { Assert.assertTrue(res.getMessage().startsWith(exceptedErrMsg)); } } } @Test public void testIsWeakPassword3() { // no limit PortalConfig mock = Mockito.mock(PortalConfig.class); Mockito.when(mock.getUserPasswordNotAllowList()).thenReturn(Collections.emptyList()); AuthUserPasswordChecker checker = new AuthUserPasswordChecker(mock); Map cases = new HashMap<>(); cases.put("a1234567", true); cases.put("b98765432", true); cases.put("c11111111", true); cases.put("d2222222", true); cases.put("e3333333", true); for (Entry c : cases.entrySet()) { CheckResult res = checker.checkWeakPassword(c.getKey()); Assert.assertEquals(res.isSuccess(), c.getValue()); } Mockito.when(mock.getUserPasswordNotAllowList()).thenReturn(null); checker = new AuthUserPasswordChecker(mock); for (Entry c : cases.entrySet()) { CheckResult res = checker.checkWeakPassword(c.getKey()); Assert.assertEquals(res.isSuccess(), c.getValue()); } } } ================================================ FILE: apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/util/ConfigFileUtilsTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.util; import static org.junit.Assert.*; import com.ctrip.framework.apollo.common.exception.BadRequestException; import com.ctrip.framework.apollo.core.enums.ConfigFileFormat; import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class ConfigFileUtilsTest { private final Logger logger = LoggerFactory.getLogger(this.getClass()); @Test public void checkFormat() { ConfigFileUtils.checkFormat("1234+default+app.properties"); ConfigFileUtils.checkFormat("1234+default+app.yml"); ConfigFileUtils.checkFormat("1234+default+app.json"); } @Test(expected = BadRequestException.class) public void checkFormatWithException0() { ConfigFileUtils.checkFormat("1234+defaultes"); } @Test(expected = BadRequestException.class) public void checkFormatWithException1() { ConfigFileUtils.checkFormat(".json"); } @Test(expected = BadRequestException.class) public void checkFormatWithException2() { ConfigFileUtils.checkFormat("application."); } @Test public void getFormat() { final String properties = ConfigFileUtils.getFormat("application+default+application.properties"); assertEquals("properties", properties); final String yml = ConfigFileUtils.getFormat("application+default+application.yml"); assertEquals("yml", yml); } @Test public void getAppId() { final String application = ConfigFileUtils.getAppId("application+default+application.properties"); assertEquals("application", application); final String abc = ConfigFileUtils.getAppId("abc+default+application.yml"); assertEquals("abc", abc); } @Test public void getClusterName() { final String cluster = ConfigFileUtils.getClusterName("application+default+application.properties"); assertEquals("default", cluster); final String Beijing = ConfigFileUtils.getClusterName("abc+Beijing+application.yml"); assertEquals("Beijing", Beijing); } @Test public void getNamespace() { final String application = ConfigFileUtils.getNamespace("234+default+application.properties"); assertEquals("application", application); final String applicationYml = ConfigFileUtils.getNamespace("abc+default+application.yml"); assertEquals("application.yml", applicationYml); } @Test public void toFilename() { final String propertiesFilename0 = ConfigFileUtils.toFilename("123", "default", "application", ConfigFileFormat.Properties); logger.info("propertiesFilename0 {}", propertiesFilename0); assertEquals("123+default+application.properties", propertiesFilename0); final String ymlFilename0 = ConfigFileUtils.toFilename("666", "none", "cc.yml", ConfigFileFormat.YML); logger.info("ymlFilename0 {}", ymlFilename0); assertEquals("666+none+cc.yml", ymlFilename0); } } ================================================ FILE: apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/util/KeyValueUtilsTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.util; import org.junit.Test; import java.util.HashMap; import java.util.Map; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; public class KeyValueUtilsTest { @Test public void testFilterWithKeyEndswith() { // map Map map = new HashMap<>(); map.put("abc.met", "none"); map.put("abc_meta", "none"); map.put("2bc.meta", "none"); map.put("abc?met", "none"); Map afterFilter = KeyValueUtils.filterWithKeyIgnoreCaseEndsWith(map, "_meta"); for (Map.Entry entry : afterFilter.entrySet()) { String key = entry.getKey(); assertTrue(key.endsWith("_meta")); } } @Test public void testRemoveKeySuffix() { Map map = new HashMap<>(); map.put("abc_meta", "none"); map.put("234_meta", "none"); map.put("888_meta", "none"); Map afterFilter = KeyValueUtils.removeKeySuffix(map, "_meta".length()); for (Map.Entry entry : afterFilter.entrySet()) { String key = entry.getKey(); assertFalse(key.endsWith("_meta")); assertFalse(key.contains("_meta")); } } } ================================================ FILE: apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/util/RoleUtilsTest.java ================================================ /* * Copyright 2025 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.ctrip.framework.apollo.portal.util; import static org.junit.Assert.*; import org.junit.Test; public class RoleUtilsTest { @Test public void testExtractAppIdFromMasterRoleName() throws Exception { assertEquals("someApp", RoleUtils.extractAppIdFromMasterRoleName("Master+someApp")); assertEquals("someApp", RoleUtils.extractAppIdFromMasterRoleName("Master+someApp+xx")); assertNull(RoleUtils.extractAppIdFromMasterRoleName("ReleaseNamespace+app1+application")); } @Test public void testExtractAppIdFromRoleName() throws Exception { assertEquals("someApp", RoleUtils.extractAppIdFromRoleName("Master+someApp")); assertEquals("someApp", RoleUtils.extractAppIdFromRoleName("ModifyNamespace+someApp+xx")); assertEquals("app1", RoleUtils.extractAppIdFromRoleName("ReleaseNamespace+app1+application")); } } ================================================ FILE: apollo-portal/src/test/resources/application.properties ================================================ # # Copyright 2024 Apollo Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # spring.cloud.consul.enabled=false spring.cloud.zookeeper.enabled=false spring.cloud.discovery.enabled=false spring.datasource.url = jdbc:h2:mem:~/apolloportaldb;mode=mysql;DB_CLOSE_ON_EXIT=FALSE;DB_CLOSE_DELAY=-1;BUILTIN_ALIAS_OVERRIDE=TRUE;DATABASE_TO_UPPER=FALSE spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl spring.jpa.hibernate.globally_quoted_identifiers=false spring.jpa.properties.hibernate.globally_quoted_identifiers=false spring.jpa.properties.hibernate.show_sql=false spring.jpa.properties.hibernate.metadata_builder_contributor=com.ctrip.framework.apollo.common.jpa.SqlFunctionsMetadataBuilderContributor spring.jpa.defer-datasource-initialization=true spring.h2.console.enabled = true spring.h2.console.settings.web-allow-others=true spring.session.store-type=none spring.main.allow-bean-definition-overriding=true ================================================ FILE: apollo-portal/src/test/resources/application.yml ================================================ # # Copyright 2024 Apollo Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # server: port: 8070 spring: application: name: apollo-portal apollo: portal: envs: local management: health: status: order: DOWN, OUT_OF_SERVICE, UNKNOWN, UP ================================================ FILE: apollo-portal/src/test/resources/import.sql ================================================ -- -- Copyright 2024 Apollo Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. -- You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. -- ALTER TABLE "Consumer" ALTER COLUMN DataChange_CreatedBy VARCHAR(255) NULL; ALTER TABLE "Consumer" ALTER COLUMN DataChange_CreatedTime TIMESTAMP NULL; ALTER TABLE "ConsumerToken" ALTER COLUMN DataChange_CreatedBy VARCHAR(255) NULL; ALTER TABLE "ConsumerToken" ALTER COLUMN DataChange_CreatedTime TIMESTAMP NULL; ALTER TABLE "ConsumerRole" ALTER COLUMN DataChange_CreatedBy VARCHAR(255) NULL; ALTER TABLE "ConsumerRole" ALTER COLUMN DataChange_CreatedTime TIMESTAMP NULL; ALTER TABLE "Role" ALTER COLUMN DataChange_CreatedBy VARCHAR(255) NULL; ALTER TABLE "Role" ALTER COLUMN DataChange_CreatedTime TIMESTAMP NULL; ALTER TABLE "UserRole" ALTER COLUMN DataChange_CreatedBy VARCHAR(255) NULL; ALTER TABLE "UserRole" ALTER COLUMN DataChange_CreatedTime TIMESTAMP NULL; ALTER TABLE "Permission" ALTER COLUMN DataChange_CreatedBy VARCHAR(255) NULL; ALTER TABLE "Permission" ALTER COLUMN DataChange_CreatedTime TIMESTAMP NULL; ALTER TABLE "RolePermission" ALTER COLUMN DataChange_CreatedBy VARCHAR(255) NULL; ALTER TABLE "RolePermission" ALTER COLUMN DataChange_CreatedTime TIMESTAMP NULL; ALTER TABLE "AppNamespace" ALTER COLUMN DataChange_CreatedBy VARCHAR(255) NULL; ALTER TABLE "AppNamespace" ALTER COLUMN DataChange_CreatedTime TIMESTAMP NULL; ALTER TABLE "AppNamespace" ALTER COLUMN Format VARCHAR(255) NULL; ALTER TABLE "App" ALTER COLUMN DataChange_CreatedTime TIMESTAMP NULL; ALTER TABLE "ServerConfig" ALTER COLUMN Comment VARCHAR(255) NULL; CREATE ALIAS IF NOT EXISTS UNIX_TIMESTAMP FOR "com.ctrip.framework.apollo.common.jpa.H2Function.unixTimestamp"; ================================================ FILE: apollo-portal/src/test/resources/logback-test.xml ================================================ utf-8 [%p] %c - %m%n ================================================ FILE: apollo-portal/src/test/resources/sql/appnamespaceservice/init-appnamespace.sql ================================================ -- -- Copyright 2024 Apollo Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. -- You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. -- INSERT INTO "AppNamespace" (`Id`, `Name`, `AppId`, `Format`, `IsPublic`, `Comment`, `IsDeleted`, `DataChange_CreatedBy`, `DataChange_CreatedTime`, `DataChange_LastModifiedBy`, `DataChange_LastTime`) VALUES (139, 'FX.old', '100003173', 'properties', 1, '', 0, 'zhanglea', '2016-07-11 10:00:58', 'zhanglea', '2016-07-11 10:00:58'), (140, 'SCC.song0711-03', 'song0711-01', 'properties', 1, '', 0, 'song_s', '2016-07-11 10:04:09', 'song_s', '2016-07-11 10:04:09'), (141, 'SCC.song0711-04', 'song0711-01', 'properties', 1, '', 0, 'song_s', '2016-07-11 10:06:29', 'song_s', '2016-07-11 10:06:29'), (142, 'application', 'song0711-02', 'properties', 1, 'default app namespace', 0, 'song_s', '2016-07-11 11:18:24', 'song_s', '2016-07-11 11:18:24'), (143, 'TFF.song0711-02', 'song0711-02', 'properties', 0, '', 0, 'song_s', '2016-07-11 11:15:11', 'song_s', '2016-07-11 11:15:11'), (144, 'datasourcexml', '100003173', 'properties', 1, '', 0, 'apollo', '2016-07-11 12:08:29', 'apollo', '2016-07-11 12:08:29'), (145, 'datasource.xml', '100003173', 'xml', 0, '', 0, 'apollo', '2016-07-11 12:09:30', 'apollo', '2016-07-11 12:09:30'), (146, 'FX.private-01', '100003173', 'properties', 0, '', 0, 'apollo', '2016-07-11 12:09:30', 'apollo', '2016-07-11 12:09:30'), (147, 'datasource', '100003173', 'properties', 0, '', 0, 'apollo', '2016-07-11 12:09:30', 'apollo', '2016-07-11 12:09:30'); INSERT INTO "App" (`AppId`, `Name`, `OrgId`, `OrgName`, `OwnerName`, `OwnerEmail`, `IsDeleted`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES ('1000', 'apollo-test', 'FX', '框架', 'song_s', 'song_s@ctrip.com', 0, 'song_s', 'song_s'), ('song0711-01', 'song0711-01', 'SCC', '框架', 'song_s', 'song_s@ctrip.com', 0, 'song_s', 'song_s'), ('song0711-02', 'song0711-02', 'SCC', '框架', 'song_s', 'song_s@ctrip.com', 0, 'song_s', 'song_s'), ('100003173', 'apollo-portal', 'FX', '框架', 'song_s', 'song_s@ctrip.com', 0, 'song_s', 'song_s'); ================================================ FILE: apollo-portal/src/test/resources/sql/cleanup.sql ================================================ -- -- Copyright 2024 Apollo Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. -- You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. -- DELETE FROM "App"; DELETE FROM "AppNamespace"; DELETE FROM "Authorities"; DELETE FROM "Consumer"; DELETE FROM "ConsumerAudit"; DELETE FROM "ConsumerRole"; DELETE FROM "ConsumerToken"; DELETE FROM "Favorite"; DELETE FROM "Permission"; DELETE FROM "Role"; DELETE FROM "RolePermission"; DELETE FROM "ServerConfig"; DELETE FROM "UserRole"; DELETE FROM "Users"; ================================================ FILE: apollo-portal/src/test/resources/sql/favorites/favorites.sql ================================================ -- -- Copyright 2024 Apollo Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. -- You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. -- INSERT INTO "Favorite" (`Id`, `UserId`, `AppId`, `Position`, `IsDeleted`, `DataChange_CreatedBy`, `DataChange_CreatedTime`, `DataChange_LastModifiedBy`, `DataChange_LastTime`) VALUES (18, 'apollo', 'test0621-03', 10000, 0, 'apollo', '2016-10-10 17:45:30', 'apollo', '2016-10-10 17:45:30'), (19, 'apollo', '100003173', 9999, 0, 'apollo', '2016-10-10 17:45:42', 'apollo', '2016-10-10 17:51:12'), (20, 'apollo', 'test0621-01', 10000, 00000000, 'apollo', '2016-10-10 17:50:57', 'apollo', '2016-10-10 17:50:57'), (21, 'apollo', 'test0621-04', 10000, 00000000, 'apollo', '2016-10-10 17:55:03', 'apollo', '2016-10-10 17:55:03'), (22, 'apollo2', 'test0621-04', 10000, 00000000, 'apollo', '2016-10-10 17:55:21', 'apollo', '2016-10-10 17:55:21'), (23, 'apollo3', 'test0621-04', 10000, 00000000, 'apollo', '2016-10-10 17:55:21', 'apollo', '2016-10-10 17:55:21'); ================================================ FILE: apollo-portal/src/test/resources/sql/openapi/ConsumerServiceIntegrationTest.commonData.sql ================================================ -- -- Copyright 2024 Apollo Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. -- You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. -- /* This sql is dumped from a apollo portal database. The logic is as follows create app: consumer-test-app-id-0 consumer-test-app-id-1 consumer-test-app-id-2 create consumer: consumer-test-app-role Authorization, let consumer-test-app-role manage: consumer-test-app-id-0: Authorization type: App consumer-test-app-id-1: Authorization type: Namespace Managed Namespace: application */ /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; /*!40101 SET NAMES utf8 */; /*!50503 SET NAMES utf8mb4 */; /*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; /*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; /*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; /*!40000 ALTER TABLE `App` DISABLE KEYS */; INSERT INTO "App" (`Id`, `AppId`, `Name`, `OrgId`, `OrgName`, `OwnerName`, `OwnerEmail`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (1000, 'consumer-test-app-id-0', 'consumer-test-app-id-0', 'TEST1', '样例部门1', 'apollo', 'apollo@acme.com', 'apollo', 'apollo'); INSERT INTO "App" (`Id`, `AppId`, `Name`, `OrgId`, `OrgName`, `OwnerName`, `OwnerEmail`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (2000, 'consumer-test-app-id-1', 'consumer-test-app-id-1', 'TEST2', '样例部门2', 'apollo', 'apollo@acme.com', 'apollo', 'apollo'); INSERT INTO "App" (`Id`, `AppId`, `Name`, `OrgId`, `OrgName`, `OwnerName`, `OwnerEmail`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (3000, 'consumer-test-app-id-2', 'consumer-test-app-id-2', 'TEST2', '样例部门2', 'apollo', 'apollo@acme.com', 'apollo', 'apollo'); /*!40000 ALTER TABLE `App` ENABLE KEYS */; /*!40000 ALTER TABLE `AppNamespace` DISABLE KEYS */; INSERT INTO "AppNamespace" (`Id`, `Name`, `AppId`, `Format`, `Comment`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (1000, 'application', 'consumer-test-app-id-0', 'properties', 'default app namespace', 'apollo', 'apollo'); INSERT INTO "AppNamespace" (`Id`, `Name`, `AppId`, `Format`, `Comment`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (2000, 'application', 'consumer-test-app-id-1', 'properties', 'default app namespace', 'apollo', 'apollo'); INSERT INTO "AppNamespace" (`Id`, `Name`, `AppId`, `Format`, `Comment`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (3000, 'application', 'consumer-test-app-id-2', 'properties', 'default app namespace', 'apollo', 'apollo'); /*!40000 ALTER TABLE `AppNamespace` ENABLE KEYS */; /*!40000 ALTER TABLE `Consumer` DISABLE KEYS */; INSERT INTO "Consumer" (`Id`, `AppId`, `Name`, `OrgId`, `OrgName`, `OwnerName`, `OwnerEmail`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (1000, 'consumer-test-app-role', 'consumer-test-app-role', 'TEST2', '样例部门2', 'apollo', 'apollo@acme.com', 'apollo', 'apollo'); INSERT INTO "Consumer" (`Id`, `AppId`, `Name`, `OrgId`, `OrgName`, `OwnerName`, `OwnerEmail`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (1001, 'consumer-test-app-role1', 'consumer-test-app-role1', 'TEST2', '样例部门2', 'apollo', 'apollo@acme.com', 'apollo', 'apollo'); INSERT INTO "Consumer" (`Id`, `AppId`, `Name`, `OrgId`, `OrgName`, `OwnerName`, `OwnerEmail`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (1002, 'consumer-test-app-role2', 'consumer-test-app-role2', 'TEST2', '样例部门2', 'apollo', 'apollo@acme.com', 'apollo', 'apollo'); INSERT INTO "Consumer" (`Id`, `AppId`, `Name`, `OrgId`, `OrgName`, `OwnerName`, `OwnerEmail`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (1003, 'consumer-test-app-role3', 'consumer-test-app-role3', 'TEST2', '样例部门2', 'apollo', 'apollo@acme.com', 'apollo', 'apollo'); /*!40000 ALTER TABLE `Consumer` ENABLE KEYS */; /*!40000 ALTER TABLE `ConsumerAudit` DISABLE KEYS */; /*!40000 ALTER TABLE `ConsumerAudit` ENABLE KEYS */; /*!40000 ALTER TABLE `ConsumerRole` DISABLE KEYS */; INSERT INTO "ConsumerRole" (`Id`, `ConsumerId`, `RoleId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (1000, 1000, 1000, 'apollo', 'apollo'); INSERT INTO "ConsumerRole" (`Id`, `ConsumerId`, `RoleId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (2000, 1000, 11000, 'apollo', 'apollo'); INSERT INTO "ConsumerRole" (`Id`, `ConsumerId`, `RoleId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (3000, 1000, 12000, 'apollo', 'apollo'); /*!40000 ALTER TABLE `ConsumerRole` ENABLE KEYS */; /*!40000 ALTER TABLE `ConsumerToken` DISABLE KEYS */; INSERT INTO "ConsumerToken" (`Id`, `ConsumerId`, `Token`, `RateLimit`, `Expires`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (1000, 1000, '3c16bf5b1f44b465179253442460e8c0ad845289', 20, '2098-12-31 10:00:00', 'apollo', 'apollo'); /*!40000 ALTER TABLE `ConsumerToken` ENABLE KEYS */; /*!40000 ALTER TABLE `Favorite` DISABLE KEYS */; /*!40000 ALTER TABLE `Favorite` ENABLE KEYS */; /*!40000 ALTER TABLE `Permission` DISABLE KEYS */; INSERT INTO "Permission" (`Id`, `PermissionType`, `TargetId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (1000, 'AssignRole', 'consumer-test-app-id-0', 'apollo', 'apollo'); INSERT INTO "Permission" (`Id`, `PermissionType`, `TargetId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (2000, 'CreateNamespace', 'consumer-test-app-id-0', 'apollo', 'apollo'); INSERT INTO "Permission" (`Id`, `PermissionType`, `TargetId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (3000, 'CreateCluster', 'consumer-test-app-id-0', 'apollo', 'apollo'); INSERT INTO "Permission" (`Id`, `PermissionType`, `TargetId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (4000, 'ManageAppMaster', 'consumer-test-app-id-0', 'apollo', 'apollo'); INSERT INTO "Permission" (`Id`, `PermissionType`, `TargetId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (5000, 'ModifyNamespace', 'consumer-test-app-id-0+application', 'apollo', 'apollo'); INSERT INTO "Permission" (`Id`, `PermissionType`, `TargetId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (6000, 'ReleaseNamespace', 'consumer-test-app-id-0+application', 'apollo', 'apollo'); INSERT INTO "Permission" (`Id`, `PermissionType`, `TargetId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (7000, 'ModifyNamespace', 'consumer-test-app-id-0+application+DEV', 'apollo', 'apollo'); INSERT INTO "Permission" (`Id`, `PermissionType`, `TargetId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (8000, 'ReleaseNamespace', 'consumer-test-app-id-0+application+DEV', 'apollo', 'apollo'); INSERT INTO "Permission" (`Id`, `PermissionType`, `TargetId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (9000, 'CreateNamespace', 'consumer-test-app-id-1', 'apollo', 'apollo'); INSERT INTO "Permission" (`Id`, `PermissionType`, `TargetId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (10000, 'AssignRole', 'consumer-test-app-id-1', 'apollo', 'apollo'); INSERT INTO "Permission" (`Id`, `PermissionType`, `TargetId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (11000, 'CreateCluster', 'consumer-test-app-id-1', 'apollo', 'apollo'); INSERT INTO "Permission" (`Id`, `PermissionType`, `TargetId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (12000, 'ManageAppMaster', 'consumer-test-app-id-1', 'apollo', 'apollo'); INSERT INTO "Permission" (`Id`, `PermissionType`, `TargetId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (13000, 'ModifyNamespace', 'consumer-test-app-id-1+application', 'apollo', 'apollo'); INSERT INTO "Permission" (`Id`, `PermissionType`, `TargetId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (14000, 'ReleaseNamespace', 'consumer-test-app-id-1+application', 'apollo', 'apollo'); INSERT INTO "Permission" (`Id`, `PermissionType`, `TargetId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (15000, 'ModifyNamespace', 'consumer-test-app-id-1+application+DEV', 'apollo', 'apollo'); INSERT INTO "Permission" (`Id`, `PermissionType`, `TargetId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (16000, 'ReleaseNamespace', 'consumer-test-app-id-1+application+DEV', 'apollo', 'apollo'); INSERT INTO "Permission" (`Id`, `PermissionType`, `TargetId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (17000, 'CreateCluster', 'consumer-test-app-id-2', 'apollo', 'apollo'); INSERT INTO "Permission" (`Id`, `PermissionType`, `TargetId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (18000, 'AssignRole', 'consumer-test-app-id-2', 'apollo', 'apollo'); INSERT INTO "Permission" (`Id`, `PermissionType`, `TargetId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (19000, 'CreateNamespace', 'consumer-test-app-id-2', 'apollo', 'apollo'); INSERT INTO "Permission" (`Id`, `PermissionType`, `TargetId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (20000, 'ManageAppMaster', 'consumer-test-app-id-2', 'apollo', 'apollo'); INSERT INTO "Permission" (`Id`, `PermissionType`, `TargetId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (21000, 'ModifyNamespace', 'consumer-test-app-id-2+application', 'apollo', 'apollo'); INSERT INTO "Permission" (`Id`, `PermissionType`, `TargetId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (22000, 'ReleaseNamespace', 'consumer-test-app-id-2+application', 'apollo', 'apollo'); INSERT INTO "Permission" (`Id`, `PermissionType`, `TargetId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (23000, 'ModifyNamespace', 'consumer-test-app-id-2+application+DEV', 'apollo', 'apollo'); INSERT INTO "Permission" (`Id`, `PermissionType`, `TargetId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (24000, 'ReleaseNamespace', 'consumer-test-app-id-2+application+DEV', 'apollo', 'apollo'); /*!40000 ALTER TABLE `Permission` ENABLE KEYS */; /*!40000 ALTER TABLE `Role` DISABLE KEYS */; INSERT INTO "Role" (`Id`, `RoleName`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (1000, 'Master+consumer-test-app-id-0', 'apollo', 'apollo'); INSERT INTO "Role" (`Id`, `RoleName`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (2000, 'ManageAppMaster+consumer-test-app-id-0', 'apollo', 'apollo'); INSERT INTO "Role" (`Id`, `RoleName`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (3000, 'ModifyNamespace+consumer-test-app-id-0+application', 'apollo', 'apollo'); INSERT INTO "Role" (`Id`, `RoleName`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (4000, 'ReleaseNamespace+consumer-test-app-id-0+application', 'apollo', 'apollo'); INSERT INTO "Role" (`Id`, `RoleName`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (5000, 'ModifyNamespace+consumer-test-app-id-0+application+DEV', 'apollo', 'apollo'); INSERT INTO "Role" (`Id`, `RoleName`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (6000, 'ReleaseNamespace+consumer-test-app-id-0+application+DEV', 'apollo', 'apollo'); INSERT INTO "Role" (`Id`, `RoleName`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (7000, 'Master+consumer-test-app-id-1', 'apollo', 'apollo'); INSERT INTO "Role" (`Id`, `RoleName`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (8000, 'ManageAppMaster+consumer-test-app-id-1', 'apollo', 'apollo'); INSERT INTO "Role" (`Id`, `RoleName`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (9000, 'ModifyNamespace+consumer-test-app-id-1+application', 'apollo', 'apollo'); INSERT INTO "Role" (`Id`, `RoleName`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (10000, 'ReleaseNamespace+consumer-test-app-id-1+application', 'apollo', 'apollo'); INSERT INTO "Role" (`Id`, `RoleName`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (11000, 'ModifyNamespace+consumer-test-app-id-1+application+DEV', 'apollo', 'apollo'); INSERT INTO "Role" (`Id`, `RoleName`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (12000, 'ReleaseNamespace+consumer-test-app-id-1+application+DEV', 'apollo', 'apollo'); INSERT INTO "Role" (`Id`, `RoleName`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (13000, 'Master+consumer-test-app-id-2', 'apollo', 'apollo'); INSERT INTO "Role" (`Id`, `RoleName`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (14000, 'ManageAppMaster+consumer-test-app-id-2', 'apollo', 'apollo'); INSERT INTO "Role" (`Id`, `RoleName`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (15000, 'ModifyNamespace+consumer-test-app-id-2+application', 'apollo', 'apollo'); INSERT INTO "Role" (`Id`, `RoleName`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (16000, 'ReleaseNamespace+consumer-test-app-id-2+application', 'apollo', 'apollo'); INSERT INTO "Role" (`Id`, `RoleName`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (17000, 'ModifyNamespace+consumer-test-app-id-2+application+DEV', 'apollo', 'apollo'); INSERT INTO "Role" (`Id`, `RoleName`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (18000, 'ReleaseNamespace+consumer-test-app-id-2+application+DEV', 'apollo', 'apollo'); /*!40000 ALTER TABLE `Role` ENABLE KEYS */; /*!40000 ALTER TABLE `RolePermission` DISABLE KEYS */; INSERT INTO "RolePermission" (`Id`, `RoleId`, `PermissionId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (1000, 1000, 1000, 'apollo', 'apollo'); INSERT INTO "RolePermission" (`Id`, `RoleId`, `PermissionId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (2000, 1000, 2000, 'apollo', 'apollo'); INSERT INTO "RolePermission" (`Id`, `RoleId`, `PermissionId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (3000, 1000, 3000, 'apollo', 'apollo'); INSERT INTO "RolePermission" (`Id`, `RoleId`, `PermissionId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (4000, 2000, 4000, 'apollo', 'apollo'); INSERT INTO "RolePermission" (`Id`, `RoleId`, `PermissionId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (5000, 3000, 5000, 'apollo', 'apollo'); INSERT INTO "RolePermission" (`Id`, `RoleId`, `PermissionId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (6000, 4000, 6000, 'apollo', 'apollo'); INSERT INTO "RolePermission" (`Id`, `RoleId`, `PermissionId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (7000, 5000, 7000, 'apollo', 'apollo'); INSERT INTO "RolePermission" (`Id`, `RoleId`, `PermissionId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (8000, 6000, 8000, 'apollo', 'apollo'); INSERT INTO "RolePermission" (`Id`, `RoleId`, `PermissionId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (9000, 7000, 9000, 'apollo', 'apollo'); INSERT INTO "RolePermission" (`Id`, `RoleId`, `PermissionId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (10000, 7000, 10000, 'apollo', 'apollo'); INSERT INTO "RolePermission" (`Id`, `RoleId`, `PermissionId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (11000, 7000, 11000, 'apollo', 'apollo'); INSERT INTO "RolePermission" (`Id`, `RoleId`, `PermissionId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (12000, 8000, 12000, 'apollo', 'apollo'); INSERT INTO "RolePermission" (`Id`, `RoleId`, `PermissionId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (13000, 9000, 13000, 'apollo', 'apollo'); INSERT INTO "RolePermission" (`Id`, `RoleId`, `PermissionId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (14000, 10000, 14000, 'apollo', 'apollo'); INSERT INTO "RolePermission" (`Id`, `RoleId`, `PermissionId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (15000, 11000, 15000, 'apollo', 'apollo'); INSERT INTO "RolePermission" (`Id`, `RoleId`, `PermissionId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (16000, 12000, 16000, 'apollo', 'apollo'); INSERT INTO "RolePermission" (`Id`, `RoleId`, `PermissionId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (17000, 13000, 17000, 'apollo', 'apollo'); INSERT INTO "RolePermission" (`Id`, `RoleId`, `PermissionId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (18000, 13000, 18000, 'apollo', 'apollo'); INSERT INTO "RolePermission" (`Id`, `RoleId`, `PermissionId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (19000, 13000, 19000, 'apollo', 'apollo'); INSERT INTO "RolePermission" (`Id`, `RoleId`, `PermissionId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (20000, 14000, 20000, 'apollo', 'apollo'); INSERT INTO "RolePermission" (`Id`, `RoleId`, `PermissionId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (21000, 15000, 21000, 'apollo', 'apollo'); INSERT INTO "RolePermission" (`Id`, `RoleId`, `PermissionId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (22000, 16000, 22000, 'apollo', 'apollo'); INSERT INTO "RolePermission" (`Id`, `RoleId`, `PermissionId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (23000, 17000, 23000, 'apollo', 'apollo'); INSERT INTO "RolePermission" (`Id`, `RoleId`, `PermissionId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (24000, 18000, 24000, 'apollo', 'apollo'); /*!40000 ALTER TABLE `RolePermission` ENABLE KEYS */; /*!40000 ALTER TABLE `UserRole` DISABLE KEYS */; INSERT INTO "UserRole" (`Id`, `UserId`, `RoleId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (1000, 'apollo', 1000, 'apollo', 'apollo'); INSERT INTO "UserRole" (`Id`, `UserId`, `RoleId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (2000, 'apollo', 3000, 'apollo', 'apollo'); INSERT INTO "UserRole" (`Id`, `UserId`, `RoleId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (3000, 'apollo', 4000, 'apollo', 'apollo'); INSERT INTO "UserRole" (`Id`, `UserId`, `RoleId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (4000, 'apollo', 7000, 'apollo', 'apollo'); INSERT INTO "UserRole" (`Id`, `UserId`, `RoleId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (5000, 'apollo', 9000, 'apollo', 'apollo'); INSERT INTO "UserRole" (`Id`, `UserId`, `RoleId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (6000, 'apollo', 10000, 'apollo', 'apollo'); INSERT INTO "UserRole" (`Id`, `UserId`, `RoleId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (7000, 'apollo', 13000, 'apollo', 'apollo'); INSERT INTO "UserRole" (`Id`, `UserId`, `RoleId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (8000, 'apollo', 15000, 'apollo', 'apollo'); INSERT INTO "UserRole" (`Id`, `UserId`, `RoleId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (9000, 'apollo', 16000, 'apollo', 'apollo'); /*!40000 ALTER TABLE `UserRole` ENABLE KEYS */; /*!40000 ALTER TABLE `Users` DISABLE KEYS */; INSERT INTO "Users" (`Id`, `Username`, `Password`, `UserDisplayName`, `Email`, `Enabled`) VALUES (1000, 'apollo', '$2a$10$7r20uS.BQ9uBpf3Baj3uQOZvMVvB1RN3PYoKE94gtz2.WAOuiiwXS', 'apollo', 'apollo@acme.com', 1); /*!40000 ALTER TABLE `Users` ENABLE KEYS */; /*!40101 SET SQL_MODE=IFNULL(@OLD_SQL_MODE, '') */; /*!40014 SET FOREIGN_KEY_CHECKS=IFNULL(@OLD_FOREIGN_KEY_CHECKS, 1) */; /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; /*!40111 SET SQL_NOTES=IFNULL(@OLD_SQL_NOTES, 1) */; ================================================ FILE: apollo-portal/src/test/resources/sql/openapi/ConsumerServiceIntegrationTest.testFindAppIdsAuthorizedByConsumerId.sql ================================================ -- -- Copyright 2024 Apollo Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. -- You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. -- /* This sql is dumped from a apollo portal database. The logic is as follows create app: consumer-test-app-id-0 consumer-test-app-id-1 consumer-test-app-id-2 create consumer: consumer-test-app-role Authorization, let consumer-test-app-role manage: consumer-test-app-id-0: Authorization type: App consumer-test-app-id-1: Authorization type: Namespace Managed Namespace: application */ /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; /*!40101 SET NAMES utf8 */; /*!50503 SET NAMES utf8mb4 */; /*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; /*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; /*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; /*!40000 ALTER TABLE `App` DISABLE KEYS */; INSERT INTO "App" (`Id`, `AppId`, `Name`, `OrgId`, `OrgName`, `OwnerName`, `OwnerEmail`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (1000, 'consumer-test-app-id-0', 'consumer-test-app-id-0', 'TEST1', '样例部门1', 'apollo', 'apollo@acme.com', 'apollo', 'apollo'); INSERT INTO "App" (`Id`, `AppId`, `Name`, `OrgId`, `OrgName`, `OwnerName`, `OwnerEmail`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (2000, 'consumer-test-app-id-1', 'consumer-test-app-id-1', 'TEST2', '样例部门2', 'apollo', 'apollo@acme.com', 'apollo', 'apollo'); INSERT INTO "App" (`Id`, `AppId`, `Name`, `OrgId`, `OrgName`, `OwnerName`, `OwnerEmail`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (3000, 'consumer-test-app-id-2', 'consumer-test-app-id-2', 'TEST2', '样例部门2', 'apollo', 'apollo@acme.com', 'apollo', 'apollo'); /*!40000 ALTER TABLE `App` ENABLE KEYS */; /*!40000 ALTER TABLE `AppNamespace` DISABLE KEYS */; INSERT INTO "AppNamespace" (`Id`, `Name`, `AppId`, `Format`, `Comment`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (1000, 'application', 'consumer-test-app-id-0', 'properties', 'default app namespace', 'apollo', 'apollo'); INSERT INTO "AppNamespace" (`Id`, `Name`, `AppId`, `Format`, `Comment`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (2000, 'application', 'consumer-test-app-id-1', 'properties', 'default app namespace', 'apollo', 'apollo'); INSERT INTO "AppNamespace" (`Id`, `Name`, `AppId`, `Format`, `Comment`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (3000, 'application', 'consumer-test-app-id-2', 'properties', 'default app namespace', 'apollo', 'apollo'); /*!40000 ALTER TABLE `AppNamespace` ENABLE KEYS */; /*!40000 ALTER TABLE `Consumer` DISABLE KEYS */; INSERT INTO "Consumer" (`Id`, `AppId`, `Name`, `OrgId`, `OrgName`, `OwnerName`, `OwnerEmail`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (1000, 'consumer-test-app-role', 'consumer-test-app-role', 'TEST2', '样例部门2', 'apollo', 'apollo@acme.com', 'apollo', 'apollo'); /*!40000 ALTER TABLE `Consumer` ENABLE KEYS */; /*!40000 ALTER TABLE `ConsumerAudit` DISABLE KEYS */; /*!40000 ALTER TABLE `ConsumerAudit` ENABLE KEYS */; /*!40000 ALTER TABLE `ConsumerRole` DISABLE KEYS */; INSERT INTO "ConsumerRole" (`Id`, `ConsumerId`, `RoleId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (1000, 1000, 1000, 'apollo', 'apollo'); INSERT INTO "ConsumerRole" (`Id`, `ConsumerId`, `RoleId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (2000, 1000, 11000, 'apollo', 'apollo'); INSERT INTO "ConsumerRole" (`Id`, `ConsumerId`, `RoleId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (3000, 1000, 12000, 'apollo', 'apollo'); /*!40000 ALTER TABLE `ConsumerRole` ENABLE KEYS */; /*!40000 ALTER TABLE `ConsumerToken` DISABLE KEYS */; INSERT INTO "ConsumerToken" (`Id`, `ConsumerId`, `Token`, `RateLimit`, `Expires`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (1000, 1000, '3c16bf5b1f44b465179253442460e8c0ad845289', 20, '2098-12-31 10:00:00', 'apollo', 'apollo'); /*!40000 ALTER TABLE `ConsumerToken` ENABLE KEYS */; /*!40000 ALTER TABLE `Favorite` DISABLE KEYS */; /*!40000 ALTER TABLE `Favorite` ENABLE KEYS */; /*!40000 ALTER TABLE `Permission` DISABLE KEYS */; INSERT INTO "Permission" (`Id`, `PermissionType`, `TargetId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (1000, 'AssignRole', 'consumer-test-app-id-0', 'apollo', 'apollo'); INSERT INTO "Permission" (`Id`, `PermissionType`, `TargetId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (2000, 'CreateNamespace', 'consumer-test-app-id-0', 'apollo', 'apollo'); INSERT INTO "Permission" (`Id`, `PermissionType`, `TargetId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (3000, 'CreateCluster', 'consumer-test-app-id-0', 'apollo', 'apollo'); INSERT INTO "Permission" (`Id`, `PermissionType`, `TargetId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (4000, 'ManageAppMaster', 'consumer-test-app-id-0', 'apollo', 'apollo'); INSERT INTO "Permission" (`Id`, `PermissionType`, `TargetId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (5000, 'ModifyNamespace', 'consumer-test-app-id-0+application', 'apollo', 'apollo'); INSERT INTO "Permission" (`Id`, `PermissionType`, `TargetId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (6000, 'ReleaseNamespace', 'consumer-test-app-id-0+application', 'apollo', 'apollo'); INSERT INTO "Permission" (`Id`, `PermissionType`, `TargetId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (7000, 'ModifyNamespace', 'consumer-test-app-id-0+application+DEV', 'apollo', 'apollo'); INSERT INTO "Permission" (`Id`, `PermissionType`, `TargetId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (8000, 'ReleaseNamespace', 'consumer-test-app-id-0+application+DEV', 'apollo', 'apollo'); INSERT INTO "Permission" (`Id`, `PermissionType`, `TargetId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (9000, 'CreateNamespace', 'consumer-test-app-id-1', 'apollo', 'apollo'); INSERT INTO "Permission" (`Id`, `PermissionType`, `TargetId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (10000, 'AssignRole', 'consumer-test-app-id-1', 'apollo', 'apollo'); INSERT INTO "Permission" (`Id`, `PermissionType`, `TargetId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (11000, 'CreateCluster', 'consumer-test-app-id-1', 'apollo', 'apollo'); INSERT INTO "Permission" (`Id`, `PermissionType`, `TargetId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (12000, 'ManageAppMaster', 'consumer-test-app-id-1', 'apollo', 'apollo'); INSERT INTO "Permission" (`Id`, `PermissionType`, `TargetId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (13000, 'ModifyNamespace', 'consumer-test-app-id-1+application', 'apollo', 'apollo'); INSERT INTO "Permission" (`Id`, `PermissionType`, `TargetId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (14000, 'ReleaseNamespace', 'consumer-test-app-id-1+application', 'apollo', 'apollo'); INSERT INTO "Permission" (`Id`, `PermissionType`, `TargetId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (15000, 'ModifyNamespace', 'consumer-test-app-id-1+application+DEV', 'apollo', 'apollo'); INSERT INTO "Permission" (`Id`, `PermissionType`, `TargetId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (16000, 'ReleaseNamespace', 'consumer-test-app-id-1+application+DEV', 'apollo', 'apollo'); INSERT INTO "Permission" (`Id`, `PermissionType`, `TargetId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (17000, 'CreateCluster', 'consumer-test-app-id-2', 'apollo', 'apollo'); INSERT INTO "Permission" (`Id`, `PermissionType`, `TargetId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (18000, 'AssignRole', 'consumer-test-app-id-2', 'apollo', 'apollo'); INSERT INTO "Permission" (`Id`, `PermissionType`, `TargetId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (19000, 'CreateNamespace', 'consumer-test-app-id-2', 'apollo', 'apollo'); INSERT INTO "Permission" (`Id`, `PermissionType`, `TargetId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (20000, 'ManageAppMaster', 'consumer-test-app-id-2', 'apollo', 'apollo'); INSERT INTO "Permission" (`Id`, `PermissionType`, `TargetId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (21000, 'ModifyNamespace', 'consumer-test-app-id-2+application', 'apollo', 'apollo'); INSERT INTO "Permission" (`Id`, `PermissionType`, `TargetId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (22000, 'ReleaseNamespace', 'consumer-test-app-id-2+application', 'apollo', 'apollo'); INSERT INTO "Permission" (`Id`, `PermissionType`, `TargetId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (23000, 'ModifyNamespace', 'consumer-test-app-id-2+application+DEV', 'apollo', 'apollo'); INSERT INTO "Permission" (`Id`, `PermissionType`, `TargetId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (24000, 'ReleaseNamespace', 'consumer-test-app-id-2+application+DEV', 'apollo', 'apollo'); /*!40000 ALTER TABLE `Permission` ENABLE KEYS */; /*!40000 ALTER TABLE `Role` DISABLE KEYS */; INSERT INTO "Role" (`Id`, `RoleName`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (1000, 'Master+consumer-test-app-id-0', 'apollo', 'apollo'); INSERT INTO "Role" (`Id`, `RoleName`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (2000, 'ManageAppMaster+consumer-test-app-id-0', 'apollo', 'apollo'); INSERT INTO "Role" (`Id`, `RoleName`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (3000, 'ModifyNamespace+consumer-test-app-id-0+application', 'apollo', 'apollo'); INSERT INTO "Role" (`Id`, `RoleName`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (4000, 'ReleaseNamespace+consumer-test-app-id-0+application', 'apollo', 'apollo'); INSERT INTO "Role" (`Id`, `RoleName`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (5000, 'ModifyNamespace+consumer-test-app-id-0+application+DEV', 'apollo', 'apollo'); INSERT INTO "Role" (`Id`, `RoleName`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (6000, 'ReleaseNamespace+consumer-test-app-id-0+application+DEV', 'apollo', 'apollo'); INSERT INTO "Role" (`Id`, `RoleName`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (7000, 'Master+consumer-test-app-id-1', 'apollo', 'apollo'); INSERT INTO "Role" (`Id`, `RoleName`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (8000, 'ManageAppMaster+consumer-test-app-id-1', 'apollo', 'apollo'); INSERT INTO "Role" (`Id`, `RoleName`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (9000, 'ModifyNamespace+consumer-test-app-id-1+application', 'apollo', 'apollo'); INSERT INTO "Role" (`Id`, `RoleName`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (10000, 'ReleaseNamespace+consumer-test-app-id-1+application', 'apollo', 'apollo'); INSERT INTO "Role" (`Id`, `RoleName`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (11000, 'ModifyNamespace+consumer-test-app-id-1+application+DEV', 'apollo', 'apollo'); INSERT INTO "Role" (`Id`, `RoleName`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (12000, 'ReleaseNamespace+consumer-test-app-id-1+application+DEV', 'apollo', 'apollo'); INSERT INTO "Role" (`Id`, `RoleName`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (13000, 'Master+consumer-test-app-id-2', 'apollo', 'apollo'); INSERT INTO "Role" (`Id`, `RoleName`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (14000, 'ManageAppMaster+consumer-test-app-id-2', 'apollo', 'apollo'); INSERT INTO "Role" (`Id`, `RoleName`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (15000, 'ModifyNamespace+consumer-test-app-id-2+application', 'apollo', 'apollo'); INSERT INTO "Role" (`Id`, `RoleName`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (16000, 'ReleaseNamespace+consumer-test-app-id-2+application', 'apollo', 'apollo'); INSERT INTO "Role" (`Id`, `RoleName`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (17000, 'ModifyNamespace+consumer-test-app-id-2+application+DEV', 'apollo', 'apollo'); INSERT INTO "Role" (`Id`, `RoleName`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (18000, 'ReleaseNamespace+consumer-test-app-id-2+application+DEV', 'apollo', 'apollo'); /*!40000 ALTER TABLE `Role` ENABLE KEYS */; /*!40000 ALTER TABLE `RolePermission` DISABLE KEYS */; INSERT INTO "RolePermission" (`Id`, `RoleId`, `PermissionId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (1000, 1000, 1000, 'apollo', 'apollo'); INSERT INTO "RolePermission" (`Id`, `RoleId`, `PermissionId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (2000, 1000, 2000, 'apollo', 'apollo'); INSERT INTO "RolePermission" (`Id`, `RoleId`, `PermissionId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (3000, 1000, 3000, 'apollo', 'apollo'); INSERT INTO "RolePermission" (`Id`, `RoleId`, `PermissionId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (4000, 2000, 4000, 'apollo', 'apollo'); INSERT INTO "RolePermission" (`Id`, `RoleId`, `PermissionId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (5000, 3000, 5000, 'apollo', 'apollo'); INSERT INTO "RolePermission" (`Id`, `RoleId`, `PermissionId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (6000, 4000, 6000, 'apollo', 'apollo'); INSERT INTO "RolePermission" (`Id`, `RoleId`, `PermissionId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (7000, 5000, 7000, 'apollo', 'apollo'); INSERT INTO "RolePermission" (`Id`, `RoleId`, `PermissionId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (8000, 6000, 8000, 'apollo', 'apollo'); INSERT INTO "RolePermission" (`Id`, `RoleId`, `PermissionId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (9000, 7000, 9000, 'apollo', 'apollo'); INSERT INTO "RolePermission" (`Id`, `RoleId`, `PermissionId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (10000, 7000, 10000, 'apollo', 'apollo'); INSERT INTO "RolePermission" (`Id`, `RoleId`, `PermissionId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (11000, 7000, 11000, 'apollo', 'apollo'); INSERT INTO "RolePermission" (`Id`, `RoleId`, `PermissionId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (12000, 8000, 12000, 'apollo', 'apollo'); INSERT INTO "RolePermission" (`Id`, `RoleId`, `PermissionId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (13000, 9000, 13000, 'apollo', 'apollo'); INSERT INTO "RolePermission" (`Id`, `RoleId`, `PermissionId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (14000, 10000, 14000, 'apollo', 'apollo'); INSERT INTO "RolePermission" (`Id`, `RoleId`, `PermissionId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (15000, 11000, 15000, 'apollo', 'apollo'); INSERT INTO "RolePermission" (`Id`, `RoleId`, `PermissionId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (16000, 12000, 16000, 'apollo', 'apollo'); INSERT INTO "RolePermission" (`Id`, `RoleId`, `PermissionId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (17000, 13000, 17000, 'apollo', 'apollo'); INSERT INTO "RolePermission" (`Id`, `RoleId`, `PermissionId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (18000, 13000, 18000, 'apollo', 'apollo'); INSERT INTO "RolePermission" (`Id`, `RoleId`, `PermissionId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (19000, 13000, 19000, 'apollo', 'apollo'); INSERT INTO "RolePermission" (`Id`, `RoleId`, `PermissionId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (20000, 14000, 20000, 'apollo', 'apollo'); INSERT INTO "RolePermission" (`Id`, `RoleId`, `PermissionId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (21000, 15000, 21000, 'apollo', 'apollo'); INSERT INTO "RolePermission" (`Id`, `RoleId`, `PermissionId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (22000, 16000, 22000, 'apollo', 'apollo'); INSERT INTO "RolePermission" (`Id`, `RoleId`, `PermissionId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (23000, 17000, 23000, 'apollo', 'apollo'); INSERT INTO "RolePermission" (`Id`, `RoleId`, `PermissionId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (24000, 18000, 24000, 'apollo', 'apollo'); /*!40000 ALTER TABLE `RolePermission` ENABLE KEYS */; /*!40000 ALTER TABLE `UserRole` DISABLE KEYS */; INSERT INTO "UserRole" (`Id`, `UserId`, `RoleId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (1000, 'apollo', 1000, 'apollo', 'apollo'); INSERT INTO "UserRole" (`Id`, `UserId`, `RoleId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (2000, 'apollo', 3000, 'apollo', 'apollo'); INSERT INTO "UserRole" (`Id`, `UserId`, `RoleId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (3000, 'apollo', 4000, 'apollo', 'apollo'); INSERT INTO "UserRole" (`Id`, `UserId`, `RoleId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (4000, 'apollo', 7000, 'apollo', 'apollo'); INSERT INTO "UserRole" (`Id`, `UserId`, `RoleId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (5000, 'apollo', 9000, 'apollo', 'apollo'); INSERT INTO "UserRole" (`Id`, `UserId`, `RoleId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (6000, 'apollo', 10000, 'apollo', 'apollo'); INSERT INTO "UserRole" (`Id`, `UserId`, `RoleId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (7000, 'apollo', 13000, 'apollo', 'apollo'); INSERT INTO "UserRole" (`Id`, `UserId`, `RoleId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (8000, 'apollo', 15000, 'apollo', 'apollo'); INSERT INTO "UserRole" (`Id`, `UserId`, `RoleId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (9000, 'apollo', 16000, 'apollo', 'apollo'); /*!40000 ALTER TABLE `UserRole` ENABLE KEYS */; /*!40000 ALTER TABLE `Users` DISABLE KEYS */; INSERT INTO "Users" (`Id`, `Username`, `Password`, `UserDisplayName`, `Email`, `Enabled`) VALUES (1000, 'apollo', '$2a$10$7r20uS.BQ9uBpf3Baj3uQOZvMVvB1RN3PYoKE94gtz2.WAOuiiwXS', 'apollo', 'apollo@acme.com', 1); /*!40000 ALTER TABLE `Users` ENABLE KEYS */; /*!40101 SET SQL_MODE=IFNULL(@OLD_SQL_MODE, '') */; /*!40014 SET FOREIGN_KEY_CHECKS=IFNULL(@OLD_FOREIGN_KEY_CHECKS, 1) */; /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; /*!40111 SET SQL_NOTES=IFNULL(@OLD_SQL_NOTES, 1) */; ================================================ FILE: apollo-portal/src/test/resources/sql/openapi/NamespaceControllerTest.testCreateAppNamespace.sql ================================================ -- -- Copyright 2024 Apollo Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. -- You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. -- /* This sql is dumped from a apollo portal database. The logic is as follows create app: consumer-test-app-id-0 consumer-test-app-id-1 create consumer: consumer-test-app-role Authorization, let consumer-test-app-role manage: consumer-test-app-id-0: Authorization type: App consumer-test-app-id-1: Authorization type: Namespace Managed Namespace: application */ /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; /*!40101 SET NAMES utf8 */; /*!50503 SET NAMES utf8mb4 */; /*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; /*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; /*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; /*!40000 ALTER TABLE `App` DISABLE KEYS */; INSERT INTO "App" (`Id`, `AppId`, `Name`, `OrgId`, `OrgName`, `OwnerName`, `OwnerEmail`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (1000, 'consumer-test-app-id-0', 'consumer-test-app-id-0', 'TEST1', '样例部门1', 'apollo', 'apollo@acme.com', 'apollo', 'apollo'); INSERT INTO "App" (`Id`, `AppId`, `Name`, `OrgId`, `OrgName`, `OwnerName`, `OwnerEmail`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (2000, 'consumer-test-app-id-1', 'consumer-test-app-id-1', 'TEST2', '样例部门2', 'apollo', 'apollo@acme.com', 'apollo', 'apollo'); /*!40000 ALTER TABLE `App` ENABLE KEYS */; /*!40000 ALTER TABLE `AppNamespace` DISABLE KEYS */; INSERT INTO "AppNamespace" (`Id`, `Name`, `AppId`, `Format`, `Comment`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (1000, 'application', 'consumer-test-app-id-0', 'properties', 'default app namespace', 'apollo', 'apollo'); INSERT INTO "AppNamespace" (`Id`, `Name`, `AppId`, `Format`, `Comment`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (2000, 'application', 'consumer-test-app-id-1', 'properties', 'default app namespace', 'apollo', 'apollo'); /*!40000 ALTER TABLE `AppNamespace` ENABLE KEYS */; /*!40000 ALTER TABLE `Consumer` DISABLE KEYS */; INSERT INTO "Consumer" (`Id`, `AppId`, `Name`, `OrgId`, `OrgName`, `OwnerName`, `OwnerEmail`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (1000, 'consumer-test-app-role', 'consumer-test-app-role', 'TEST2', '样例部门2', 'apollo', 'apollo@acme.com', 'apollo', 'apollo'); /*!40000 ALTER TABLE `Consumer` ENABLE KEYS */; /*!40000 ALTER TABLE `ConsumerAudit` DISABLE KEYS */; /*!40000 ALTER TABLE `ConsumerAudit` ENABLE KEYS */; /*!40000 ALTER TABLE `ConsumerRole` DISABLE KEYS */; INSERT INTO "ConsumerRole" (`Id`, `ConsumerId`, `RoleId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (1000, 1000, 1000, 'apollo', 'apollo'); INSERT INTO "ConsumerRole" (`Id`, `ConsumerId`, `RoleId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (2000, 1000, 11000, 'apollo', 'apollo'); /*!40000 ALTER TABLE `ConsumerRole` ENABLE KEYS */; /*!40000 ALTER TABLE `ConsumerToken` DISABLE KEYS */; INSERT INTO "ConsumerToken" (`Id`, `ConsumerId`, `Token`, `RateLimit`, `Expires`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (1000, 1000, '3c16bf5b1f44b465179253442460e8c0ad845289', 20, '2098-12-31 10:00:00', 'apollo', 'apollo'); /*!40000 ALTER TABLE `ConsumerToken` ENABLE KEYS */; /*!40000 ALTER TABLE `Favorite` DISABLE KEYS */; /*!40000 ALTER TABLE `Favorite` ENABLE KEYS */; /*!40000 ALTER TABLE `Permission` DISABLE KEYS */; INSERT INTO "Permission" (`Id`, `PermissionType`, `TargetId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (1000, 'AssignRole', 'consumer-test-app-id-0', 'apollo', 'apollo'); INSERT INTO "Permission" (`Id`, `PermissionType`, `TargetId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (2000, 'CreateNamespace', 'consumer-test-app-id-0', 'apollo', 'apollo'); INSERT INTO "Permission" (`Id`, `PermissionType`, `TargetId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (3000, 'CreateCluster', 'consumer-test-app-id-0', 'apollo', 'apollo'); INSERT INTO "Permission" (`Id`, `PermissionType`, `TargetId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (4000, 'ManageAppMaster', 'consumer-test-app-id-0', 'apollo', 'apollo'); INSERT INTO "Permission" (`Id`, `PermissionType`, `TargetId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (5000, 'ModifyNamespace', 'consumer-test-app-id-0+application', 'apollo', 'apollo'); INSERT INTO "Permission" (`Id`, `PermissionType`, `TargetId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (6000, 'ReleaseNamespace', 'consumer-test-app-id-0+application', 'apollo', 'apollo'); INSERT INTO "Permission" (`Id`, `PermissionType`, `TargetId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (7000, 'ModifyNamespace', 'consumer-test-app-id-0+application+DEV', 'apollo', 'apollo'); INSERT INTO "Permission" (`Id`, `PermissionType`, `TargetId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (8000, 'ReleaseNamespace', 'consumer-test-app-id-0+application+DEV', 'apollo', 'apollo'); INSERT INTO "Permission" (`Id`, `PermissionType`, `TargetId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (9000, 'CreateNamespace', 'consumer-test-app-id-1', 'apollo', 'apollo'); INSERT INTO "Permission" (`Id`, `PermissionType`, `TargetId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (10000, 'AssignRole', 'consumer-test-app-id-1', 'apollo', 'apollo'); INSERT INTO "Permission" (`Id`, `PermissionType`, `TargetId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (11000, 'CreateCluster', 'consumer-test-app-id-1', 'apollo', 'apollo'); INSERT INTO "Permission" (`Id`, `PermissionType`, `TargetId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (12000, 'ManageAppMaster', 'consumer-test-app-id-1', 'apollo', 'apollo'); INSERT INTO "Permission" (`Id`, `PermissionType`, `TargetId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (13000, 'ModifyNamespace', 'consumer-test-app-id-1+application', 'apollo', 'apollo'); INSERT INTO "Permission" (`Id`, `PermissionType`, `TargetId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (14000, 'ReleaseNamespace', 'consumer-test-app-id-1+application', 'apollo', 'apollo'); INSERT INTO "Permission" (`Id`, `PermissionType`, `TargetId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (15000, 'ModifyNamespace', 'consumer-test-app-id-1+application+DEV', 'apollo', 'apollo'); INSERT INTO "Permission" (`Id`, `PermissionType`, `TargetId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (16000, 'ReleaseNamespace', 'consumer-test-app-id-1+application+DEV', 'apollo', 'apollo'); /*!40000 ALTER TABLE `Permission` ENABLE KEYS */; /*!40000 ALTER TABLE `Role` DISABLE KEYS */; INSERT INTO "Role" (`Id`, `RoleName`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (1000, 'Master+consumer-test-app-id-0', 'apollo', 'apollo'); INSERT INTO "Role" (`Id`, `RoleName`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (2000, 'ManageAppMaster+consumer-test-app-id-0', 'apollo', 'apollo'); INSERT INTO "Role" (`Id`, `RoleName`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (3000, 'ModifyNamespace+consumer-test-app-id-0+application', 'apollo', 'apollo'); INSERT INTO "Role" (`Id`, `RoleName`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (4000, 'ReleaseNamespace+consumer-test-app-id-0+application', 'apollo', 'apollo'); INSERT INTO "Role" (`Id`, `RoleName`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (5000, 'ModifyNamespace+consumer-test-app-id-0+application+DEV', 'apollo', 'apollo'); INSERT INTO "Role" (`Id`, `RoleName`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (6000, 'ReleaseNamespace+consumer-test-app-id-0+application+DEV', 'apollo', 'apollo'); INSERT INTO "Role" (`Id`, `RoleName`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (7000, 'Master+consumer-test-app-id-1', 'apollo', 'apollo'); INSERT INTO "Role" (`Id`, `RoleName`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (8000, 'ManageAppMaster+consumer-test-app-id-1', 'apollo', 'apollo'); INSERT INTO "Role" (`Id`, `RoleName`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (9000, 'ModifyNamespace+consumer-test-app-id-1+application', 'apollo', 'apollo'); INSERT INTO "Role" (`Id`, `RoleName`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (10000, 'ReleaseNamespace+consumer-test-app-id-1+application', 'apollo', 'apollo'); INSERT INTO "Role" (`Id`, `RoleName`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (11000, 'ModifyNamespace+consumer-test-app-id-1+application+DEV', 'apollo', 'apollo'); INSERT INTO "Role" (`Id`, `RoleName`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (12000, 'ReleaseNamespace+consumer-test-app-id-1+application+DEV', 'apollo', 'apollo'); /*!40000 ALTER TABLE `Role` ENABLE KEYS */; /*!40000 ALTER TABLE `RolePermission` DISABLE KEYS */; INSERT INTO "RolePermission" (`Id`, `RoleId`, `PermissionId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (1000, 1000, 1000, 'apollo', 'apollo'); INSERT INTO "RolePermission" (`Id`, `RoleId`, `PermissionId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (2000, 1000, 2000, 'apollo', 'apollo'); INSERT INTO "RolePermission" (`Id`, `RoleId`, `PermissionId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (3000, 1000, 3000, 'apollo', 'apollo'); INSERT INTO "RolePermission" (`Id`, `RoleId`, `PermissionId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (4000, 2000, 4000, 'apollo', 'apollo'); INSERT INTO "RolePermission" (`Id`, `RoleId`, `PermissionId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (5000, 3000, 5000, 'apollo', 'apollo'); INSERT INTO "RolePermission" (`Id`, `RoleId`, `PermissionId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (6000, 4000, 6000, 'apollo', 'apollo'); INSERT INTO "RolePermission" (`Id`, `RoleId`, `PermissionId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (7000, 5000, 7000, 'apollo', 'apollo'); INSERT INTO "RolePermission" (`Id`, `RoleId`, `PermissionId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (8000, 6000, 8000, 'apollo', 'apollo'); INSERT INTO "RolePermission" (`Id`, `RoleId`, `PermissionId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (9000, 7000, 9000, 'apollo', 'apollo'); INSERT INTO "RolePermission" (`Id`, `RoleId`, `PermissionId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (10000, 7000, 10000, 'apollo', 'apollo'); INSERT INTO "RolePermission" (`Id`, `RoleId`, `PermissionId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (11000, 7000, 11000, 'apollo', 'apollo'); INSERT INTO "RolePermission" (`Id`, `RoleId`, `PermissionId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (12000, 8000, 12000, 'apollo', 'apollo'); INSERT INTO "RolePermission" (`Id`, `RoleId`, `PermissionId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (13000, 9000, 13000, 'apollo', 'apollo'); INSERT INTO "RolePermission" (`Id`, `RoleId`, `PermissionId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (14000, 10000, 14000, 'apollo', 'apollo'); INSERT INTO "RolePermission" (`Id`, `RoleId`, `PermissionId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (15000, 11000, 15000, 'apollo', 'apollo'); INSERT INTO "RolePermission" (`Id`, `RoleId`, `PermissionId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (16000, 12000, 16000, 'apollo', 'apollo'); INSERT INTO "RolePermission" (`Id`, `RoleId`, `PermissionId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (17000, 13000, 17000, 'apollo', 'apollo'); INSERT INTO "RolePermission" (`Id`, `RoleId`, `PermissionId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (18000, 13000, 18000, 'apollo', 'apollo'); INSERT INTO "RolePermission" (`Id`, `RoleId`, `PermissionId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (19000, 13000, 19000, 'apollo', 'apollo'); INSERT INTO "RolePermission" (`Id`, `RoleId`, `PermissionId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (20000, 14000, 20000, 'apollo', 'apollo'); INSERT INTO "RolePermission" (`Id`, `RoleId`, `PermissionId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (21000, 15000, 21000, 'apollo', 'apollo'); INSERT INTO "RolePermission" (`Id`, `RoleId`, `PermissionId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (22000, 16000, 22000, 'apollo', 'apollo'); INSERT INTO "RolePermission" (`Id`, `RoleId`, `PermissionId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (23000, 17000, 23000, 'apollo', 'apollo'); INSERT INTO "RolePermission" (`Id`, `RoleId`, `PermissionId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (24000, 18000, 24000, 'apollo', 'apollo'); /*!40000 ALTER TABLE `RolePermission` ENABLE KEYS */; /*!40000 ALTER TABLE `UserRole` DISABLE KEYS */; INSERT INTO "UserRole" (`Id`, `UserId`, `RoleId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (1000, 'apollo', 1000, 'apollo', 'apollo'); INSERT INTO "UserRole" (`Id`, `UserId`, `RoleId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (2000, 'apollo', 3000, 'apollo', 'apollo'); INSERT INTO "UserRole" (`Id`, `UserId`, `RoleId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (3000, 'apollo', 4000, 'apollo', 'apollo'); INSERT INTO "UserRole" (`Id`, `UserId`, `RoleId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (4000, 'apollo', 7000, 'apollo', 'apollo'); INSERT INTO "UserRole" (`Id`, `UserId`, `RoleId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (5000, 'apollo', 9000, 'apollo', 'apollo'); INSERT INTO "UserRole" (`Id`, `UserId`, `RoleId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (6000, 'apollo', 10000, 'apollo', 'apollo'); INSERT INTO "UserRole" (`Id`, `UserId`, `RoleId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (7000, 'apollo', 13000, 'apollo', 'apollo'); INSERT INTO "UserRole" (`Id`, `UserId`, `RoleId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (8000, 'apollo', 15000, 'apollo', 'apollo'); INSERT INTO "UserRole" (`Id`, `UserId`, `RoleId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (9000, 'apollo', 16000, 'apollo', 'apollo'); /*!40000 ALTER TABLE `UserRole` ENABLE KEYS */; /*!40000 ALTER TABLE `Users` DISABLE KEYS */; INSERT INTO "Users" (`Id`, `Username`, `Password`, `UserDisplayName`, `Email`, `Enabled`) VALUES (1000, 'apollo', '$2a$10$7r20uS.BQ9uBpf3Baj3uQOZvMVvB1RN3PYoKE94gtz2.WAOuiiwXS', 'apollo', 'apollo@acme.com', 1); /*!40000 ALTER TABLE `Users` ENABLE KEYS */; /*!40101 SET SQL_MODE=IFNULL(@OLD_SQL_MODE, '') */; /*!40014 SET FOREIGN_KEY_CHECKS=IFNULL(@OLD_FOREIGN_KEY_CHECKS, 1) */; /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; /*!40111 SET SQL_NOTES=IFNULL(@OLD_SQL_NOTES, 1) */; ================================================ FILE: apollo-portal/src/test/resources/sql/permission/RolePermissionServiceTest.deleteRolePermissionsByAppIdWithClusterRoles.sql ================================================ -- Copyright 2025 Apollo Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. -- You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. INSERT INTO "Permission" (`Id`, `PermissionType`, `TargetId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (1500, 'ModifyNamespacesInCluster', 'clusterApp+DEV+default', 'someOperator', 'someOperator'), (1501, 'ReleaseNamespacesInCluster', 'clusterApp+DEV+default', 'someOperator', 'someOperator'); INSERT INTO "Role" (`Id`, `RoleName`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (1500, 'ModifyNamespacesInCluster+clusterApp+DEV+default', 'someOperator', 'someOperator'), (1501, 'ReleaseNamespacesInCluster+clusterApp+DEV+default', 'someOperator', 'someOperator'); INSERT INTO "RolePermission" (`Id`, `RoleId`, `PermissionId`) VALUES (1500, 1500, 1500), (1501, 1501, 1501); ================================================ FILE: apollo-portal/src/test/resources/sql/permission/consumer_role_permission_service/test_get_user_permission_set_different_users.sql ================================================ -- -- Copyright 2025 Apollo Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. -- You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. -- -- User 7's permissions: app:app2 INSERT INTO "ConsumerRole" ("Id", "ConsumerId", "RoleId", "DataChange_CreatedBy", "DataChange_LastModifiedBy") VALUES (1003, 7, 400, 'test-operator', 'test-operator'); INSERT INTO "RolePermission" ("RoleId", "PermissionId") VALUES (400, 401); INSERT INTO "Permission" ("Id", "PermissionType", "TargetId") VALUES (401, 'app', 'app2'); -- User 8's permissions: namespace:ns2, cluster:cluster2 INSERT INTO "ConsumerRole" ("Id", "ConsumerId", "RoleId", "DataChange_CreatedBy", "DataChange_LastModifiedBy") VALUES (1004, 8, 500, 'test-operator', 'test-operator'), (1005, 8, 501, 'test-operator', 'test-operator'); INSERT INTO "RolePermission" ("RoleId", "PermissionId") VALUES (500, 502), (501, 503); INSERT INTO "Permission" ("Id", "PermissionType", "TargetId") VALUES (502, 'namespace', 'ns2'), (503, 'cluster', 'cluster2'); ================================================ FILE: apollo-portal/src/test/resources/sql/permission/consumer_role_permission_service/test_get_user_permission_set_no_roles.sql ================================================ -- -- Copyright 2025 Apollo Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. -- You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. -- -- Ensure that user 4 has no role association records in the ConsumerRole table -- (no data needs to be inserted, or explicitly delete possible interfering data) DELETE FROM "ConsumerRole" WHERE "ConsumerId" = 4; ================================================ FILE: apollo-portal/src/test/resources/sql/permission/consumer_role_permission_service/test_get_user_permission_set_roles_without_permissions.sql ================================================ -- -- Copyright 2025 Apollo Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. -- You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. -- -- Insert role association for user 5 (role 100), but role 100 has no permissions INSERT INTO "ConsumerRole" ("Id", "ConsumerId", "RoleId", "DataChange_CreatedBy", "DataChange_LastModifiedBy") VALUES (1000, 5, 100, 'test-operator', 'test-operator'); -- Insert role association record for user 5 -- Role 100 has no associated permissions in RolePermission table (no need to insert RolePermission records) ================================================ FILE: apollo-portal/src/test/resources/sql/permission/consumer_role_permission_service/test_get_user_permission_set_with_permissions.sql ================================================ -- -- Copyright 2025 Apollo Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. -- You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. -- -- Insert role associations for user 6 (roles 200 and 201) INSERT INTO "ConsumerRole" ("Id", "ConsumerId", "RoleId", "DataChange_CreatedBy", "DataChange_LastModifiedBy") VALUES (1001, 6, 200, 'test-operator', 'test-operator'), (1002, 6, 201, 'test-operator', 'test-operator'); -- Insert role-permission associations (role 200 associated with permission 300, role 201 associated with permissions 301 and 302) INSERT INTO "RolePermission" ("RoleId", "PermissionId") -- Assuming RolePermission table structure is (RoleId, PermissionId) VALUES (200, 300), (201, 301), (201, 302); -- Insert permission details (permission table) INSERT INTO "Permission" ("Id", "PermissionType", "TargetId") -- Assuming Permission table structure is (Id, PermissionType, TargetId) VALUES (300, 'app', 'app1'), (301, 'namespace', 'ns1'), (302, 'cluster', 'cluster1'); ================================================ FILE: apollo-portal/src/test/resources/sql/permission/insert-test-consumerroles.sql ================================================ -- -- Copyright 2024 Apollo Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. -- You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. -- INSERT INTO "ConsumerRole" (`Id`, `ConsumerId`, `RoleId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (890, 1, 990, 'someOperator', 'someOperator'); INSERT INTO "ConsumerRole" (`Id`, `ConsumerId`, `RoleId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (891, 2, 990, 'someOperator', 'someOperator'); ================================================ FILE: apollo-portal/src/test/resources/sql/permission/insert-test-getUserPermissionSet.sql ================================================ -- -- Copyright 2025 Apollo Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. -- You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. -- -- Clean up possible legacy data (optional, will be done in cleanup.sql, written again here for safety) DELETE FROM `RolePermission` WHERE `RoleId` IN (2001, 2002); DELETE FROM `UserRole` WHERE `RoleId` IN (2001, 2002); DELETE FROM `Permission` WHERE `Id` IN (1001, 1002); DELETE FROM `Role` WHERE `Id` IN (2001, 2002); -- Permissions INSERT INTO `Permission` (`Id`, `PermissionType`, `TargetId`, `IsDeleted`, `DeletedAt`, `DataChange_CreatedBy`, `DataChange_CreatedTime`, `DataChange_LastModifiedBy`, `DataChange_LastTime`) VALUES (1001, 'ModifyNamespace', 'someApp+someNamespace', 0, 0, 'test', NOW(), 'test', NOW()), (1002, 'ReleaseNamespace', 'someApp+someNamespace', 0, 0, 'test', NOW(), 'test', NOW()); -- Roles INSERT INTO `Role` (`Id`, `RoleName`, `IsDeleted`, `DeletedAt`, `DataChange_CreatedBy`, `DataChange_CreatedTime`, `DataChange_LastModifiedBy`, `DataChange_LastTime`) VALUES (2001, 'role-with-modify', 0, 0, 'test', NOW(), 'test', NOW()), (2002, 'role-with-release', 0, 0, 'test', NOW(), 'test', NOW()); -- Role-Permission associations INSERT INTO `RolePermission` (`RoleId`, `PermissionId`, `IsDeleted`, `DeletedAt`, `DataChange_CreatedBy`, `DataChange_CreatedTime`, `DataChange_LastModifiedBy`, `DataChange_LastTime`) VALUES (2001, 1001, 0, 0, 'test', NOW(), 'test', NOW()), (2002, 1002, 0, 0, 'test', NOW(), 'test', NOW()); -- User-Role associations (apollo has two permissions, nobody has no roles) INSERT INTO `UserRole` (`UserId`, `RoleId`, `IsDeleted`, `DeletedAt`, `DataChange_CreatedBy`, `DataChange_CreatedTime`, `DataChange_LastModifiedBy`, `DataChange_LastTime`) VALUES ('apollo', 2001, 0, 0, 'test', NOW(), 'test', NOW()), ('apollo', 2002, 0, 0, 'test', NOW(), 'test', NOW()); ================================================ FILE: apollo-portal/src/test/resources/sql/permission/insert-test-permissions.sql ================================================ -- -- Copyright 2024 Apollo Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. -- You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. -- INSERT INTO "Permission" (`Id`, `PermissionType`, `TargetId`) VALUES (990, 'somePermissionType', 'someTargetId'); INSERT INTO "Permission" (`Id`, `PermissionType`, `TargetId`) VALUES (991, 'anotherPermissionType', 'anotherTargetId'); ================================================ FILE: apollo-portal/src/test/resources/sql/permission/insert-test-rolepermissions.sql ================================================ -- -- Copyright 2024 Apollo Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. -- You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. -- INSERT INTO "RolePermission" (`Id`, `RoleId`, `PermissionId`) VALUES (990, 990, 990); INSERT INTO "RolePermission" (`Id`, `RoleId`, `PermissionId`) VALUES (991, 990, 991); ================================================ FILE: apollo-portal/src/test/resources/sql/permission/insert-test-roles.sql ================================================ -- -- Copyright 2024 Apollo Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. -- You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. -- INSERT INTO "Role" (`Id`, `RoleName`) VALUES (990, 'someRoleName'); INSERT INTO "Role" (`Id`, `RoleName`) VALUES (991, 'anotherRoleName'); INSERT INTO "Role" (`Id`, `RoleName`) VALUES (992, 'apolloRoleName'); ================================================ FILE: apollo-portal/src/test/resources/sql/permission/insert-test-userroles.sql ================================================ -- -- Copyright 2024 Apollo Authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. -- You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. -- INSERT INTO "UserRole" (`Id`, `UserId`, `RoleId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (990, 'someUser', 990, 'someOperator', 'someOperator'); INSERT INTO "UserRole" (`Id`, `UserId`, `RoleId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (991, 'anotherUser', 990, 'someOperator', 'someOperator'); INSERT INTO "UserRole" (`Id`, `UserId`, `RoleId`, `DataChange_CreatedBy`, `DataChange_LastModifiedBy`) VALUES (992, 'apollo', 992, 'someOperator', 'someOperator'); ================================================ FILE: apollo-portal/src/test/resources/static/scripts/test_hasDuplicateKeys.js ================================================ /* * Copyright 2024 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ /* * Test for hasDuplicateKeys function. * Run with Node.js: node test_hasDuplicateKeys.js */ // Test harness for hasDuplicateKeys function (not a direct copy) function hasDuplicateKeys(text) { try { // Character-level scan because JSON.parse reviver cannot detect // duplicates (browser already deduplicates). // Note: keys are compared after JSON decoding, so unicode escape equivalences are resolved. // Strategy: scan for "key": patterns respecting nesting depth. var i = 0; var len = text.length; var depth = 0; var keySets = []; while (i < len) { var ch = text.charAt(i); if (ch === '"') { // Read the full string var strStart = i; i++; // skip opening quote while (i < len) { if (text.charAt(i) === '\\') { i += 2; // skip escaped char } else if (text.charAt(i) === '"') { break; } else { i++; } } var strEnd = i; i++; // skip closing quote // Check if this string is a key (followed by ':') var j = i; while (j < len && (text.charAt(j) === ' ' || text.charAt(j) === '\t' || text.charAt(j) === '\n' || text.charAt(j) === '\r')) { j++; } if (j < len && text.charAt(j) === ':') { var rawKey = text.substring(strStart + 1, strEnd); var key; try { key = JSON.parse('"' + rawKey + '"'); } catch (e) { // If decoding fails, fall back to raw key (invalid JSON, but we still compare raw) key = rawKey; } if (depth >= 0 && depth < keySets.length) { if (key in keySets[depth]) { return true; } keySets[depth][key] = true; } } } else if (ch === '{') { depth++; while (keySets.length <= depth) { keySets.push(Object.create(null)); } keySets[depth] = Object.create(null); i++; } else if (ch === '}') { depth--; i++; } else { i++; } } return false; } catch (e) { return false; } } // Test cases function runTests() { var passed = 0; var total = 0; function assert(desc, expected, actual) { total++; if (expected === actual) { passed++; console.log('✅ ' + desc); } else { console.log('❌ ' + desc + ' — expected ' + expected + ', got ' + actual); } } // Helper to ensure backslashes are literal in JSON text // In JavaScript strings, a single backslash must be escaped as \\ // So to represent the JSON text {"\u0061":1}, we need '{"\\u0061":1}' // This helper makes it explicit. function jsonText(str) { return str; } // No duplicates assert('Empty object', false, hasDuplicateKeys('{}')); assert('Simple object', false, hasDuplicateKeys('{"a":1}')); assert('Nested object', false, hasDuplicateKeys('{"a":{"b":2}}')); assert('Multiple keys', false, hasDuplicateKeys('{"a":1,"b":2}')); // Raw duplicates assert('Raw duplicate keys', true, hasDuplicateKeys('{"a":1,"a":2}')); assert('Raw duplicate nested', true, hasDuplicateKeys('{"a":1,"b":{"c":3,"c":4}}')); // Unicode escape equivalence – critical: backslash must be literal in JSON // JSON text: {"\u0061":1,"a":2} (backslash-u-0061) // JavaScript string: '{"\\u0061":1,"a":2}' assert('Unicode escape duplicate', true, hasDuplicateKeys('{"\\u0061":1,"a":2}')); assert('Unicode escape duplicate reverse', true, hasDuplicateKeys('{"a":1,"\\u0061":2}')); // Same escape appears twice assert('Same escape duplicate', true, hasDuplicateKeys('{"\\u0061":1,"\\u0061":2}')); // Mixed escapes, one duplicate assert('Mixed escapes duplicate', true, hasDuplicateKeys('{"\\u0061":1,"\\u0062":2,"a":3}')); // a duplicate with \u0061 // Different escapes, no duplicate assert('Different escapes not duplicate', false, hasDuplicateKeys('{"\\u0061":1,"\\u0062":2}')); // Additional Unicode equivalence cases // Uppercase A assert('Unicode uppercase duplicate', true, hasDuplicateKeys('{"\\u0041":1,"A":2}')); // Digit (unicode escape for digit '1' is \u0031) assert('Unicode digit duplicate', true, hasDuplicateKeys('{"\\u0031":1,"1":2}')); // Chinese character: \u4e2d = 中 assert('Unicode Chinese duplicate', true, hasDuplicateKeys('{"\\u4e2d":1,"中":2}')); // Surrogate pair? Not needed for key detection (JSON strings are Unicode) // Nested with Unicode equivalence assert('Nested Unicode duplicate', true, hasDuplicateKeys('{"outer":{"\\u0061":1,"a":2}}')); assert('Deep nested duplicate', true, hasDuplicateKeys('{"a":{"b":{"\\u0061":1,"a":2}}}')); // Other JSON escape sequences (should not be considered equivalent to unescaped chars) // \n is a control character, there is no 'n' key. assert('Escape n', false, hasDuplicateKeys('{"\\n":1,"a":2}')); // \" is a quote character, not a plain quote assert('Escape quote', false, hasDuplicateKeys('{"\\"":1,"a":2}')); // \\ is a backslash, not a plain backslash assert('Escape backslash', false, hasDuplicateKeys('{"\\\\":1,"a":2}')); // Double backslash before u (literal backslash + u0061) vs plain a – not equivalent assert('Literal backslash-u0061 vs a', false, hasDuplicateKeys('{"\\\\u0061":1,"a":2}')); // Invalid JSON (should not crash) assert('Invalid JSON missing quote', false, hasDuplicateKeys('{"a:1}')); // Malformed escape (incomplete \u) assert('Incomplete Unicode escape', false, hasDuplicateKeys('{"\\u00":1}')); // Edge cases suggested by CodeRabbit assert('Object inside array with duplicate', true, hasDuplicateKeys('[{"a":1,"a":2}]')); // Unicode duplicate inside array assert('Object inside array with Unicode duplicate', true, hasDuplicateKeys('[{"\\u0061":1,"a":2}]')); assert('String value resembling a key', false, hasDuplicateKeys('{"a":"b:c","d":1}')); assert('Sibling objects with same keys', false, hasDuplicateKeys('{"x":{"a":1},"y":{"a":1}}')); // Sibling objects with Unicode equivalent keys (should not be duplicate because different scopes) assert('Sibling objects with Unicode equivalent keys', false, hasDuplicateKeys('{"x":{"\\u0061":1},"y":{"a":1}}')); // Additional edge: empty key (allowed in JSON) assert('Empty key duplicate', true, hasDuplicateKeys('{"":1,"":2}')); // Unicode escape for empty string? impossible. // Ensure detection works with spaces/tabs/newlines in JSON assert('Pretty JSON with duplicate', true, hasDuplicateKeys('{\n "\\u0061": 1,\n "a": 2\n}')); console.log('\n' + passed + '/' + total + ' tests passed'); return passed === total; } if (typeof module !== 'undefined' && module.exports) { // Node.js environment module.exports = { hasDuplicateKeys: hasDuplicateKeys, runTests: runTests }; if (require.main === module) { process.exit(runTests() ? 0 : 1); } } else { // Browser environment runTests(); } ================================================ FILE: apollo-portal/src/test/resources/yaml/case1.yaml ================================================ # # Copyright 2024 Apollo Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # root: key1: "someValue" key2: 100 key3: key4: key5: '(%sender%) %message%' key6: '* %sender% %message%' # commented: "xxx" list: - 'item 1' - 'item 2' intList: - 100 - 200 listOfMap: - key: '#mychannel' value: '' - key: '#myprivatechannel' value: 'mypassword' listOfList: - - 'a1' - 'a2' - - 'b1' - 'b2' listOfList2: [ ['a1', 'a2'], ['b1', 'b2'] ] ================================================ FILE: apollo-portal/src/test/resources/yaml/case2.yaml ================================================ # # Copyright 2024 Apollo Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # root: key1: "someValue" key2: 100 key1: "anotherValue" ================================================ FILE: apollo-portal/src/test/resources/yaml/case3.yaml ================================================ # # Copyright 2024 Apollo Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # !!javax.script.ScriptEngineManager [ !!java.net.URLClassLoader [[ !!java.net.URL ["http://localhost"] ]] ] ================================================ FILE: changes/changes-1.9.0.md ================================================ Changes by Version ================== Release Notes. Apollo 1.9.0 ------------------ * [extend DataChange_CreatedBy, DataChange_LastModifiedBy from 32 to 64.](https://github.com/ctripcorp/apollo/pull/3552) * [add spring configuration metadata](https://github.com/ctripcorp/apollo/pull/3553) * [fix #3551 and optimize ldap samples ](https://github.com/ctripcorp/apollo/pull/3561) * [update wiki url and refine documentation](https://github.com/ctripcorp/apollo/pull/3563) * [slim docker images](https://github.com/ctripcorp/apollo/pull/3572) * [add network strategy guideline to docker quick start](https://github.com/ctripcorp/apollo/pull/3574) * [Added support for consul service discovery](https://github.com/ctripcorp/apollo/pull/3575) * [disable consul in apollo-assembly by default ](https://github.com/ctripcorp/apollo/pull/3585) * [ServiceBootstrap unit test fix](https://github.com/ctripcorp/apollo/pull/3593) * [replace http client implementation with interface ](https://github.com/ctripcorp/apollo/pull/3594) * [Allow users to inject customized instance via ApolloInjectorCustomizer](https://github.com/ctripcorp/apollo/pull/3602) * [Fixes #3606](https://github.com/ctripcorp/apollo/pull/3609) * [Bump xstream from 1.4.15 to 1.4.16](https://github.com/ctripcorp/apollo/pull/3611) * [docs: user practices. Alibaba Sentinel Dashboard Push Rules to apollo](https://github.com/ctripcorp/apollo/pull/3617) * [update known users](https://github.com/ctripcorp/apollo/pull/3619) * [add maven deploy action](https://github.com/ctripcorp/apollo/pull/3620) * [fix the issue that access key doesn't work if appid passed is in different case](https://github.com/ctripcorp/apollo/pull/3627) * [fix oidc logout](https://github.com/ctripcorp/apollo/pull/3628) * [docs: third party sdk nodejs client](https://github.com/ctripcorp/apollo/pull/3632) * [update known users](https://github.com/ctripcorp/apollo/pull/3633) * [docs: use docsify pagination plugin](https://github.com/ctripcorp/apollo/pull/3634) * [apollo client to support jdk16](https://github.com/ctripcorp/apollo/pull/3646) * [add English version of readme](https://github.com/ctripcorp/apollo/pull/3656) * [update known users](https://github.com/ctripcorp/apollo/pull/3657) * [update apolloconfig.com domain](https://github.com/ctripcorp/apollo/pull/3658) * [localize css to speed up the loading of google fonts](https://github.com/ctripcorp/apollo/pull/3660) * [test(apollo-client): use assertEquals instead of assertThat](https://github.com/ctripcorp/apollo/pull/3667) * [test(apollo-client): make timeout more longer when long poll](https://github.com/ctripcorp/apollo/pull/3668) * [fix unit test](https://github.com/ctripcorp/apollo/pull/3669) * [解决日志系统未初始化完成时,apollo 的加载日志没法输出问题](https://github.com/ctripcorp/apollo/pull/3677) * [fix[apollo-configService]: Solve configService startup exception](https://github.com/ctripcorp/apollo/pull/3679) * [Community Governance Proposal](https://github.com/ctripcorp/apollo/pull/3670) * [增加阿波罗client的php库](https://github.com/ctripcorp/apollo/pull/3682) * [Bump xstream from 1.4.16 to 1.4.17](https://github.com/ctripcorp/apollo/pull/3692) * [Improve the nacos registry configuration document](https://github.com/ctripcorp/apollo/pull/3695) * [Remove redundant invoke of trySyncFromUpstream](https://github.com/ctripcorp/apollo/pull/3699) * [add apollo team introduction and community related contents](https://github.com/ctripcorp/apollo/pull/3713) * [fix oidc sql](https://github.com/ctripcorp/apollo/pull/3720) * [feat(apollo-client): add method interestedChangedKeys to ConfigChangeEvent](https://github.com/ctripcorp/apollo/pull/3666) * [add More... link for known users](https://github.com/ctripcorp/apollo/pull/3757) * [update OIDC documentation](https://github.com/ctripcorp/apollo/pull/3766) * [Improve the item-value length limit configuration document](https://github.com/ctripcorp/apollo/pull/3789) * [Use queue#take instead of poll](https://github.com/ctripcorp/apollo/pull/3765) * [feature: add Spring Boot 2.4 config data loader support](https://github.com/ctripcorp/apollo/pull/3754) * [feat(open-api): get authorized apps](https://github.com/ctripcorp/apollo/pull/3647) * [feature: shared session for multi apollo portal](https://github.com/ctripcorp/apollo/pull/3786) * [feature: add email for select user on apollo portal](https://github.com/ctripcorp/apollo/pull/3797) * [feature: modify item comment valid size](https://github.com/ctripcorp/apollo/pull/3803) * [set default session store-type](https://github.com/ctripcorp/apollo/pull/3812) * [speed up the stale issue mark and close phase](https://github.com/ctripcorp/apollo/pull/3808) * [feature: add the delegating password encoder for apollo-portal simple auth](https://github.com/ctripcorp/apollo/pull/3804) * [support release apollo-client-config-data](https://github.com/ctripcorp/apollo/pull/3822) * [Fix possiable NPE](https://github.com/ctripcorp/apollo/pull/3832) * [Reduce bootstrap time in the situation with large properties](https://github.com/ctripcorp/apollo/pull/3816) * [docs: English catalog in sidebar](https://github.com/ctripcorp/apollo/pull/3831) * [fix the issue that release messages might be missed in certain scenarios](https://github.com/ctripcorp/apollo/pull/3819) * [use official docker images for manual kubernetes deployment](https://github.com/ctripcorp/apollo/pull/3840) * [fix size of create project button](https://github.com/ctripcorp/apollo/pull/3849) * [translation of "portal-how-to-enable-webhook-notification.md"](https://github.com/ctripcorp/apollo/pull/3847) * [feature: add history detail for not key-value type of namespace](https://github.com/ctripcorp/apollo/pull/3856) * [fix show-text-modal number display](https://github.com/ctripcorp/apollo/pull/3851) * [Lazy load ConfigUtil](https://github.com/ctripcorp/apollo/pull/3864) * [make jdbc session enable default](https://github.com/ctripcorp/apollo/pull/3869) * [support json/yaml/xml format for public namespace](https://github.com/ctripcorp/apollo/pull/3836) * [Translate application into 应用 not 项目](https://github.com/ctripcorp/apollo/pull/3877) * [add spring configuration metadata for property names cache](https://github.com/ctripcorp/apollo/pull/3879) * [Fix Multiple PropertySourcesPlaceholderConfigurer beans registered issue](https://github.com/ctripcorp/apollo/pull/3865) * [use jdk 8 to publish apollo-client-config-data](https://github.com/ctripcorp/apollo/pull/3880) * [fix apollo config data loader with profiles](https://github.com/ctripcorp/apollo/pull/3870) * [polish log](https://github.com/ctripcorp/apollo/pull/3882) * [add history query](https://github.com/ctripcorp/apollo/pull/3878) * [fix history query](https://github.com/ctripcorp/apollo/pull/3894) ------------------ All issues and pull requests are [here](https://github.com/ctripcorp/apollo/milestone/6?closed=1) ================================================ FILE: changes/changes-1.9.1.md ================================================ Changes by Version ================== Release Notes. Apollo 1.9.1 ------------------ * [Remove spring dependencies from internal code](https://github.com/apolloconfig/apollo/pull/3937) * [Fix issue: ingress syntax](https://github.com/apolloconfig/apollo/pull/3933) ------------------ All issues and pull requests are [here](https://github.com/apolloconfig/apollo/milestone/9?closed=1) ================================================ FILE: changes/changes-1.9.2.md ================================================ Changes by Version ================== Release Notes. Apollo 1.9.2 ------------------ * [Fix the issue that property placeholder doesn't work for dubbo reference beans](https://github.com/apolloconfig/apollo/pull/4161) * [Update xstream version to 1.4.18](https://github.com/apolloconfig/apollo/pull/4177) * [Fix the NPE occurred when using EnableApolloConfig with Spring 3.1.1](https://github.com/apolloconfig/apollo/pull/4179) * [Catch LinkageError for ClassLoaderUtil.isClassPresent in case class is present but is failed to load](https://github.com/apolloconfig/apollo/pull/4187) ------------------ All issues and pull requests are [here](https://github.com/apolloconfig/apollo/milestone/10?closed=1) ================================================ FILE: changes/changes-2.0.0.md ================================================ Changes by Version ================== Release Notes. Apollo 2.0.0 ------------------ * [Fix issue that the $ symbol is not used when reading shell variables](https://github.com/ctripcorp/apollo/pull/3890) * [Bump xstream from 1.4.17 to 1.4.18](https://github.com/apolloconfig/apollo/pull/3916) * [switch apollo.config-service log from warning to info level](https://github.com/ctripcorp/apollo/pull/3884) * [Make Access Key Timestamp check configurable](https://github.com/ctripcorp/apollo/pull/3908) * [remove ctrip profile](https://github.com/ctripcorp/apollo/pull/3920) * [Remove spring dependencies from internal code](https://github.com/apolloconfig/apollo/pull/3937) * [Fix issue: ingress syntax](https://github.com/apolloconfig/apollo/pull/3933) * [refactor: let open api more easier to use and development](https://github.com/apolloconfig/apollo/pull/3943) * [feat(scripts): use bash to call openapi](https://github.com/apolloconfig/apollo/pull/3980) * [Support search by item](https://github.com/apolloconfig/apollo/pull/3977) * [Implement password policies to avoid weak passwords](https://github.com/apolloconfig/apollo/pull/4008) * [Extend the gray release capability to support dimensions other than IP](https://github.com/apolloconfig/apollo/pull/4013) * [public namespace basic function](https://github.com/apolloconfig/apollo/pull/3850) * [Bump version to 2.0.0 and drop java 1.7 support](https://github.com/apolloconfig/apollo/pull/4015) * [Optimize home page style](https://github.com/apolloconfig/apollo/pull/4052) * [Support Java 17](https://github.com/apolloconfig/apollo/pull/4060) * [Optimize top navbar style](https://github.com/apolloconfig/apollo/pull/4073) * [Support export/import configs by apollo env](https://github.com/apolloconfig/apollo/pull/3947) * [Catch LinkageError for ClassLoaderUtil.isClassPresent in case class is present but is failed to load](https://github.com/apolloconfig/apollo/pull/4097) * [Split helm chart into another repo](https://github.com/apolloconfig/apollo/pull/4125) * [fix gray publish refresh item status](https://github.com/apolloconfig/apollo/pull/4128) * [Support only show difference keys when compare namespace](https://github.com/apolloconfig/apollo/pull/4165) * [Fix the issue that property placeholder doesn't work for dubbo reference beans](https://github.com/apolloconfig/apollo/pull/4175) * [Fix the NPE occurred when using EnableApolloConfig with Spring 3.1.1](https://github.com/apolloconfig/apollo/pull/4180) * [Bump guava from 29.0 to 31.0.1](https://github.com/apolloconfig/apollo/pull/4182) * [fix the json number display issue when it's longer than 16](https://github.com/apolloconfig/apollo/pull/4183) * [Bump client springboot version](https://github.com/apolloconfig/apollo/pull/4189) * [The release history of namespaces that are not properties will also show comments and release times](https://github.com/apolloconfig/apollo/pull/4198) * [Add unit tests for Utils](https://github.com/apolloconfig/apollo/pull/4193) * [Change Copy Right year to 2022](https://github.com/apolloconfig/apollo/pull/4202) * [Optimize create namespace page](https://github.com/apolloconfig/apollo/pull/4213) * [Allow disable apollo client cache](https://github.com/apolloconfig/apollo/pull/4199) * [Make password check not hardcoded](https://github.com/apolloconfig/apollo/pull/4207) * [Fix update user's password failure](https://github.com/apolloconfig/apollo/pull/4212) * [Fix bug: associated namespace display incorrect in text view](https://github.com/apolloconfig/apollo/pull/4219) * [Add Ordered interface to ProviderManager SPI](https://github.com/apolloconfig/apollo/pull/4218) * [Using commons-lang3 to replace commons-lang](https://github.com/apolloconfig/apollo/pull/4225) * [optimize import/export config](https://github.com/apolloconfig/apollo/pull/4231) * [Configure publish and rollback modal boxes to add scrollbars](https://github.com/apolloconfig/apollo/pull/4251) * [fix import config bug](https://github.com/apolloconfig/apollo/pull/4262) * [Refactor the soft delete design](https://github.com/apolloconfig/apollo/pull/3866) * [Fix the potential data inconsistency issue](https://github.com/apolloconfig/apollo/pull/4256) * [Fix the deleted items display issue in text mode](https://github.com/apolloconfig/apollo/pull/4279) * [Upgrade spring boot to 2.6.6 and spring cloud to 2021.0.1](https://github.com/apolloconfig/apollo/pull/4295) * [Fix the apollo portal start failed issue](https://github.com/apolloconfig/apollo/pull/4298) * [fix: javax.net.ssl.SSLHandshakeException: No appropriate protocol](https://github.com/apolloconfig/apollo/pull/4308) * [Upgrade flyway to 8.0.5](https://github.com/apolloconfig/apollo/pull/4312) * [Broadcast ConfigChangeEvent using Spring ApplicationEvent](https://github.com/apolloconfig/apollo/pull/4305) * [Upgrade maven plugin versions to fix error in Java 17](https://github.com/apolloconfig/apollo/pull/4333) ------------------ All issues and pull requests are [here](https://github.com/ctripcorp/apollo/milestone/8?closed=1) ================================================ FILE: changes/changes-2.0.1.md ================================================ Changes by Version ================== Release Notes. Apollo 2.0.1 ------------------ * [Upgrade spring boot to fix search user issue](https://github.com/apolloconfig/apollo/pull/4366) * [Fix search user duplication issue](https://github.com/apolloconfig/apollo/pull/4371) * [Fix the npe issue for old version of gray release rules](https://github.com/apolloconfig/apollo/pull/4382) * [Fix the delete AppNamespace failed issue](https://github.com/apolloconfig/apollo/pull/4388) * [Bump okhttp3 from 3.11.0 to 4.9.3](https://github.com/apolloconfig/apollo/pull/4392) ------------------ All issues and pull requests are [here](https://github.com/apolloconfig/apollo/milestone/12?closed=1) ================================================ FILE: changes/changes-2.1.0.md ================================================ Changes by Version ================== Release Notes. Apollo 2.1.0 ------------------ * [fix:occur a 400 error request when openapi key's parameter contain "a[0]"](https://github.com/apolloconfig/apollo/pull/4424) * [Upgrade mysql-connector-java version to fix possible transaction rollback failure issue](https://github.com/apolloconfig/apollo/pull/4425) * [Remove database migration tool Flyway](https://github.com/apolloconfig/apollo/pull/4361) * [Optimize Spring-Security Firewall Deny Request Response 400](https://github.com/apolloconfig/apollo/pull/4428) * [Optimize the UI experience of open platform authorization management](https://github.com/apolloconfig/apollo/pull/4436) * [Allow users to associate multiple public namespaces at a time](https://github.com/apolloconfig/apollo/pull/4437) * [Move apollo-demo, scripts/docker-quick-start and scripts/apollo-on-kubernetes out of main repository](https://github.com/apolloconfig/apollo/pull/4440) * [Add search key when comparing Configuration items](https://github.com/apolloconfig/apollo/pull/4459) * [A user-friendly user management page for apollo portal](https://github.com/apolloconfig/apollo/pull/4464) * [Optimize performance of '/apps/{appId}/envs/{env}/clusters/{clusterName}/namespaces' interface queries](https://github.com/apolloconfig/apollo/pull/4473) * [Add a new API to load items with pagination](https://github.com/apolloconfig/apollo/pull/4468) * [fix(#4474):'openjdk:8-jre-alpine' potentially causing wrong number of cpu cores](https://github.com/apolloconfig/apollo/pull/4475) * [Switching spring-session serialization mode to json for compatibility with spring-security version updates](https://github.com/apolloconfig/apollo/pull/4484) * [fix(#4483):Fixed overwrite JSON type configuration being empty](https://github.com/apolloconfig/apollo/pull/4486) * [Allow users to delete AppNamespace](https://github.com/apolloconfig/apollo/pull/4499) * [fix the deleted at timestamp issue](https://github.com/apolloconfig/apollo/pull/4493) * [add configuration processor for portal developers](https://github.com/apolloconfig/apollo/pull/4521) * [Add a potential json value check feature](https://github.com/apolloconfig/apollo/pull/4519) * [Add index for table ReleaseHistory](https://github.com/apolloconfig/apollo/pull/4550) * [Add basic type check for Item value](https://github.com/apolloconfig/apollo/pull/4542) * [add an option to custom oidc userDisplayName](https://github.com/apolloconfig/apollo/pull/4507) * [fix openapi item with url illegalKey 400 error](https://github.com/apolloconfig/apollo/pull/4549) * [fix the exception occurred when publish/rollback namespaces with grayrelease](https://github.com/apolloconfig/apollo/pull/4564) * [fix create namespace with single dot 500 error](https://github.com/apolloconfig/apollo/pull/4568) * [Add nodejs client sdk and fix doc](https://github.com/apolloconfig/apollo/pull/4590) * [Move apollo-core, apollo-client, apollo-mockserver, apollo-openapi and apollo-client-config-data to apollo-java repo](https://github.com/apolloconfig/apollo/pull/4594) * [fix get the openapi interface that contains namespace information for deleted items](https://github.com/apolloconfig/apollo/pull/4596) * [A user-friendly config management page for apollo portal](https://github.com/apolloconfig/apollo/pull/4592) * [feat: support use database as a registry](https://github.com/apolloconfig/apollo/pull/4595) * [fix doc bug](https://github.com/apolloconfig/apollo/pull/4579) * [fix Grayscale release Item Value length limit can not be synchronized with its main version](https://github.com/apolloconfig/apollo/pull/4622) * [feat: use can change spring.profiles.active's value without rebuild project](https://github.com/apolloconfig/apollo/pull/4616) * [refactor: remove app.properties and move some config file's location](https://github.com/apolloconfig/apollo/pull/4637) * [Fix the problem of deleting blank items appear at the end](https://github.com/apolloconfig/apollo/pull/4662) * [Portal-UI adds serverConfig configuration management of ApolloConfigDB](https://github.com/apolloconfig/apollo/pull/4680) * [Enable login authentication for eureka](https://github.com/apolloconfig/apollo/pull/4663) * [Add github action to publish docker image](https://github.com/apolloconfig/apollo/pull/4685) ------------------ All issues and pull requests are [here](https://github.com/apolloconfig/apollo/milestone/11?closed=1) ================================================ FILE: changes/changes-2.2.0.md ================================================ Changes by Version ================== Release Notes. Apollo 2.2.0 ------------------ * [Fix the problem of inconsistent length of appId column](https://github.com/apolloconfig/apollo/pull/4725) * [Bump springcloud springboot version to solve cve problems](https://github.com/apolloconfig/apollo/pull/4712) * [rename mysql-connector-java to mysql-connector-j](https://github.com/apolloconfig/apollo/pull/4748) * [Bump springboot version from 2.7.8 to 2.7.9](https://github.com/apolloconfig/apollo/pull/4750) * [[Multi-Database Support] Without Reliance on globally_quoted_identifiers Variable](https://github.com/apolloconfig/apollo/pull/4749) * [[Multi-Database Support] Without Reliance on boolean integer compare](https://github.com/apolloconfig/apollo/pull/4757) * [[Multi-Database Support] package postgre h2 dependency](https://github.com/apolloconfig/apollo/pull/4763) * [[Multi-Database Support] Optimize table case](https://github.com/apolloconfig/apollo/pull/4768) * [Fix OIDC logout unnecessary redirect](https://github.com/apolloconfig/apollo/pull/4773) * [[Multi-Database Support] Introduce h2 postgre profile properties to let user config database config](https://github.com/apolloconfig/apollo/pull/4766) * [[Multi-Database Support] Optimize column define case sensitivity](https://github.com/apolloconfig/apollo/pull/4776) * [[Multi-Database Support][pg] Where clause need escape, otherwise will request postgre use lowwer case](https://github.com/apolloconfig/apollo/pull/4780) * [Misc dependency updates](https://github.com/apolloconfig/apollo/pull/4784) * [Fix the problem that the deletion failure of the system rights management page does not prompt](https://github.com/apolloconfig/apollo/pull/4803) * [Fix the issue of the system permission management page retrieving non-existent users](https://github.com/apolloconfig/apollo/pull/4802) * [Add release history cleaning function](https://github.com/apolloconfig/apollo/pull/4813) * [[Multi-Database Support][pg] Make JdbcUserDetailsManager compat with postgre](https://github.com/apolloconfig/apollo/pull/4790) * [refactor(apollo logging): Simplify the default log path to `/opt/logs`](https://github.com/apolloconfig/apollo/pull/4833) * [Add a configuration config-service.cache.key.ignore-case to control whether the cache key is case-sensitive](https://github.com/apolloconfig/apollo/pull/4820) * [feat: check port use by another process or not when startup](https://github.com/apolloconfig/apollo/pull/4656) * [Bump springboot version from 2.7.9 to 2.7.11](https://github.com/apolloconfig/apollo/pull/4828) * [[Multi-Database Support][h2] Support run on h2](https://github.com/apolloconfig/apollo/pull/4851) * [Fix the issue that env special case handling is missing in some case](https://github.com/apolloconfig/apollo/pull/4887) * [Fix the issue that namespace content being cleared when identical content is pasted into the namespace](https://github.com/apolloconfig/apollo/pull/4922) * [feat(openapi): allow user create app via openapi](https://github.com/apolloconfig/apollo/pull/4954) * [Support grayscale feature for non-properties namespaces](https://github.com/apolloconfig/apollo/pull/4952) ------------------ All issues and pull requests are [here](https://github.com/apolloconfig/apollo/milestone/13?closed=1) ================================================ FILE: changes/changes-2.3.0.md ================================================ Changes by Version ================== Release Notes. Apollo 2.3.0 ------------------ * [Fix circular references on LdapAutoConfiguration](https://github.com/apolloconfig/apollo/pull/5055) * [Add comment for clusters and UI display](https://github.com/apolloconfig/apollo/pull/5072) * [Fix the issue that the length of private namespaces are mis-calculated](https://github.com/apolloconfig/apollo/pull/5078) * [apollo assembly optimization](https://github.com/apolloconfig/apollo/pull/5035) * [update the config item table column width](https://github.com/apolloconfig/apollo/pull/5131) * [sync apollo portal server config to apollo quick start server](https://github.com/apolloconfig/apollo/pull/5134) * [Fix the role permission deletion issue when appid contains '_'](https://github.com/apolloconfig/apollo/pull/5150) * [fix: -XX:HeapDumpPath doesn't ready when meet OOM](https://github.com/apolloconfig/apollo/pull/5157) * [Fix the error occurred when using configuration retention feature](https://github.com/apolloconfig/apollo/pull/5162) ------------------ All issues and pull requests are [here](https://github.com/apolloconfig/apollo/milestone/14?closed=1) ================================================ FILE: changes/changes-2.4.0.md ================================================ Changes by Version ================== Release Notes. Apollo 2.4.0 ------------------ * [Update the server config link in system info page](https://github.com/apolloconfig/apollo/pull/5204) * [Feature support portal restTemplate Client connection pool config](https://github.com/apolloconfig/apollo/pull/5200) * [Feature added the ability for administrators to globally search for Value](https://github.com/apolloconfig/apollo/pull/5182) * [Fix: Resolve issues with duplicate comments and blank lines in configuration management](https://github.com/apolloconfig/apollo/pull/5232) * [Fix link namespace published items show missing some items](https://github.com/apolloconfig/apollo/pull/5240) * [Feature: Add limit and whitelist for namespace count per appid+cluster](https://github.com/apolloconfig/apollo/pull/5228) * [Feature support the observe status access-key for pre-check and logging only](https://github.com/apolloconfig/apollo/pull/5236) * [Feature add limit for items count per namespace](https://github.com/apolloconfig/apollo/pull/5227) * [Feature: Add ConfigService cache record stats function](https://github.com/apolloconfig/apollo/pull/5247) * [RefreshAdminServerAddressTask supports dynamic configuration of time interval](https://github.com/apolloconfig/apollo/pull/5248) * [Refactor: Configuration files uniformly use Kebab style](https://github.com/apolloconfig/apollo/pull/5262) * [Feature: openapi query namespace support not fill item](https://github.com/apolloconfig/apollo/pull/5249) * [Refactor: align database ClusterName and NamespaceName fields lengths](https://github.com/apolloconfig/apollo/pull/5263) * [Feature: Added the value length limit function for AppId-level configuration items](https://github.com/apolloconfig/apollo/pull/5264) * [Fix: ensure clusters order in envClusters open api](https://github.com/apolloconfig/apollo/pull/5277) * [Fix: bump xstream from 1.4.20 to 1.4.21 to fix CVE-2024-47072](https://github.com/apolloconfig/apollo/pull/5280) * [Feature: highlight diffs for properties](https://github.com/apolloconfig/apollo/pull/5282) * [Feature: Add rate limiting function to ConsumerToken](https://github.com/apolloconfig/apollo/pull/5267) * [Feature: add JSON formatting function in apollo-portal](https://github.com/apolloconfig/apollo/pull/5287) * [Fix: add missing url patterns for AdminServiceAuthenticationFilter](https://github.com/apolloconfig/apollo/pull/5291) * [Fix: support java.time.Instant serialization with gson](https://github.com/apolloconfig/apollo/pull/5298) * [Feature: support to assign users management authority by the cluster (modify, publish)](https://github.com/apolloconfig/apollo/pull/5302) * [Feature: notification by email when releasing by OpenApi also](https://github.com/apolloconfig/apollo/pull/5324) ------------------ All issues and pull requests are [here](https://github.com/apolloconfig/apollo/milestone/15?closed=1) ================================================ FILE: changes/changes-2.5.0.md ================================================ Changes by Version ================== Release Notes. Apollo 2.5.0 ------------------ * [Refactor: align permission validator api between openapi and portal](https://github.com/apolloconfig/apollo/pull/5337) * [Feature: Provide a new configfiles API to return the raw content of configuration files directly](https://github.com/apolloconfig/apollo/pull/5336) * [Feature: Enhanced instance configuration auditing and caching](https://github.com/apolloconfig/apollo/pull/5361) * [Feature: Provide a new open API to return the organization list](https://github.com/apolloconfig/apollo/pull/5365) * [Refactor: Exception handler adds root cause information](https://github.com/apolloconfig/apollo/pull/5367) * [Feature: Enhanced parameter verification for edit item](https://github.com/apolloconfig/apollo/pull/5376) * [Feature: Added a new feature to get instance count by namespace.](https://github.com/apolloconfig/apollo/pull/5381) * [Bugfix: Remove cluster-related roles and permissions upon deletion](https://github.com/apolloconfig/apollo/pull/5395) * [Security: Prevent unauthorized access to other users' apps in /apps/by-owner endpoint](https://github.com/apolloconfig/apollo/pull/5396) * [Fix: Bump h2database and snakeyaml version](https://github.com/apolloconfig/apollo/pull/5406) * [Bugfix: Correct permission target format to appId+env+namespace/cluster](https://github.com/apolloconfig/apollo/pull/5407) * [Security: Hide password when registering or modifying users](https://github.com/apolloconfig/apollo/pull/5414) * [Fix: the logical judgment for configuration addition, deletion, and modification.](https://github.com/apolloconfig/apollo/pull/5432) * [Feature support incremental configuration synchronization client](https://github.com/apolloconfig/apollo/pull/5288) * [optimize: Implement unified permission verification logic and Optimize the implementation of permission verification](https://github.com/apolloconfig/apollo/pull/5456) * [CI: Add code and header formatter by spotless plugin](https://github.com/apolloconfig/apollo/pull/5485) * [Fix: Operate the AccessKey multiple times within one second](https://github.com/apolloconfig/apollo/pull/5490) * [Bugfix: Prevent accidental cache deletion when recreating AppNamespace with the same name and appid](https://github.com/apolloconfig/apollo/issues/5502) * [Feature: Support ordinary users to modify personal information](https://github.com/apolloconfig/apollo/pull/5511) * [Feature: Support exporting and importing configurations for specified applications and clusters](https://github.com/apolloconfig/apollo/pull/5517) * [doc: Add rust apollo client link](https://github.com/apolloconfig/apollo/pull/5514) * [Perf: optimize namespace-related interface](https://github.com/apolloconfig/apollo/pull/5518) * [Perf: Replace synchronized multimap with concurrent hashmap in NotificationControllerV2 for better performance](https://github.com/apolloconfig/apollo/pull/5532) * [Feature: Enable graceful shutdown for apollo-adminservice and apollo-configservice](https://github.com/apolloconfig/apollo/pull/5536) * [Feature: Support search box and fullscreen in namespace text editor](https://github.com/apolloconfig/apollo/pull/5545) * [CI: Add portal UI Playwright e2e gate on PRs with JDK 17](https://github.com/apolloconfig/apollo/pull/5551) * [CI: Add portal auth matrix Playwright E2E gate for LDAP and OIDC login flows](https://github.com/apolloconfig/apollo/pull/5557) * [CI: Add standalone Docker validation workflow with Java 17 runtime image checks](https://github.com/apolloconfig/apollo/pull/5558) ------------------ All issues and pull requests are [here](https://github.com/apolloconfig/apollo/milestone/16?closed=1) ================================================ FILE: docs/.nojekyll ================================================ ================================================ FILE: docs/CNAME ================================================ www.apolloconfig.com ================================================ FILE: docs/_coverpage.md ================================================ apollo-logo > A reliable configuration management system - Multiple environments and clusters support - Configuration changes take effect in real time - Versioned and grayscale releases management - Great authentication, authorization and audit control [GitHub](https://github.com/apolloconfig) [Gitee](https://gitee.com/apolloconfig) [Get Started](zh/README) ================================================ FILE: docs/charts/index.yaml ================================================ # Copyright 2024 Apollo Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # apiVersion: v1 entries: apollo-portal: - apiVersion: v2 appVersion: 1.9.1 created: "2021-09-09T09:24:18.945869+08:00" description: A Helm chart for Apollo Portal digest: 6e50025665de4179f3485aa58ef33f24556267764afc645eaaab98810716f7ab home: https://github.com/apolloconfig/apollo icon: https://raw.githubusercontent.com/apolloconfig/apollo/master/apollo-portal/src/main/resources/static/img/logo-simple.png maintainers: - email: nobodyiam@gmail.com name: nobodyiam url: https://github.com/nobodyiam name: apollo-portal type: application urls: - apollo-portal-0.3.1.tgz version: 0.3.1 - apiVersion: v2 appVersion: 1.9.0 created: "2021-08-23T20:17:19.061588+08:00" description: A Helm chart for Apollo Portal digest: 5013793bf02482003afc17df98e5a6c62fbfb1e17f08ba49f3d27bd02fbd768d home: https://github.com/apolloconfig/apollo icon: https://raw.githubusercontent.com/apolloconfig/apollo/master/apollo-portal/src/main/resources/static/img/logo-simple.png maintainers: - email: nobodyiam@gmail.com name: nobodyiam url: https://github.com/nobodyiam name: apollo-portal type: application urls: - apollo-portal-0.3.0.tgz version: 0.3.0 - apiVersion: v2 appVersion: 1.8.2 created: "2021-06-02T23:05:43.379448+08:00" description: A Helm chart for Apollo Portal digest: 5586b2597ebe098a26c94273855461891041241628d85dc13e05ce661ff5e7a2 home: https://github.com/apolloconfig/apollo icon: https://raw.githubusercontent.com/apolloconfig/apollo/master/apollo-portal/src/main/resources/static/img/logo-simple.png maintainers: - email: nobodyiam@gmail.com name: nobodyiam url: https://github.com/nobodyiam name: apollo-portal type: application urls: - apollo-portal-0.2.2.tgz version: 0.2.2 - apiVersion: v2 appVersion: 1.8.1 created: "2021-06-02T23:05:43.378586+08:00" description: A Helm chart for Apollo Portal digest: 8bac1b48361e2717d7d4ee669a85a14ee22ada96f249300d90ee563def63baea home: https://github.com/apolloconfig/apollo icon: https://raw.githubusercontent.com/apolloconfig/apollo/master/apollo-portal/src/main/resources/static/img/logo-simple.png maintainers: - email: nobodyiam@gmail.com name: nobodyiam url: https://github.com/nobodyiam name: apollo-portal type: application urls: - apollo-portal-0.2.1.tgz version: 0.2.1 - apiVersion: v2 appVersion: 1.8.0 created: "2021-06-02T23:05:43.377736+08:00" description: A Helm chart for Apollo Portal digest: 8a55c5f3ddd76b9768067fe6eecfb34b588084557c11882e263abb2af7dfc173 home: https://github.com/apolloconfig/apollo icon: https://raw.githubusercontent.com/apolloconfig/apollo/master/apollo-portal/src/main/resources/static/img/logo-simple.png maintainers: - email: nobodyiam@gmail.com name: nobodyiam url: https://github.com/nobodyiam name: apollo-portal type: application urls: - apollo-portal-0.2.0.tgz version: 0.2.0 - apiVersion: v2 appVersion: 1.7.2 created: "2021-06-02T23:05:43.376846+08:00" description: A Helm chart for Apollo Portal digest: 1db8b2435e0471ef55c63c47b970e4ae059800f88ed4ac4f1ca6cd27fe502722 home: https://github.com/apolloconfig/apollo icon: https://raw.githubusercontent.com/apolloconfig/apollo/master/apollo-portal/src/main/resources/static/img/logo-simple.png maintainers: - email: nobodyiam@gmail.com name: nobodyiam url: https://github.com/nobodyiam name: apollo-portal type: application urls: - apollo-portal-0.1.2.tgz version: 0.1.2 - apiVersion: v2 appVersion: 1.7.1 created: "2021-06-02T23:05:43.375959+08:00" description: A Helm chart for Apollo Portal digest: 7c1b4efa13d4cc54a714b3ec836f609a8f97286c465789f6d807d1227cfc5b74 home: https://github.com/apolloconfig/apollo icon: https://raw.githubusercontent.com/apolloconfig/apollo/master/apollo-portal/src/main/resources/static/img/logo-simple.png maintainers: - email: nobodyiam@gmail.com name: nobodyiam url: https://github.com/nobodyiam name: apollo-portal type: application urls: - apollo-portal-0.1.1.tgz version: 0.1.1 - apiVersion: v2 appVersion: 1.7.0 created: "2021-06-02T23:05:43.37481+08:00" description: A Helm chart for Apollo Portal digest: b838e4478f0c031093691defeffb5805d51189989db0eb9caaa2dc67905c8391 home: https://github.com/apolloconfig/apollo icon: https://raw.githubusercontent.com/apolloconfig/apollo/master/apollo-portal/src/main/resources/static/img/logo-simple.png maintainers: - email: nobodyiam@gmail.com name: nobodyiam url: https://github.com/nobodyiam name: apollo-portal type: application urls: - apollo-portal-0.1.0.tgz version: 0.1.0 apollo-service: - apiVersion: v2 appVersion: 1.9.1 created: "2021-09-09T09:24:18.953677+08:00" description: A Helm chart for Apollo Config Service and Apollo Admin Service digest: e444ef8db74d8e11b7b5cfebb31ac2e1138a40d036045f432898ebf98fc33f07 home: https://github.com/apolloconfig/apollo icon: https://raw.githubusercontent.com/apolloconfig/apollo/master/apollo-portal/src/main/resources/static/img/logo-simple.png maintainers: - email: nobodyiam@gmail.com name: nobodyiam url: https://github.com/nobodyiam name: apollo-service type: application urls: - apollo-service-0.3.1.tgz version: 0.3.1 - apiVersion: v2 appVersion: 1.9.0 created: "2021-08-23T20:17:19.069986+08:00" description: A Helm chart for Apollo Config Service and Apollo Admin Service digest: 8eab8f69d2fddc8b3c930c76f9e930d2e144cc31805b4df8856cf1e945e9333a home: https://github.com/apolloconfig/apollo icon: https://raw.githubusercontent.com/apolloconfig/apollo/master/apollo-portal/src/main/resources/static/img/logo-simple.png maintainers: - email: nobodyiam@gmail.com name: nobodyiam url: https://github.com/nobodyiam name: apollo-service type: application urls: - apollo-service-0.3.0.tgz version: 0.3.0 - apiVersion: v2 appVersion: 1.8.2 created: "2021-06-02T23:05:43.388235+08:00" description: A Helm chart for Apollo Config Service and Apollo Admin Service digest: 7291d516b94390843439bae7a2635561e713a549339a314fa2ef79ef098b6ab4 home: https://github.com/apolloconfig/apollo icon: https://raw.githubusercontent.com/apolloconfig/apollo/master/apollo-portal/src/main/resources/static/img/logo-simple.png maintainers: - email: nobodyiam@gmail.com name: nobodyiam url: https://github.com/nobodyiam name: apollo-service type: application urls: - apollo-service-0.2.2.tgz version: 0.2.2 - apiVersion: v2 appVersion: 1.8.1 created: "2021-06-02T23:05:43.387399+08:00" description: A Helm chart for Apollo Config Service and Apollo Admin Service digest: f88f0c6942353b3d49b4caa43a7580b885ccc94a646428694adc2ed3d43e49fe home: https://github.com/apolloconfig/apollo icon: https://raw.githubusercontent.com/apolloconfig/apollo/master/apollo-portal/src/main/resources/static/img/logo-simple.png maintainers: - email: nobodyiam@gmail.com name: nobodyiam url: https://github.com/nobodyiam name: apollo-service type: application urls: - apollo-service-0.2.1.tgz version: 0.2.1 - apiVersion: v2 appVersion: 1.8.0 created: "2021-06-02T23:05:43.385355+08:00" description: A Helm chart for Apollo Config Service and Apollo Admin Service digest: 73e66c651499f93b6f3891e979dcbe2a20ad37809087e42bc98643c604c457c6 home: https://github.com/apolloconfig/apollo icon: https://raw.githubusercontent.com/apolloconfig/apollo/master/apollo-portal/src/main/resources/static/img/logo-simple.png maintainers: - email: nobodyiam@gmail.com name: nobodyiam url: https://github.com/nobodyiam name: apollo-service type: application urls: - apollo-service-0.2.0.tgz version: 0.2.0 - apiVersion: v2 appVersion: 1.7.2 created: "2021-06-02T23:05:43.38449+08:00" description: A Helm chart for Apollo Config Service and Apollo Admin Service digest: be9379611b45db416a32b7e1367527faec7c29a969493ca4a0a72b5b87a280f4 home: https://github.com/apolloconfig/apollo icon: https://raw.githubusercontent.com/apolloconfig/apollo/master/apollo-portal/src/main/resources/static/img/logo-simple.png maintainers: - email: nobodyiam@gmail.com name: nobodyiam url: https://github.com/nobodyiam name: apollo-service type: application urls: - apollo-service-0.1.2.tgz version: 0.1.2 - apiVersion: v2 appVersion: 1.7.1 created: "2021-06-02T23:05:43.381142+08:00" description: A Helm chart for Apollo Config Service and Apollo Admin Service digest: 787a26590d68735341b7839c14274492097fed4a0843fc9a1e5c692194199707 home: https://github.com/apolloconfig/apollo icon: https://raw.githubusercontent.com/apolloconfig/apollo/master/apollo-portal/src/main/resources/static/img/logo-simple.png maintainers: - email: nobodyiam@gmail.com name: nobodyiam url: https://github.com/nobodyiam name: apollo-service type: application urls: - apollo-service-0.1.1.tgz version: 0.1.1 - apiVersion: v2 appVersion: 1.7.0 created: "2021-06-02T23:05:43.380337+08:00" description: A Helm chart for Apollo Config Service and Apollo Admin Service digest: 65c08f39b54ad1ac1d849cc841ce978bd6e95b0b6cbd2423d9f17f66bc7e8d3e home: https://github.com/apolloconfig/apollo icon: https://raw.githubusercontent.com/apolloconfig/apollo/master/apollo-portal/src/main/resources/static/img/logo-simple.png maintainers: - email: nobodyiam@gmail.com name: nobodyiam url: https://github.com/nobodyiam name: apollo-service type: application urls: - apollo-service-0.1.0.tgz version: 0.1.0 generated: "2021-06-02T23:05:43.373363+08:00" ================================================ FILE: docs/css/buble.css ================================================ /* * Copyright 2024 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ @import url("fonts.css");*{-webkit-font-smoothing:antialiased;-webkit-overflow-scrolling:touch;-webkit-tap-highlight-color:rgba(0,0,0,0);-webkit-text-size-adjust:none;-webkit-touch-callout:none;box-sizing:border-box}body:not(.ready){overflow:hidden}body:not(.ready) .app-nav,body:not(.ready)>nav,body:not(.ready) [data-cloak]{display:none}div#app{font-size:30px;font-weight:lighter;margin:40vh auto;text-align:center}div#app:empty:before{content:"Loading..."}.emoji{height:1.2rem;vertical-align:middle}.progress{background-color:var(--theme-color,#0074d9);height:2px;left:0;position:fixed;right:0;top:0;transition:width .2s,opacity .4s;width:0;z-index:999999}.search .search-keyword,.search a:hover{color:var(--theme-color,#0074d9)}.search .search-keyword{font-style:normal;font-weight:700}body,html{height:100%}body{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;color:#34495e;font-family:Source Sans Pro,Helvetica Neue,Arial,sans-serif;font-size:15px;letter-spacing:0;margin:0;overflow-x:hidden}img{max-width:100%}a[disabled]{cursor:not-allowed;opacity:.6}kbd{border:1px solid #ccc;border-radius:3px;display:inline-block;font-size:12px!important;line-height:12px;margin-bottom:3px;padding:3px 5px;vertical-align:middle}li input[type=checkbox]{margin:0 .2em .25em 0;vertical-align:middle}.app-nav{margin:25px 60px 0 0;position:absolute;right:0;text-align:right;z-index:10}.app-nav.no-badge{margin-right:25px}.app-nav p{margin:0}.app-nav>a{margin:0 1rem;padding:5px 0}.app-nav li,.app-nav ul{display:inline-block;list-style:none;margin:0}.app-nav a{color:inherit;font-size:16px;text-decoration:none;transition:color .3s}.app-nav a.active,.app-nav a:hover{color:var(--theme-color,#0074d9)}.app-nav a.active{border-bottom:2px solid var(--theme-color,#0074d9)}.app-nav li{display:inline-block;margin:0 1rem;padding:5px 0;position:relative;cursor:pointer}.app-nav li ul{background-color:#fff;border:1px solid;border-color:#ddd #ddd #ccc;border-radius:4px;box-sizing:border-box;display:none;max-height:calc(100vh - 61px);overflow-y:auto;padding:10px 0;position:absolute;right:-15px;text-align:left;top:100%;white-space:nowrap}.app-nav li ul li{display:block;font-size:14px;line-height:1rem;margin:8px 14px;white-space:nowrap}.app-nav li ul a{display:block;font-size:inherit;margin:0;padding:0}.app-nav li ul a.active{border-bottom:0}.app-nav li:hover ul{display:block}.github-corner{border-bottom:0;position:fixed;right:0;text-decoration:none;top:0;z-index:1}.github-corner:hover .octo-arm{-webkit-animation:octocat-wave .56s ease-in-out;animation:octocat-wave .56s ease-in-out}.github-corner svg{color:#fff;fill:var(--theme-color,#0074d9);height:80px;width:80px}main{display:block;position:relative;width:100vw;height:100%;z-index:0}main.hidden{display:none}.anchor{display:inline-block;text-decoration:none;transition:all .3s}.anchor span{color:#34495e}.anchor:hover{text-decoration:underline}.sidebar{border-right:1px solid rgba(0,0,0,.07);overflow-y:auto;padding:40px 0 0;position:absolute;top:0;bottom:0;left:0;transition:transform .25s ease-out;width:16rem;z-index:20}.sidebar>h1{margin:0 auto 1rem;font-size:1.5rem;font-weight:300;text-align:center}.sidebar>h1 a{color:inherit;text-decoration:none}.sidebar>h1 .app-nav{display:block;position:static}.sidebar .sidebar-nav{line-height:2em;padding-bottom:40px}.sidebar li.collapse .app-sub-sidebar{display:none}.sidebar ul{margin:0 0 0 15px;padding:0}.sidebar li>p{font-weight:700;margin:0}.sidebar ul,.sidebar ul li{list-style:none}.sidebar ul li a{border-bottom:none;display:block}.sidebar ul li ul{padding-left:20px}.sidebar::-webkit-scrollbar{width:4px}.sidebar::-webkit-scrollbar-thumb{background:transparent;border-radius:4px}.sidebar:hover::-webkit-scrollbar-thumb{background:hsla(0,0%,53.3%,.4)}.sidebar:hover::-webkit-scrollbar-track{background:hsla(0,0%,53.3%,.1)}.sidebar-toggle{background-color:transparent;background-color:hsla(0,0%,100%,.8);border:0;outline:none;padding:10px;position:absolute;bottom:0;left:0;text-align:center;transition:opacity .3s;width:0;z-index:30;cursor:pointer}.sidebar-toggle:hover .sidebar-toggle-button{opacity:.4}.sidebar-toggle span{background-color:var(--theme-color,#0074d9);display:block;margin-bottom:4px;width:16px;height:2px}body.sticky .sidebar,body.sticky .sidebar-toggle{position:fixed}.content{padding-top:60px;position:absolute;top:0;right:0;bottom:0;left:16rem;transition:left .25s ease}.markdown-section{margin:0 auto;max-width:80%;padding:30px 15px 40px;position:relative}.markdown-section>*{box-sizing:border-box;font-size:inherit}.markdown-section>:first-child{margin-top:0!important}.markdown-section hr{border:none;border-bottom:1px solid #eee;margin:2em 0}.markdown-section iframe{border:1px solid #eee;width:1px;min-width:100%}.markdown-section table{border-collapse:collapse;border-spacing:0;display:block;margin-bottom:1rem;overflow:auto;width:100%}.markdown-section th{font-weight:700}.markdown-section td,.markdown-section th{border:1px solid #ddd;padding:6px 13px}.markdown-section tr{border-top:1px solid #ccc}.markdown-section p.tip,.markdown-section tr:nth-child(2n){background-color:#f8f8f8}.markdown-section p.tip{border-bottom-right-radius:2px;border-left:4px solid #f66;border-top-right-radius:2px;margin:2em 0;padding:12px 24px 12px 30px;position:relative}.markdown-section p.tip:before{background-color:#f66;border-radius:100%;color:#fff;content:"!";font-family:Dosis,Source Sans Pro,Helvetica Neue,Arial,sans-serif;font-size:14px;font-weight:700;left:-12px;line-height:20px;position:absolute;height:20px;width:20px;text-align:center;top:14px}.markdown-section p.tip code{background-color:#efefef}.markdown-section p.tip em{color:#34495e}.markdown-section p.warn{background:rgba(0,116,217,.1);border-radius:2px;padding:1rem}.markdown-section ul.task-list>li{list-style-type:none}body.close .sidebar{transform:translateX(-16rem)}body.close .sidebar-toggle{width:auto}body.close .content{left:0}@media print{.app-nav,.github-corner,.sidebar,.sidebar-toggle{display:none}}@media screen and (max-width:768px){.github-corner,.sidebar,.sidebar-toggle{position:fixed}.app-nav{margin-top:16px}.app-nav li ul{top:30px}main{height:auto;overflow-x:hidden}.sidebar{left:-16rem;transition:transform .25s ease-out}.content{left:0;max-width:100vw;position:static;padding-top:20px;transition:transform .25s ease}.app-nav,.github-corner{transition:transform .25s ease-out}.sidebar-toggle{background-color:transparent;width:auto;padding:30px 30px 10px 10px}body.close .sidebar{transform:translateX(16rem)}body.close .sidebar-toggle{background-color:hsla(0,0%,100%,.8);transition:background-color 1s;width:0;padding:10px}body.close .content{transform:translateX(16rem)}body.close .app-nav,body.close .github-corner{display:none}.github-corner:hover .octo-arm{-webkit-animation:none;animation:none}.github-corner .octo-arm{-webkit-animation:octocat-wave .56s ease-in-out;animation:octocat-wave .56s ease-in-out}}@-webkit-keyframes octocat-wave{0%,to{transform:rotate(0)}20%,60%{transform:rotate(-25deg)}40%,80%{transform:rotate(10deg)}}@keyframes octocat-wave{0%,to{transform:rotate(0)}20%,60%{transform:rotate(-25deg)}40%,80%{transform:rotate(10deg)}}section.cover{align-items:center;background-position:50%;background-repeat:no-repeat;background-size:cover;height:100vh;width:100vw;display:none}section.cover.show{display:flex}section.cover.has-mask .mask{background-color:#fff;opacity:.8;position:absolute;top:0;height:100%;width:100%}section.cover .cover-main{flex:1;margin:-20px 16px 0;text-align:center;position:relative}section.cover a{color:inherit}section.cover a,section.cover a:hover{text-decoration:none}section.cover p{line-height:1.5rem;margin:1em 0}section.cover h1{color:inherit;font-size:2.5rem;font-weight:300;margin:.625rem 0 2.5rem;position:relative;text-align:center}section.cover h1 a{display:block}section.cover h1 small{bottom:-.4375rem;font-size:1rem;position:absolute}section.cover blockquote{font-size:1.5rem;text-align:center}section.cover ul{line-height:1.8;list-style-type:none;margin:1em auto;max-width:500px;padding:0}section.cover .cover-main>p:last-child a{border-radius:2rem;border:1px solid var(--theme-color,#0074d9);box-sizing:border-box;color:var(--theme-color,#0074d9);display:inline-block;font-size:1.05rem;letter-spacing:.1rem;margin:.5rem 1rem;padding:.75em 2rem;text-decoration:none;transition:all .15s ease}section.cover .cover-main>p:last-child a:last-child{background-color:var(--theme-color,#0074d9);color:#fff}section.cover .cover-main>p:last-child a:last-child:hover{color:inherit;opacity:.8}section.cover .cover-main>p:last-child a:hover{color:inherit}section.cover blockquote>p>a{border-bottom:2px solid var(--theme-color,#0074d9);transition:color .3s}section.cover blockquote>p>a:hover{color:var(--theme-color,#0074d9)}.sidebar{color:#364149;background-color:#fff}.sidebar a{color:#666;text-decoration:none}.sidebar li{list-style:none;margin:0;padding:.2em 0}.sidebar ul li ul{padding:0}.sidebar li.active{background-color:#eee}.sidebar li.active a{color:#333}.markdown-section h1,.markdown-section h2,.markdown-section h3,.markdown-section h4,.markdown-section strong{color:#333;font-weight:400}.markdown-section strong{color:#333;font-weight:600}.markdown-section a{color:var(--theme-color,#0074d9)}.markdown-section ol,.markdown-section p,.markdown-section ul{line-height:1.6rem;margin:0 0 1em;word-spacing:.05rem}.markdown-section h1{font-size:2rem;font-weight:500;margin:0 0 1rem}.markdown-section h2{font-size:1.8rem;font-weight:400;margin:0 0 1rem;padding:1rem 0 0}.markdown-section h3{font-size:1.5rem;margin:52px 0 1.2rem}.markdown-section h4{font-size:1.25rem}.markdown-section h5{font-size:1rem}.markdown-section h6{color:#777;font-size:1rem}.markdown-section figure,.markdown-section ol,.markdown-section p,.markdown-section ul{margin:1.2em 0}.markdown-section ol,.markdown-section ul{padding-left:1.5rem}.markdown-section li{line-height:1.5;margin:0}.markdown-section blockquote{border-left:4px solid var(--theme-color,#0074d9);color:#858585;margin:2em 0;padding-left:20px}.markdown-section blockquote p{font-weight:600;margin-left:0}.markdown-section iframe{margin:1em 0}.markdown-section em{color:#7f8c8d}.markdown-section code{border-radius:3px;padding:.2em .4rem;white-space:nowrap}.markdown-section code,.markdown-section pre{background-color:#f9f9f9;font-family:Inconsolata}.markdown-section pre{border-left:2px solid #eee;font-size:16px;margin:0 0 1em;padding:0 10px 12px 0;overflow:auto;word-wrap:normal;position:relative}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#93a1a1}.token.punctuation{color:#586e75}.namespace{opacity:.7}.token.boolean,.token.constant,.token.deleted,.token.number,.token.property,.token.symbol,.token.tag{color:#268bd2}.token.attr-name,.token.builtin,.token.char,.token.inserted,.token.selector,.token.string,.token.url{color:#2aa198}.token.entity{color:#657b83;background:#eee8d5}.token.atrule,.token.attr-value,.token.keyword{color:#a11}.token.function{color:#b58900}.token.important,.token.regex,.token.variable{color:#cb4b16}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}.markdown-section pre>code{background-color:#f8f8f8;border-radius:2px;display:block;font-family:Inconsolata;line-height:1.1rem;max-width:inherit;overflow:inherit;padding:20px .8em;position:relative;white-space:inherit}.markdown-section code:after,.markdown-section code:before{letter-spacing:.05rem}code .token{-webkit-font-smoothing:initial;-moz-osx-font-smoothing:initial;min-height:1.5rem;position:relative;left:auto} ================================================ FILE: docs/css/dark.css ================================================ /* * Copyright 2024 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ @import url("fonts.css");*{-webkit-font-smoothing:antialiased;-webkit-overflow-scrolling:touch;-webkit-tap-highlight-color:rgba(0,0,0,0);-webkit-text-size-adjust:none;-webkit-touch-callout:none;box-sizing:border-box}body:not(.ready){overflow:hidden}body:not(.ready) .app-nav,body:not(.ready)>nav,body:not(.ready) [data-cloak]{display:none}div#app{font-size:30px;font-weight:lighter;margin:40vh auto;text-align:center}div#app:empty:before{content:"Loading..."}.emoji{height:1.2rem;vertical-align:middle}.progress{background-color:var(--theme-color,#ea6f5a);height:2px;left:0;position:fixed;right:0;top:0;transition:width .2s,opacity .4s;width:0;z-index:999999}.search .search-keyword,.search a:hover{color:var(--theme-color,#ea6f5a)}.search .search-keyword{font-style:normal;font-weight:700}body,html{height:100%}body{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;color:#c8c8c8;font-family:Source Sans Pro,Helvetica Neue,Arial,sans-serif;font-size:15px;letter-spacing:0;margin:0;overflow-x:hidden}img{max-width:100%}a[disabled]{cursor:not-allowed;opacity:.6}kbd{border:1px solid #ccc;border-radius:3px;display:inline-block;font-size:12px!important;line-height:12px;margin-bottom:3px;padding:3px 5px;vertical-align:middle}li input[type=checkbox]{margin:0 .2em .25em 0;vertical-align:middle}.app-nav{margin:25px 60px 0 0;position:absolute;right:0;text-align:right;z-index:10}.app-nav.no-badge{margin-right:25px}.app-nav p{margin:0}.app-nav>a{margin:0 1rem;padding:5px 0}.app-nav li,.app-nav ul{display:inline-block;list-style:none;margin:0}.app-nav a{color:inherit;font-size:16px;text-decoration:none;transition:color .3s}.app-nav a.active,.app-nav a:hover{color:var(--theme-color,#ea6f5a)}.app-nav a.active{border-bottom:2px solid var(--theme-color,#ea6f5a)}.app-nav li{display:inline-block;margin:0 1rem;padding:5px 0;position:relative;cursor:pointer}.app-nav li ul{background-color:#fff;border:1px solid;border-color:#ddd #ddd #ccc;border-radius:4px;box-sizing:border-box;display:none;max-height:calc(100vh - 61px);overflow-y:auto;padding:10px 0;position:absolute;right:-15px;text-align:left;top:100%;white-space:nowrap}.app-nav li ul li{display:block;font-size:14px;line-height:1rem;margin:8px 14px;white-space:nowrap}.app-nav li ul a{display:block;font-size:inherit;margin:0;padding:0}.app-nav li ul a.active{border-bottom:0}.app-nav li:hover ul{display:block}.github-corner{border-bottom:0;position:fixed;right:0;text-decoration:none;top:0;z-index:1}.github-corner:hover .octo-arm{-webkit-animation:octocat-wave .56s ease-in-out;animation:octocat-wave .56s ease-in-out}.github-corner svg{color:#3f3f3f;fill:var(--theme-color,#ea6f5a);height:80px;width:80px}main{display:block;position:relative;width:100vw;height:100%;z-index:0}main.hidden{display:none}.anchor{display:inline-block;text-decoration:none;transition:all .3s}.anchor span{color:#c8c8c8}.anchor:hover{text-decoration:underline}.sidebar{border-right:1px solid rgba(0,0,0,.07);overflow-y:auto;padding:40px 0 0;position:absolute;top:0;bottom:0;left:0;transition:transform .25s ease-out;width:300px;z-index:20}.sidebar>h1{margin:0 auto 1rem;font-size:1.5rem;font-weight:300;text-align:center}.sidebar>h1 a{color:inherit;text-decoration:none}.sidebar>h1 .app-nav{display:block;position:static}.sidebar .sidebar-nav{line-height:2em;padding-bottom:40px}.sidebar li.collapse .app-sub-sidebar{display:none}.sidebar ul{margin:0 0 0 15px;padding:0}.sidebar li>p{font-weight:700;margin:0}.sidebar ul,.sidebar ul li{list-style:none}.sidebar ul li a{border-bottom:none;display:block}.sidebar ul li ul{padding-left:20px}.sidebar::-webkit-scrollbar{width:4px}.sidebar::-webkit-scrollbar-thumb{background:transparent;border-radius:4px}.sidebar:hover::-webkit-scrollbar-thumb{background:hsla(0,0%,53.3%,.4)}.sidebar:hover::-webkit-scrollbar-track{background:hsla(0,0%,53.3%,.1)}.sidebar-toggle{background-color:transparent;background-color:rgba(63,63,63,.8);border:0;outline:none;padding:10px;position:absolute;bottom:0;left:0;text-align:center;transition:opacity .3s;width:284px;z-index:30;cursor:pointer}.sidebar-toggle:hover .sidebar-toggle-button{opacity:.4}.sidebar-toggle span{background-color:var(--theme-color,#ea6f5a);display:block;margin-bottom:4px;width:16px;height:2px}body.sticky .sidebar,body.sticky .sidebar-toggle{position:fixed}.content{padding-top:60px;position:absolute;top:0;right:0;bottom:0;left:300px;transition:left .25s ease}.markdown-section{margin:0 auto;max-width:80%;padding:30px 15px 40px;position:relative}.markdown-section>*{box-sizing:border-box;font-size:inherit}.markdown-section>:first-child{margin-top:0!important}.markdown-section hr{border:none;border-bottom:1px solid #eee;margin:2em 0}.markdown-section iframe{border:1px solid #eee;width:1px;min-width:100%}.markdown-section table{border-collapse:collapse;border-spacing:0;display:block;margin-bottom:1rem;overflow:auto;width:100%}.markdown-section th{font-weight:700}.markdown-section td,.markdown-section th{border:1px solid #ddd;padding:6px 13px}.markdown-section tr{border-top:1px solid #ccc}.markdown-section p.tip,.markdown-section tr:nth-child(2n){background-color:#f8f8f8}.markdown-section p.tip{border-bottom-right-radius:2px;border-left:4px solid #f66;border-top-right-radius:2px;margin:2em 0;padding:12px 24px 12px 30px;position:relative}.markdown-section p.tip:before{background-color:#f66;border-radius:100%;color:#3f3f3f;content:"!";font-family:Dosis,Source Sans Pro,Helvetica Neue,Arial,sans-serif;font-size:14px;font-weight:700;left:-12px;line-height:20px;position:absolute;height:20px;width:20px;text-align:center;top:14px}.markdown-section p.tip code{background-color:#efefef}.markdown-section p.tip em{color:#c8c8c8}.markdown-section p.warn{background:rgba(234,111,90,.1);border-radius:2px;padding:1rem}.markdown-section ul.task-list>li{list-style-type:none}body.close .sidebar{transform:translateX(-300px)}body.close .sidebar-toggle{width:auto}body.close .content{left:0}@media print{.app-nav,.github-corner,.sidebar,.sidebar-toggle{display:none}}@media screen and (max-width:768px){.github-corner,.sidebar,.sidebar-toggle{position:fixed}.app-nav{margin-top:16px}.app-nav li ul{top:30px}main{height:auto;overflow-x:hidden}.sidebar{left:-300px;transition:transform .25s ease-out}.content{left:0;max-width:100vw;position:static;padding-top:20px;transition:transform .25s ease}.app-nav,.github-corner{transition:transform .25s ease-out}.sidebar-toggle{background-color:transparent;width:auto;padding:30px 30px 10px 10px}body.close .sidebar{transform:translateX(300px)}body.close .sidebar-toggle{background-color:rgba(63,63,63,.8);transition:background-color 1s;width:284px;padding:10px}body.close .content{transform:translateX(300px)}body.close .app-nav,body.close .github-corner{display:none}.github-corner:hover .octo-arm{-webkit-animation:none;animation:none}.github-corner .octo-arm{-webkit-animation:octocat-wave .56s ease-in-out;animation:octocat-wave .56s ease-in-out}}@-webkit-keyframes octocat-wave{0%,to{transform:rotate(0)}20%,60%{transform:rotate(-25deg)}40%,80%{transform:rotate(10deg)}}@keyframes octocat-wave{0%,to{transform:rotate(0)}20%,60%{transform:rotate(-25deg)}40%,80%{transform:rotate(10deg)}}section.cover{align-items:center;background-position:50%;background-repeat:no-repeat;background-size:cover;height:100vh;width:100vw;display:none}section.cover.show{display:flex}section.cover.has-mask .mask{background-color:#3f3f3f;opacity:.8;position:absolute;top:0;height:100%;width:100%}section.cover .cover-main{flex:1;margin:-20px 16px 0;text-align:center;position:relative}section.cover a{color:inherit}section.cover a,section.cover a:hover{text-decoration:none}section.cover p{line-height:1.5rem;margin:1em 0}section.cover h1{color:inherit;font-size:2.5rem;font-weight:300;margin:.625rem 0 2.5rem;position:relative;text-align:center}section.cover h1 a{display:block}section.cover h1 small{bottom:-.4375rem;font-size:1rem;position:absolute}section.cover blockquote{font-size:1.5rem;text-align:center}section.cover ul{line-height:1.8;list-style-type:none;margin:1em auto;max-width:500px;padding:0}section.cover .cover-main>p:last-child a{border-radius:2rem;border:1px solid var(--theme-color,#ea6f5a);box-sizing:border-box;color:var(--theme-color,#ea6f5a);display:inline-block;font-size:1.05rem;letter-spacing:.1rem;margin:.5rem 1rem;padding:.75em 2rem;text-decoration:none;transition:all .15s ease}section.cover .cover-main>p:last-child a:last-child{background-color:var(--theme-color,#ea6f5a);color:#fff}section.cover .cover-main>p:last-child a:last-child:hover{color:inherit;opacity:.8}section.cover .cover-main>p:last-child a:hover{color:inherit}section.cover blockquote>p>a{border-bottom:2px solid var(--theme-color,#ea6f5a);transition:color .3s}section.cover blockquote>p>a:hover{color:var(--theme-color,#ea6f5a)}.sidebar,body{background-color:#3f3f3f}.sidebar{color:#c8c8c8}.sidebar li{margin:6px 15px 6px 0}.sidebar ul li a{color:#c8c8c8;font-size:14px;overflow:hidden;text-decoration:none;text-overflow:ellipsis;white-space:nowrap}.sidebar ul li a:hover{text-decoration:underline}.sidebar ul li ul{padding:0}.sidebar ul li.active>a{color:var(--theme-color,#ea6f5a);font-weight:600}.markdown-section h1,.markdown-section h2,.markdown-section h3,.markdown-section h4,.markdown-section strong{color:#657b83;font-weight:600}.markdown-section a{color:var(--theme-color,#ea6f5a);font-weight:600}.markdown-section h1{font-size:2rem;margin:0 0 1rem}.markdown-section h2{font-size:1.75rem;margin:45px 0 .8rem}.markdown-section h3{font-size:1.5rem;margin:40px 0 .6rem}.markdown-section h4{font-size:1.25rem}.markdown-section h5{font-size:1rem}.markdown-section h6{color:#777;font-size:1rem}.markdown-section figure,.markdown-section ol,.markdown-section p,.markdown-section ul{margin:1.2em 0}.markdown-section ol,.markdown-section p,.markdown-section ul{line-height:1.6rem;word-spacing:.05rem}.markdown-section ol,.markdown-section ul{padding-left:1.5rem}.markdown-section blockquote{border-left:4px solid var(--theme-color,#ea6f5a);color:#858585;margin:2em 0;padding-left:20px}.markdown-section blockquote p{font-weight:600;margin-left:0}.markdown-section iframe{margin:1em 0}.markdown-section em{color:#7f8c8d}.markdown-section code{background-color:#282828;border-radius:2px;color:#657b83;font-family:Roboto Mono,Monaco,courier,monospace;margin:0 2px;padding:3px 5px;white-space:pre-wrap}.markdown-section>:not(h1):not(h2):not(h3):not(h4):not(h5):not(h6) code{font-size:.8rem}.markdown-section pre{-moz-osx-font-smoothing:initial;-webkit-font-smoothing:initial;background-color:#282828;font-family:Roboto Mono,Monaco,courier,monospace;line-height:1.5rem;margin:1.2em 0;overflow:auto;padding:0 1.4rem;position:relative;word-wrap:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#8e908c}.token.namespace{opacity:.7}.token.boolean,.token.number{color:#c76b29}.token.punctuation{color:#525252}.token.property{color:#c08b30}.token.tag{color:#2973b7}.token.string{color:var(--theme-color,#ea6f5a)}.token.selector{color:#6679cc}.token.attr-name{color:#2973b7}.language-css .token.string,.style .token.string,.token.entity,.token.url{color:#22a2c9}.token.attr-value,.token.control,.token.directive,.token.unit{color:var(--theme-color,#ea6f5a)}.token.keyword{color:#e96900}.token.atrule,.token.regex,.token.statement{color:#22a2c9}.token.placeholder,.token.variable{color:#3d8fd1}.token.deleted{text-decoration:line-through}.token.inserted{border-bottom:1px dotted #202746;text-decoration:none}.token.italic{font-style:italic}.token.bold,.token.important{font-weight:700}.token.important{color:#c94922}.token.entity{cursor:help}.markdown-section pre>code{-moz-osx-font-smoothing:initial;-webkit-font-smoothing:initial;background-color:#282828;border-radius:2px;color:#657b83;display:block;font-family:Roboto Mono,Monaco,courier,monospace;font-size:.8rem;line-height:inherit;margin:0 2px;max-width:inherit;overflow:inherit;padding:2.2em 5px;white-space:inherit}.markdown-section code:after,.markdown-section code:before{letter-spacing:.05rem}code .token{-moz-osx-font-smoothing:initial;-webkit-font-smoothing:initial;min-height:1.5rem;position:relative;left:auto}pre:after{color:#ccc;content:attr(data-lang);font-size:.6rem;font-weight:600;height:15px;line-height:15px;padding:5px 10px 0;position:absolute;right:0;text-align:right;top:0}.markdown-section p.tip{background-color:#282828;color:#657b83}input[type=search]{background:#4f4f4f;border-color:#4f4f4f;color:#c8c8c8} ================================================ FILE: docs/css/fonts.css ================================================ /* * Copyright 2024 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ /* roboto-mono-regular */ @font-face { font-family: 'Roboto Mono'; font-style: normal; font-weight: regular; src: url('//lib.baomitu.com/fonts/roboto-mono/roboto-mono-regular.eot'); /* IE9 Compat Modes */ src: local('Roboto Mono'), local('RobotoMono-Normal'), url('//lib.baomitu.com/fonts/roboto-mono/roboto-mono-regular.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */ url('//lib.baomitu.com/fonts/roboto-mono/roboto-mono-regular.woff2') format('woff2'), /* Super Modern Browsers */ url('//lib.baomitu.com/fonts/roboto-mono/roboto-mono-regular.woff') format('woff'), /* Modern Browsers */ url('//lib.baomitu.com/fonts/roboto-mono/roboto-mono-regular.ttf') format('truetype'), /* Safari, Android, iOS */ url('//lib.baomitu.com/fonts/roboto-mono/roboto-mono-regular.svg#RobotoMono') format('svg'); /* Legacy iOS */ } /* source-sans-pro-300 */ @font-face { font-family: 'Source Sans Pro'; font-style: normal; font-weight: 300; src: url('//lib.baomitu.com/fonts/source-sans-pro/source-sans-pro-300.eot'); /* IE9 Compat Modes */ src: local('Source Sans Pro'), local('SourceSans Pro-Normal'), url('//lib.baomitu.com/fonts/source-sans-pro/source-sans-pro-300.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */ url('//lib.baomitu.com/fonts/source-sans-pro/source-sans-pro-300.woff2') format('woff2'), /* Super Modern Browsers */ url('//lib.baomitu.com/fonts/source-sans-pro/source-sans-pro-300.woff') format('woff'), /* Modern Browsers */ url('//lib.baomitu.com/fonts/source-sans-pro/source-sans-pro-300.ttf') format('truetype'), /* Safari, Android, iOS */ url('//lib.baomitu.com/fonts/source-sans-pro/source-sans-pro-300.svg#SourceSans Pro') format('svg'); /* Legacy iOS */ } /* source-sans-pro-regular */ @font-face { font-family: 'Source Sans Pro'; font-style: normal; font-weight: regular; src: url('//lib.baomitu.com/fonts/source-sans-pro/source-sans-pro-regular.eot'); /* IE9 Compat Modes */ src: local('Source Sans Pro'), local('SourceSans Pro-Normal'), url('//lib.baomitu.com/fonts/source-sans-pro/source-sans-pro-regular.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */ url('//lib.baomitu.com/fonts/source-sans-pro/source-sans-pro-regular.woff2') format('woff2'), /* Super Modern Browsers */ url('//lib.baomitu.com/fonts/source-sans-pro/source-sans-pro-regular.woff') format('woff'), /* Modern Browsers */ url('//lib.baomitu.com/fonts/source-sans-pro/source-sans-pro-regular.ttf') format('truetype'), /* Safari, Android, iOS */ url('//lib.baomitu.com/fonts/source-sans-pro/source-sans-pro-regular.svg#SourceSans Pro') format('svg'); /* Legacy iOS */ } /* source-sans-pro-600 */ @font-face { font-family: 'Source Sans Pro'; font-style: normal; font-weight: 600; src: url('//lib.baomitu.com/fonts/source-sans-pro/source-sans-pro-600.eot'); /* IE9 Compat Modes */ src: local('Source Sans Pro'), local('SourceSans Pro-Normal'), url('//lib.baomitu.com/fonts/source-sans-pro/source-sans-pro-600.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */ url('//lib.baomitu.com/fonts/source-sans-pro/source-sans-pro-600.woff2') format('woff2'), /* Super Modern Browsers */ url('//lib.baomitu.com/fonts/source-sans-pro/source-sans-pro-600.woff') format('woff'), /* Modern Browsers */ url('//lib.baomitu.com/fonts/source-sans-pro/source-sans-pro-600.ttf') format('truetype'), /* Safari, Android, iOS */ url('//lib.baomitu.com/fonts/source-sans-pro/source-sans-pro-600.svg#SourceSans Pro') format('svg'); /* Legacy iOS */ } ================================================ FILE: docs/css/pure.css ================================================ /* * Copyright 2024 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ *{-webkit-font-smoothing:antialiased;-webkit-overflow-scrolling:touch;-webkit-tap-highlight-color:rgba(0,0,0,0);-webkit-text-size-adjust:none;-webkit-touch-callout:none;box-sizing:border-box}body:not(.ready){overflow:hidden}body:not(.ready) .app-nav,body:not(.ready)>nav,body:not(.ready) [data-cloak]{display:none}div#app{font-size:30px;font-weight:lighter;margin:40vh auto;text-align:center}div#app:empty:before{content:"Loading..."}.emoji{height:1.2rem;vertical-align:middle}.progress{background-color:var(--theme-color,#000);height:2px;left:0;position:fixed;right:0;top:0;transition:width .2s,opacity .4s;width:0;z-index:999999}.search .search-keyword,.search a:hover{color:var(--theme-color,#000)}.search .search-keyword{font-style:normal;font-weight:700}body,html{height:100%}body{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;color:#000;font-family:Source Sans Pro,Helvetica Neue,Arial,sans-serif;font-size:15px;letter-spacing:0;margin:0;overflow-x:hidden}img{max-width:100%}a[disabled]{cursor:not-allowed;opacity:.6}kbd{border:1px solid #ccc;border-radius:3px;display:inline-block;font-size:12px!important;line-height:12px;margin-bottom:3px;padding:3px 5px;vertical-align:middle}li input[type=checkbox]{margin:0 .2em .25em 0;vertical-align:middle}.app-nav{margin:25px 60px 0 0;position:absolute;right:0;text-align:right;z-index:10}.app-nav.no-badge{margin-right:25px}.app-nav p{margin:0}.app-nav>a{margin:0 1rem;padding:5px 0}.app-nav li,.app-nav ul{display:inline-block;list-style:none;margin:0}.app-nav a{color:inherit;font-size:16px;text-decoration:none;transition:color .3s}.app-nav a.active,.app-nav a:hover{color:var(--theme-color,#000)}.app-nav a.active{border-bottom:2px solid var(--theme-color,#000)}.app-nav li{display:inline-block;margin:0 1rem;padding:5px 0;position:relative;cursor:pointer}.app-nav li ul{background-color:#fff;border:1px solid;border-color:#ddd #ddd #ccc;border-radius:4px;box-sizing:border-box;display:none;max-height:calc(100vh - 61px);overflow-y:auto;padding:10px 0;position:absolute;right:-15px;text-align:left;top:100%;white-space:nowrap}.app-nav li ul li{display:block;font-size:14px;line-height:1rem;margin:8px 14px;white-space:nowrap}.app-nav li ul a{display:block;font-size:inherit;margin:0;padding:0}.app-nav li ul a.active{border-bottom:0}.app-nav li:hover ul{display:block}.github-corner{border-bottom:0;position:fixed;right:0;text-decoration:none;top:0;z-index:1}.github-corner:hover .octo-arm{-webkit-animation:octocat-wave .56s ease-in-out;animation:octocat-wave .56s ease-in-out}.github-corner svg{color:#fff;fill:var(--theme-color,#000);height:80px;width:80px}main{display:block;position:relative;width:100vw;height:100%;z-index:0}main.hidden{display:none}.anchor{display:inline-block;text-decoration:none;transition:all .3s}.anchor span{color:#000}.anchor:hover{text-decoration:underline}.sidebar{border-right:1px solid rgba(0,0,0,.07);overflow-y:auto;padding:40px 0 0;position:absolute;top:0;bottom:0;left:0;transition:transform .25s ease-out;width:300px;z-index:20}.sidebar>h1{margin:0 auto 1rem;font-size:1.5rem;font-weight:300;text-align:center}.sidebar>h1 a{color:inherit;text-decoration:none}.sidebar>h1 .app-nav{display:block;position:static}.sidebar .sidebar-nav{line-height:2em;padding-bottom:40px}.sidebar li.collapse .app-sub-sidebar{display:none}.sidebar ul{margin:0 0 0 15px;padding:0}.sidebar li>p{font-weight:700;margin:0}.sidebar ul,.sidebar ul li{list-style:none}.sidebar ul li a{border-bottom:none;display:block}.sidebar ul li ul{padding-left:20px}.sidebar::-webkit-scrollbar{width:4px}.sidebar::-webkit-scrollbar-thumb{background:transparent;border-radius:4px}.sidebar:hover::-webkit-scrollbar-thumb{background:hsla(0,0%,53.3%,.4)}.sidebar:hover::-webkit-scrollbar-track{background:hsla(0,0%,53.3%,.1)}.sidebar-toggle{background-color:transparent;background-color:hsla(0,0%,100%,.8);border:0;outline:none;padding:10px;position:absolute;bottom:0;left:0;text-align:center;transition:opacity .3s;width:284px;z-index:30;cursor:pointer}.sidebar-toggle:hover .sidebar-toggle-button{opacity:.4}.sidebar-toggle span{background-color:var(--theme-color,#000);display:block;margin-bottom:4px;width:16px;height:2px}body.sticky .sidebar,body.sticky .sidebar-toggle{position:fixed}.content{padding-top:60px;position:absolute;top:0;right:0;bottom:0;left:300px;transition:left .25s ease}.markdown-section{margin:0 auto;max-width:80%;padding:30px 15px 40px;position:relative}.markdown-section>*{box-sizing:border-box;font-size:inherit}.markdown-section>:first-child{margin-top:0!important}.markdown-section hr{border:none;border-bottom:1px solid #eee;margin:2em 0}.markdown-section iframe{border:1px solid #eee;width:1px;min-width:100%}.markdown-section table{border-collapse:collapse;border-spacing:0;display:block;margin-bottom:1rem;overflow:auto;width:100%}.markdown-section th{font-weight:700}.markdown-section td,.markdown-section th{border:1px solid #ddd;padding:6px 13px}.markdown-section tr{border-top:1px solid #ccc}.markdown-section p.tip,.markdown-section tr:nth-child(2n){background-color:#f8f8f8}.markdown-section p.tip{border-bottom-right-radius:2px;border-left:4px solid #f66;border-top-right-radius:2px;margin:2em 0;padding:12px 24px 12px 30px;position:relative}.markdown-section p.tip:before{background-color:#f66;border-radius:100%;color:#fff;content:"!";font-family:Dosis,Source Sans Pro,Helvetica Neue,Arial,sans-serif;font-size:14px;font-weight:700;left:-12px;line-height:20px;position:absolute;height:20px;width:20px;text-align:center;top:14px}.markdown-section p.tip code{background-color:#efefef}.markdown-section p.tip em{color:#000}.markdown-section p.warn{background:rgba(0,0,0,.1);border-radius:2px;padding:1rem}.markdown-section ul.task-list>li{list-style-type:none}body.close .sidebar{transform:translateX(-300px)}body.close .sidebar-toggle{width:auto}body.close .content{left:0}@media print{.app-nav,.github-corner,.sidebar,.sidebar-toggle{display:none}}@media screen and (max-width:768px){.github-corner,.sidebar,.sidebar-toggle{position:fixed}.app-nav{margin-top:16px}.app-nav li ul{top:30px}main{height:auto;overflow-x:hidden}.sidebar{left:-300px;transition:transform .25s ease-out}.content{left:0;max-width:100vw;position:static;padding-top:20px;transition:transform .25s ease}.app-nav,.github-corner{transition:transform .25s ease-out}.sidebar-toggle{background-color:transparent;width:auto;padding:30px 30px 10px 10px}body.close .sidebar{transform:translateX(300px)}body.close .sidebar-toggle{background-color:hsla(0,0%,100%,.8);transition:background-color 1s;width:284px;padding:10px}body.close .content{transform:translateX(300px)}body.close .app-nav,body.close .github-corner{display:none}.github-corner:hover .octo-arm{-webkit-animation:none;animation:none}.github-corner .octo-arm{-webkit-animation:octocat-wave .56s ease-in-out;animation:octocat-wave .56s ease-in-out}}@-webkit-keyframes octocat-wave{0%,to{transform:rotate(0)}20%,60%{transform:rotate(-25deg)}40%,80%{transform:rotate(10deg)}}@keyframes octocat-wave{0%,to{transform:rotate(0)}20%,60%{transform:rotate(-25deg)}40%,80%{transform:rotate(10deg)}}section.cover{align-items:center;background-position:50%;background-repeat:no-repeat;background-size:cover;height:100vh;width:100vw;display:none}section.cover.show{display:flex}section.cover.has-mask .mask{background-color:#fff;opacity:.8;position:absolute;top:0;height:100%;width:100%}section.cover .cover-main{flex:1;margin:-20px 16px 0;text-align:center;position:relative}section.cover a{color:inherit}section.cover a,section.cover a:hover{text-decoration:none}section.cover p{line-height:1.5rem;margin:1em 0}section.cover h1{color:inherit;font-size:2.5rem;font-weight:300;margin:.625rem 0 2.5rem;position:relative;text-align:center}section.cover h1 a{display:block}section.cover h1 small{bottom:-.4375rem;font-size:1rem;position:absolute}section.cover blockquote{font-size:1.5rem;text-align:center}section.cover ul{line-height:1.8;list-style-type:none;margin:1em auto;max-width:500px;padding:0}section.cover .cover-main>p:last-child a{border-radius:2rem;border:1px solid var(--theme-color,#000);box-sizing:border-box;color:var(--theme-color,#000);display:inline-block;font-size:1.05rem;letter-spacing:.1rem;margin:.5rem 1rem;padding:.75em 2rem;text-decoration:none;transition:all .15s ease}section.cover .cover-main>p:last-child a:last-child{background-color:var(--theme-color,#000);color:#fff}section.cover .cover-main>p:last-child a:last-child:hover{color:inherit;opacity:.8}section.cover .cover-main>p:last-child a:hover{color:inherit}section.cover blockquote>p>a{border-bottom:2px solid var(--theme-color,#000);transition:color .3s}section.cover blockquote>p>a:hover{color:var(--theme-color,#000)} ================================================ FILE: docs/css/vue.css ================================================ /* * Copyright 2024 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ @import url("fonts.css");*{-webkit-font-smoothing:antialiased;-webkit-overflow-scrolling:touch;-webkit-tap-highlight-color:rgba(0,0,0,0);-webkit-text-size-adjust:none;-webkit-touch-callout:none;box-sizing:border-box}body:not(.ready){overflow:hidden}body:not(.ready) .app-nav,body:not(.ready)>nav,body:not(.ready) [data-cloak]{display:none}div#app{font-size:30px;font-weight:lighter;margin:40vh auto;text-align:center}div#app:empty:before{content:"Loading..."}.emoji{height:1.2rem;vertical-align:middle}.progress{background-color:var(--theme-color,#42b983);height:2px;left:0;position:fixed;right:0;top:0;transition:width .2s,opacity .4s;width:0;z-index:999999}.search .search-keyword,.search a:hover{color:var(--theme-color,#42b983)}.search .search-keyword{font-style:normal;font-weight:700}body,html{height:100%}body{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;color:#34495e;font-family:Source Sans Pro,Helvetica Neue,Arial,sans-serif;font-size:15px;letter-spacing:0;margin:0;overflow-x:hidden}img{max-width:100%}a[disabled]{cursor:not-allowed;opacity:.6}kbd{border:1px solid #ccc;border-radius:3px;display:inline-block;font-size:12px!important;line-height:12px;margin-bottom:3px;padding:3px 5px;vertical-align:middle}li input[type=checkbox]{margin:0 .2em .25em 0;vertical-align:middle}.app-nav{margin:25px 60px 0 0;position:absolute;right:0;text-align:right;z-index:10}.app-nav.no-badge{margin-right:25px}.app-nav p{margin:0}.app-nav>a{margin:0 1rem;padding:5px 0}.app-nav li,.app-nav ul{display:inline-block;list-style:none;margin:0}.app-nav a{color:inherit;font-size:16px;text-decoration:none;transition:color .3s}.app-nav a.active,.app-nav a:hover{color:var(--theme-color,#42b983)}.app-nav a.active{border-bottom:2px solid var(--theme-color,#42b983)}.app-nav li{display:inline-block;margin:0 1rem;padding:5px 0;position:relative;cursor:pointer}.app-nav li ul{background-color:#fff;border:1px solid;border-color:#ddd #ddd #ccc;border-radius:4px;box-sizing:border-box;display:none;max-height:calc(100vh - 61px);overflow-y:auto;padding:10px 0;position:absolute;right:-15px;text-align:left;top:100%;white-space:nowrap}.app-nav li ul li{display:block;font-size:14px;line-height:1rem;margin:8px 14px;white-space:nowrap}.app-nav li ul a{display:block;font-size:inherit;margin:0;padding:0}.app-nav li ul a.active{border-bottom:0}.app-nav li:hover ul{display:block}.github-corner{border-bottom:0;position:fixed;right:0;text-decoration:none;top:0;z-index:1}.github-corner:hover .octo-arm{-webkit-animation:octocat-wave .56s ease-in-out;animation:octocat-wave .56s ease-in-out}.github-corner svg{color:#fff;fill:var(--theme-color,#42b983);height:80px;width:80px}main{display:block;position:relative;width:100vw;height:100%;z-index:0}main.hidden{display:none}.anchor{display:inline-block;text-decoration:none;transition:all .3s}.anchor span{color:#34495e}.anchor:hover{text-decoration:underline}.sidebar{border-right:1px solid rgba(0,0,0,.07);overflow-y:auto;padding:40px 0 0;position:absolute;top:0;bottom:0;left:0;transition:transform .25s ease-out;width:300px;z-index:20}.sidebar>h1{margin:0 auto 1rem;font-size:1.5rem;font-weight:300;text-align:center}.sidebar>h1 a{color:inherit;text-decoration:none}.sidebar>h1 .app-nav{display:block;position:static}.sidebar .sidebar-nav{line-height:2em;padding-bottom:40px}.sidebar li.collapse .app-sub-sidebar{display:none}.sidebar ul{margin:0 0 0 15px;padding:0}.sidebar li>p{font-weight:700;margin:0}.sidebar ul,.sidebar ul li{list-style:none}.sidebar ul li a{border-bottom:none;display:block}.sidebar ul li ul{padding-left:20px}.sidebar::-webkit-scrollbar{width:4px}.sidebar::-webkit-scrollbar-thumb{background:transparent;border-radius:4px}.sidebar:hover::-webkit-scrollbar-thumb{background:hsla(0,0%,53.3%,.4)}.sidebar:hover::-webkit-scrollbar-track{background:hsla(0,0%,53.3%,.1)}.sidebar-toggle{background-color:transparent;background-color:hsla(0,0%,100%,.8);border:0;outline:none;padding:10px;position:absolute;bottom:0;left:0;text-align:center;transition:opacity .3s;width:284px;z-index:30;cursor:pointer}.sidebar-toggle:hover .sidebar-toggle-button{opacity:.4}.sidebar-toggle span{background-color:var(--theme-color,#42b983);display:block;margin-bottom:4px;width:16px;height:2px}body.sticky .sidebar,body.sticky .sidebar-toggle{position:fixed}.content{padding-top:60px;position:absolute;top:0;right:0;bottom:0;left:300px;transition:left .25s ease}.markdown-section{margin:0 auto;max-width:80%;padding:30px 15px 40px;position:relative}.markdown-section>*{box-sizing:border-box;font-size:inherit}.markdown-section>:first-child{margin-top:0!important}.markdown-section hr{border:none;border-bottom:1px solid #eee;margin:2em 0}.markdown-section iframe{border:1px solid #eee;width:1px;min-width:100%}.markdown-section table{border-collapse:collapse;border-spacing:0;display:block;margin-bottom:1rem;overflow:auto;width:100%}.markdown-section th{font-weight:700}.markdown-section td,.markdown-section th{border:1px solid #ddd;padding:6px 13px}.markdown-section tr{border-top:1px solid #ccc}.markdown-section p.tip,.markdown-section tr:nth-child(2n){background-color:#f8f8f8}.markdown-section p.tip{border-bottom-right-radius:2px;border-left:4px solid #f66;border-top-right-radius:2px;margin:2em 0;padding:12px 24px 12px 30px;position:relative}.markdown-section p.tip:before{background-color:#f66;border-radius:100%;color:#fff;content:"!";font-family:Dosis,Source Sans Pro,Helvetica Neue,Arial,sans-serif;font-size:14px;font-weight:700;left:-12px;line-height:20px;position:absolute;height:20px;width:20px;text-align:center;top:14px}.markdown-section p.tip code{background-color:#efefef}.markdown-section p.tip em{color:#34495e}.markdown-section p.warn{background:rgba(66,185,131,.1);border-radius:2px;padding:1rem}.markdown-section ul.task-list>li{list-style-type:none}body.close .sidebar{transform:translateX(-300px)}body.close .sidebar-toggle{width:auto}body.close .content{left:0}@media print{.app-nav,.github-corner,.sidebar,.sidebar-toggle{display:none}}@media screen and (max-width:768px){.github-corner,.sidebar,.sidebar-toggle{position:fixed}.app-nav{margin-top:16px}.app-nav li ul{top:30px}main{height:auto;overflow-x:hidden}.sidebar{left:-300px;transition:transform .25s ease-out}.content{left:0;max-width:100vw;position:static;padding-top:20px;transition:transform .25s ease}.app-nav,.github-corner{transition:transform .25s ease-out}.sidebar-toggle{background-color:transparent;width:auto;padding:30px 30px 10px 10px}body.close .sidebar{transform:translateX(300px)}body.close .sidebar-toggle{background-color:hsla(0,0%,100%,.8);transition:background-color 1s;width:284px;padding:10px}body.close .content{transform:translateX(300px)}body.close .app-nav,body.close .github-corner{display:none}.github-corner:hover .octo-arm{-webkit-animation:none;animation:none}.github-corner .octo-arm{-webkit-animation:octocat-wave .56s ease-in-out;animation:octocat-wave .56s ease-in-out}}@-webkit-keyframes octocat-wave{0%,to{transform:rotate(0)}20%,60%{transform:rotate(-25deg)}40%,80%{transform:rotate(10deg)}}@keyframes octocat-wave{0%,to{transform:rotate(0)}20%,60%{transform:rotate(-25deg)}40%,80%{transform:rotate(10deg)}}section.cover{align-items:center;background-position:50%;background-repeat:no-repeat;background-size:cover;height:100vh;width:100vw;display:none}section.cover.show{display:flex}section.cover.has-mask .mask{background-color:#fff;opacity:.8;position:absolute;top:0;height:100%;width:100%}section.cover .cover-main{flex:1;margin:-20px 16px 0;text-align:center;position:relative}section.cover a{color:inherit}section.cover a,section.cover a:hover{text-decoration:none}section.cover p{line-height:1.5rem;margin:1em 0}section.cover h1{color:inherit;font-size:2.5rem;font-weight:300;margin:.625rem 0 2.5rem;position:relative;text-align:center}section.cover h1 a{display:block}section.cover h1 small{bottom:-.4375rem;font-size:1rem;position:absolute}section.cover blockquote{font-size:1.5rem;text-align:center}section.cover ul{line-height:1.8;list-style-type:none;margin:1em auto;max-width:500px;padding:0}section.cover .cover-main>p:last-child a{border-radius:2rem;border:1px solid var(--theme-color,#42b983);box-sizing:border-box;color:var(--theme-color,#42b983);display:inline-block;font-size:1.05rem;letter-spacing:.1rem;margin:.5rem 1rem;padding:.75em 2rem;text-decoration:none;transition:all .15s ease}section.cover .cover-main>p:last-child a:last-child{background-color:var(--theme-color,#42b983);color:#fff}section.cover .cover-main>p:last-child a:last-child:hover{color:inherit;opacity:.8}section.cover .cover-main>p:last-child a:hover{color:inherit}section.cover blockquote>p>a{border-bottom:2px solid var(--theme-color,#42b983);transition:color .3s}section.cover blockquote>p>a:hover{color:var(--theme-color,#42b983)}.sidebar,body{background-color:#fff}.sidebar{color:#364149}.sidebar li{margin:6px 0}.sidebar ul li a{color:#505d6b;font-size:14px;font-weight:400;overflow:hidden;text-decoration:none;text-overflow:ellipsis;white-space:nowrap}.sidebar ul li a:hover{text-decoration:underline}.sidebar ul li ul{padding:0}.sidebar ul li.active>a{border-right:2px solid;color:var(--theme-color,#42b983);font-weight:600}.app-sub-sidebar li:before{content:"-";padding-right:4px;float:left}.markdown-section h1,.markdown-section h2,.markdown-section h3,.markdown-section h4,.markdown-section strong{color:#2c3e50;font-weight:600}.markdown-section a{color:var(--theme-color,#42b983);font-weight:600}.markdown-section h1{font-size:2rem;margin:0 0 1rem}.markdown-section h2{font-size:1.75rem;margin:45px 0 .8rem}.markdown-section h3{font-size:1.5rem;margin:40px 0 .6rem}.markdown-section h4{font-size:1.25rem}.markdown-section h5{font-size:1rem}.markdown-section h6{color:#777;font-size:1rem}.markdown-section figure,.markdown-section p{margin:1.2em 0}.markdown-section ol,.markdown-section p,.markdown-section ul{line-height:1.6rem;word-spacing:.05rem}.markdown-section ol,.markdown-section ul{padding-left:1.5rem}.markdown-section blockquote{border-left:4px solid var(--theme-color,#42b983);color:#858585;margin:2em 0;padding-left:20px}.markdown-section blockquote p{font-weight:600;margin-left:0}.markdown-section iframe{margin:1em 0}.markdown-section em{color:#7f8c8d}.markdown-section code,.markdown-section output:after,.markdown-section pre{font-family:Roboto Mono,Monaco,courier,monospace}.markdown-section code,.markdown-section pre{background-color:#f8f8f8}.markdown-section output,.markdown-section pre{margin:1.2em 0;position:relative}.markdown-section output,.markdown-section pre>code{border-radius:2px;display:block}.markdown-section output:after,.markdown-section pre>code{-moz-osx-font-smoothing:initial;-webkit-font-smoothing:initial}.markdown-section code{border-radius:2px;color:#e96900;margin:0 2px;padding:3px 5px;white-space:pre-wrap}.markdown-section>:not(h1):not(h2):not(h3):not(h4):not(h5):not(h6) code{font-size:.8rem}.markdown-section pre{padding:0 1.4rem;line-height:1.5rem;overflow:auto;word-wrap:normal}.markdown-section pre>code{color:#525252;font-size:.8rem;padding:2.2em 5px;line-height:inherit;margin:0 2px;max-width:inherit;overflow:inherit;white-space:inherit}.markdown-section output{padding:1.7rem 1.4rem;border:1px dotted #ccc}.markdown-section output>:first-child{margin-top:0}.markdown-section output>:last-child{margin-bottom:0}.markdown-section code:after,.markdown-section code:before,.markdown-section output:after,.markdown-section output:before{letter-spacing:.05rem}.markdown-section output:after,.markdown-section pre:after{color:#ccc;font-size:.6rem;font-weight:600;height:15px;line-height:15px;padding:5px 10px 0;position:absolute;right:0;text-align:right;top:0;content:attr(data-lang)}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#8e908c}.token.namespace{opacity:.7}.token.boolean,.token.number{color:#c76b29}.token.punctuation{color:#525252}.token.property{color:#c08b30}.token.tag{color:#2973b7}.token.string{color:var(--theme-color,#42b983)}.token.selector{color:#6679cc}.token.attr-name{color:#2973b7}.language-css .token.string,.style .token.string,.token.entity,.token.url{color:#22a2c9}.token.attr-value,.token.control,.token.directive,.token.unit{color:var(--theme-color,#42b983)}.token.function,.token.keyword{color:#e96900}.token.atrule,.token.regex,.token.statement{color:#22a2c9}.token.placeholder,.token.variable{color:#3d8fd1}.token.deleted{text-decoration:line-through}.token.inserted{border-bottom:1px dotted #202746;text-decoration:none}.token.italic{font-style:italic}.token.bold,.token.important{font-weight:700}.token.important{color:#c94922}.token.entity{cursor:help}code .token{-moz-osx-font-smoothing:initial;-webkit-font-smoothing:initial;min-height:1.5rem;position:relative;left:auto} ================================================ FILE: docs/en/README.md ================================================ apollo-logo # Introduction Apollo is a reliable configuration management system. It can centrally manage the configurations of different applications and different clusters. It is suitable for microservice configuration management scenarios. The server side is developed based on Spring Boot and Spring Cloud, which can simply run without the need to install additional application containers such as Tomcat. The Java SDK does not rely on any framework and can run in all Java runtime environments. It also has good support for Spring/Spring Boot environments. The .Net SDK does not rely on any framework and can run in all .Net runtime environments. For more details of the product introduction, please refer [Introduction to Apollo Configuration Center](en/design/apollo-introduction). For local demo purpose, please refer [Quick Start](en/deployment/quick-start). Demo Environment: - [http://81.68.181.139](http://81.68.181.139/) - User/Password: apollo/admin # Screenshots ![Screenshot](images/apollo-home-screenshot.jpg) # Features * **Unified management of the configurations of different environments and different clusters** * Apollo provides a unified interface to centrally manage the configurations of different environments, different clusters, and different namespaces * The same codebase could have different configurations when deployed in different clusters * With the namespace concept, it is easy to support multiple applications to share the same configurations, while also allowing them to customize the configurations * Multiple languages is provided in user interface(currently Chinese and English) * **Configuration changes takes effect in real time (hot release)** * After the user modified the configuration and released it in Apollo, the sdk will receive the latest configurations in real time (1 second) and notify the application * **Release version management** * Every configuration releases are versioned, which is friendly to support configuration rollback * **Grayscale release** * Support grayscale configuration release, for example, after clicking release, it will only take effect for some application instances. After a period of observation, we could push the configurations to all application instances if there is no problem - **Global Search Configuration Items** - A fuzzy search of the key and value of a configuration item finds in which application, environment, cluster, namespace the configuration item with the corresponding value is used - It is easy for administrators and SRE roles to quickly and easily find and change the configuration values of resources by highlighting, paging and jumping through configurations * **Authorization management, release approval and operation audit** * Great authorization mechanism is designed for applications and configurations management, and the management of configurations is divided into two operations: editing and publishing, therefore greatly reducing human errors * All operations have audit logs for easy tracking of problems * **Client side configuration information monitoring** * It's very easy to see which instances are using the configurations and what versions they are using * **Rich SDKs available** * Provides native sdks of Java and .Net to facilitate application integration * Support Spring Placeholder, Annotation and Spring Boot ConfigurationProperties for easy application use (requires Spring 3.1.1+) * Http APIs are provided, so non-Java and .Net applications can integrate conveniently * Rich third party sdks are also available, e.g. Golang, Python, NodeJS, PHP, C, etc * **Open platform API** * Apollo itself provides a unified configuration management interface, which supports features such as multi-environment, multi-data center configuration management, permissions, and process governance * However, for the sake of versatility, Apollo will not put too many restrictions on the modification of the configuration, as long as it conforms to the basic format, it can be saved. * In our research, we found that for some users, their configurations may have more complicated formats, such as xml, json, and the format needs to be verified * There are also some users such as DAL, which not only have a specific format, but also need to verify the entered value before saving, such as checking whether the database, username and password match * For this type of application, Apollo allows the application to modify and release configurations through open APIs, which has great authorization and permission control mechanism built in * **Simple deployment** * As an infrastructure service, the configuration center has very high availability requirements, which forces Apollo to rely on external dependencies as little as possible * Currently, the only external dependency is MySQL, so the deployment is very simple. Apollo can run as long as Java and MySQL are installed * Apollo also provides a packaging script, which can generate all required installation packages with just one click, and supports customization of runtime parameters # Release Notes * [Releases](https://github.com/apolloconfig/apollo/releases) # Presentation * [Design and Implementation Details of Apollo](http://www.itdks.com/dakalive/detail/3420) * [Slides](https://github.com/apolloconfig/apollo-community/blob/master/slides/design-and-implementation-of-apollo.pdf) * [Configuration Center Makes Microservices Smart](https://2018.qconshanghai.com/presentation/799) * [Slides](https://github.com/apolloconfig/apollo-community/blob/master/slides/configuration-center-makes-microservices-smart.pdf) # Publication * [Design and Implementation Details of Apollo](https://www.infoq.cn/article/open-source-configuration-center-apollo) * [Configuration Center Makes Microservices Smart](https://mp.weixin.qq.com/s/iDmYJre_ULEIxuliu1EbIQ) # License The project is licensed under the [Apache 2 license](https://github.com/apolloconfig/apollo/blob/master/LICENSE). ================================================ FILE: docs/en/_navbar.md ================================================ - Community - [Team](en/community/team.md) - [Community Governance](en/governance.md) - [Contributing Guide](en/contributing.md) - [Acknowledgements](en/community/thank-you.md) - Translations - [:uk: English](/en/) - [:cn: 中文](/zh/) ================================================ FILE: docs/en/_sidebar.md ================================================ - [**Home**](en/README.md) - Design Document - [Apollo Config Center Design](en/design/apollo-design.md) - [Apollo Config Center Introduction](en/design/apollo-introduction.md) - [Apollo Core Concept Namespace](en/design/apollo-core-concept-namespace.md) - [Apollo Source Code Analysis](http://www.iocoder.cn/categories/Apollo/) - Deployment Guide - [Quick Start](en/deployment/quick-start.md) - [Deployment Quick Start By Docker](en/deployment/quick-start-docker.md) - [Deployment Architecture](en/deployment/deployment-architecture.md) - [Distributed Deployment Guide](en/deployment/distributed-deployment-guide.md) - Deployment By Third-party Tool - [Install the HA Apollo cluster in Rainbond with one-click](en/deployment/third-party-tool-rainbond.md) - [Quickly deploy Apollo based on the aaPanel](en/deployment/third-party-tool-btpanel.md) - Admin Guide - [Apollo Usage Guide](en/portal/apollo-user-guide.md) - [Apollo Openapi Guide](en/portal/apollo-open-api-platform.md) - [Apollo Security Best Practices](en/portal/apollo-user-guide?id=_71-security-related) - [Apollo User Practices](en/portal/apollo-user-practices.md) - [Apollo Use Cases](https://github.com/ctripcorp/apollo-use-cases) - SDK Guide - [Java Client Usage Guide](en/client/java-sdk-user-guide.md) - [.Net Client Usage Guide](en/client/dotnet-sdk-user-guide.md) - [Golang Client Usage Guide](en/client/golang-sdks-user-guide.md) - [Python Client Usage Guide](en/client/python-sdks-user-guide.md) - [NodeJS Client Usage Guide](en/client/nodejs-sdks-user-guide.md) - [PHP Client Usage Guide](en/client/php-sdks-user-guide.md) - [C Client Usage Guide](en/client/c-sdks-user-guide.md) - [C++ Client Usage Guide](en/client/cpp-sdks-user-guide.md) - [Rust Client Usage Guide](en/client/rust-sdks-user-guide.md) - [K8S ConfigMap Integration Usage Guide](en/client/k8s-configmap-user-guide.md) - [HTTP API Guide](en/client/other-language-client-user-guide.md) - Extension Guide - [Portal Implement User Login Function](en/extension/portal-how-to-implement-user-login-function.md) - [Portal Enable Email Service](en/extension/portal-how-to-enable-email-service.md) - [Portal Enable Session Store](en/extension/portal-how-to-enable-session-store.md) - [Portal Enable Webhook Notification](en/extension/portal-how-to-enable-webhook-notification.md) - Contributor Guide - [Apollo Development Guide](en/contribution/apollo-development-guide.md) - Code Styles - [Eclipse Code Style](https://github.com/apolloconfig/apollo/blob/master/apollo-buildtools/style/eclipse-java-google-style.xml) - [Intellij Code Style](https://github.com/apolloconfig/apollo/blob/master/apollo-buildtools/style/intellij-java-google-style.xml) - [Release New Version Guide](en/contribution/apollo-release-guide.md) - [Contributing Guide](en/contributing.md) - FAQ - [Frequently Asked Question](en/faq/faq.md) - [Common Issues In Deployment & Development Phase](en/faq/common-issues-in-deployment-and-development-phase.md) - Other - [Release History](https://github.com/apolloconfig/apollo/releases) - [Apollo Benchmark](en/misc/apollo-benchmark.md) - Community - [Team](en/community/team.md) - [Community Governance](en/governance.md) - [Acknowledgements](en/community/thank-you.md) ================================================ FILE: docs/en/client/c-sdks-user-guide.md ================================================ ### Apollo C client Project address: [apollo-c-client](https://github.com/lzeqian/apollo) > Thanks [@lzeqian](https://github.com/lzeqian) for providing support for the C Apollo client ================================================ FILE: docs/en/client/cpp-sdks-user-guide.md ================================================ ### Apollo C++ client Project address: [apollo-cpp-client](https://github.com/jiazhanfeng1989/apollo-cpp-client) > Thanks [@jiazhanfeng](https://github.com/jiazhanfeng1989) for providing support for the C++ Apollo client ================================================ FILE: docs/en/client/dotnet-sdk-user-guide.md ================================================ > Note: This document is intended for users of Apollo systems. If you are a developer/maintainer of Apollo systems in your company, it is recommended to refer to [Apollo Development Guide](en/contribution/apollo-development-guide) first. #   # ! Important ! > Net access documentation, please refer to [Apollo.net Framework Integration](https://github.com/apolloconfig/apollo.net#一框架集成) # I. Preparation ## 1.1 Environment requirements * .Net: 4.0+ ## 1.2 Mandatory settings Apollo client relies on `AppId`, `Environment` and other environment information to work, so make sure to read the following instructions and do the correct configuration. ### 1.2.1 AppId AppId is the identity of the application and is an important piece of information to get the configuration from the server. Make sure you have the AppID configuration in `app.config` or `web.config`, where the content looks like ```xml ``` > Note: app.id is a unique id used to identify the application identity in string format. ### 1.2.2 Environment Apollo supports applications that have different configurations for different environments, so Environment is another important piece of information to get the configuration from the server. Environment is specified through a configuration file in the location `C:\opt\settings\server.properties`, with the contents of the file shaped like this ```properties env=DEV ``` Currently, `env` supports the following values (case-insensitive). * DEV * Development environment * FAT * Feature Acceptance Test environment * UAT * User Acceptance Test environment * PRO * Production environment ### 1.2.3 Service address Apollo clients get their configuration from different servers for different environments, so make sure that the server address (Apollo.{ENV}.Meta) is correctly configured in app.config or web.config, where the content looks like ```xml ``` ### 1.2.4 Local cache path The Apollo client will cache a copy of the configuration obtained from the server in the local file system, so that if the service is unavailable or the network is down, the configuration can still be restored locally without affecting the normal operation of the application. The local cache path is located in `C:\opt\data\{appId}\config-cache`, so please make sure that the `C:\opt\data\` directory exists and the application has read/write access. ### 1.2.5 Optional settings **Cluster** Apollo supports configuration by cluster, meaning that for an appId and an environment, there can be different configurations for different clusters. If you need to use this feature, you can specify the runtime clusters by. 1. via App Config * We can specify the runtime cluster by setting Apollo.Cluster in the App.config file (note the case) * For example, the following screenshot configuration specifies the runtime cluster as SomeCluster * ![apollo-net-apollo-cluster](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/apollo-net-apollo-cluster.png) 2. via configuration file * First make sure that `C:\opt\settings\server.properties` exists on the target machine * In this file, you can set the data center cluster, such as `idc=xxx` * Note that the key is all lowercase **Cluster Precedence** (cluster order) 1. If `Apollo.Cluster` and `idc` are both specified. * We will first try to load the configuration from the cluster specified by `Apollo.Cluster` * If no configuration is found, we will try to load the configuration from the cluster specified by `idc`. * If nothing is found, we will load from the default cluster (`default`) 2. If only `Apollo.Cluster` is specified. * We will first try to load the configuration from the cluster specified by `Apollo.Cluster` * If not found, it will be loaded from the default cluster (`default`) 3. If only `idc` is specified. * We will first try to load the configuration from the cluster specified by `idc` * If not found, it will be loaded from the default cluster (`default`) 4. If neither `Apollo.Cluster` nor `idc` is specified. * It will load the configuration from the default cluster (`default`) # II. DLL references Net client project address is located at: [https://github.com/ctripcorp/apollo.net](https://github.com/ctripcorp/apollo.net). Download the project locally, switch to the `Release` configuration, compile the Solution and it will generate `Framework.Apollo.Client.dll` in `apollo.net\Apollo\bin\Release`. Just reference `Framework.Apollo.Client.dll` in your application. .Net Core, you can refer to [dotnet-core](https://github.com/ctripcorp/apollo.net/tree/dotnet-core) and [nuget repository](https://www.nuget.org/packages?q=Com.Ctrip.Framework.Apollo) # III. Client-side usage ## 3.1 Get the configuration of the default namespace (application) ```c# Config config = ConfigService.GetAppConfig(); //config instance is singleton for each namespace and is never null string someKey = "someKeyFromDefaultNamespace"; string someDefaultValue = "someDefaultValueForTheKey"; string value = config.GetProperty(someKey, someDefaultValue); ``` With the above **config.getProperty** you can get the real-time latest configuration value corresponding to someKey. In addition, the configuration values are fetched from memory, so there is no need for the application to do its own caching. ## 3.2 Listening for configuration change events Listening for configuration change events is only used when the application really cares about configuration changes and needs to be notified when the configuration changes, e.g. when the database connection string changes and the connection needs to be rebuilt, etc. If you just want to fetch the latest configuration every time, just call **config.GetProperty** as in the example above. ```c# Config config = ConfigService.GetAppConfig(); //config instance is singleton for each namespace and is never null config.ConfigChanged += new ConfigChangeEvent(OnChanged); private void OnChanged(object sender, ConfigChangeEventArgs changeEvent) { Console.WriteLine("Changes for namespace {0}", changeEvent.Namespace); foreach (string key in changeEvent.ChangedKeys) { ConfigChange change = changeEvent.GetChange(key); Console.WriteLine("Change - key: {0}, oldValue: {1}, newValue: {2}, changeType: {3}", change.PropertyName, change, NewValue, change.ChangeType); } } ``` ## 3.3 Get the configuration of the public Namespace ```c# string somePublicNamespace = "CAT"; Config config = ConfigService.GetConfig(somePublicNamespace); //config instance is singleton for each namespace and is never null string someKey = "someKeyFromPublicNamespace"; string someDefaultValue = "someDefaultValueForTheKey"; string value = config.GetProperty(someKey, someDefaultValue); ``` ## 3.4 Demo There is a sample client project in apollo.net project: `ApolloDemo`, you can refer to [2.4 .Net sample client startup](en/contribution/apollo-development-guide?id=_24-net-sample-client-startup) for more information. >Net client open source version will output logs directly to the Console by default, you can implement your own logging-related features. > >See [https://github.com/ctripcorp/apollo.net/tree/master/Apollo/Logging/Spi](https://github.com/apolloconfig/apollo.net/tree/dotnet-old/Apollo/Spi) for more details . # IV. Client design ![client-architecture](https://github.com/apolloconfig/apollo/raw/master/doc/images/client-architecture.png) The above diagram briefly describes the principle of Apollo client implementation. 1. The client and the server maintain a long connection, so that the first time to get the configuration updates pushed. (achieved through Http Long Polling) . 2. The client also regularly pulls the latest configuration of the application from the Apollo Configuration Center server. * This is a fallback mechanism to prevent the configuration from being updated due to the failure of the push mechanism. * The client will report the local version of the timed pull, so in general, for the timed pull operation, the server will return 304 - Not Modified. * Timing frequency is pulled every 5 minutes by default, client can also set `Apollo.RefreshInterval` through App.config to override it in milliseconds. 3. The client will save the latest configuration of the application in memory after getting it from the Apollo Configuration Center server 4. The client will cache a copy of the configuration obtained from the server in the local file system. In case of service unavailability or network failure, the configuration can still be restored locally. 5. The application can get the latest configuration from the Apollo client, subscribe to configuration update notifications. # V. Local Development Mode Apollo client also supports local development mode, which is mainly used when the development environment cannot connect to Apollo server, such as doing related function development on cruise ships or airplanes. In local development mode, Apollo will only read configuration information from local files, not from Apollo server. You can enable Apollo local development mode by following the steps below. ## 5.1 Modify the environment Modify the `C:\opt\settings\server.properties` file to set the env to Local: ```properties env=Local ``` ## 5.2 Preparing local configuration files In local development mode, Apollo client will read files from local, so we need to prepare the configuration file beforehand. ### 5.2.1 Local configuration directory The local configuration directory is located at: C:\opt\data\{_appId_}\config-cache. The appId is the appId of the application, e.g. 100004458. Please make sure that the directory exists and the application has read access to the directory. **[Tip]** The recommended way is to use Apollo in normal mode first, so that Apollo will automatically create the directory and generate the configuration file under it. ### 5.2.2 Local configuration files Local configuration files need to be placed in the local configuration directory according to a certain file name format, which is as follows. **_{appId}+{cluster}+{namespace}.json_** * AppId is the application's own appId, such as 100004458 * Cluster is the cluster used by the application, generally in local mode without configuration, it is default * Namespace is the configuration `namespace` used by the application, usually `application` ![client-local-cache](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/apollo-net-config-cache.png) The content of the file is stored in json format, for example, if there are two keys, one is request.timeout, the other is batch, then the content of the file is the following format. ```json { "request.timeout": "1000", "batch": "2000" } ``` ## 5.3 Modifying the configuration In local development mode, Apollo does not monitor the file content for changes in real time, so if you modify the configuration, you need to restart the application to take effect. ================================================ FILE: docs/en/client/golang-sdks-user-guide.md ================================================ ### Apollo Go client 1 Project address: [apolloconfig/agollo](https://github.com/apolloconfig/agollo) > Thanks [@zouyx](https://github.com/zouyx) for providing support for the Go Apollo client ### Apollo Go client 2 Project address: [philchia/agollo](https://github.com/philchia/agollo) > Thanks [@philchia](https://github.com/philchia) for providing support for the Go Apollo client ### Apollo Go client 3 Project address: [shima-park/agollo](https://github.com/shima-park/agollo) > Thanks [@shima-park](https://github.com/shima-park) for providing support for the Go Apollo client ### Apollo Go client 4 Project address: [go-microservices/php_conf_agent](https://github.com/go-microservices/php_conf_agent) > Thanks [@GanymedeNil](https://github.com/GanymedeNil) for providing support for the Go Apollo client ### Apollo Go client 5 Project address: [hyperjiang/lunar](https://github.com/hyperjiang/lunar) > Thanks [@hyperjiang](https://github.com/hyperjiang) for providing support for the Go Apollo client ### Apollo Go client 6 Project address: [tagconfig/tagconfig](https://github.com/tagconfig/tagconfig) > Thanks [@n0trace](https://github.com/n0trace) for providing support for the Go Apollo client ### Apollo Go client 7 Project address: [go-chassis/go-archaius](https://github.com/go-chassis/go-archaius/tree/master/examples/apollo) > Thanks [@tianxiaoliang](https://github.com/tianxiaoliang) and [@Shonminh](https://github.com/Shonminh) for providing support for the Go Apollo client ### Apollo Go client 8 Project address: [xhrg-product/apollo-client-golang](https://github.com/xhrg-product/apollo-client-golang) > Thanks [@xhrg](https://github.com/xhrg) for providing support for the Go Apollo client ### Apollo Go client 9 Project address: [xnzone/apollo-go](https://github.com/xnzone/apollo-go) > Thanks [@xnzone](https://github.com/xnzone) for providing support for the Go Apollo client ================================================ FILE: docs/en/client/java-sdk-user-guide.md ================================================ > Note: This document is intended for users of Apollo systems. If you are a developer/maintainer of Apollo systems in your company, it is recommended to refer to [Apollo Development Guide](en/contribution/apollo-development-guide) first. #   # I. Preparation ## 1.1 Environment requirements * Java: 1.8+ * To run in Java 1.7 runtime environment, please use 1.x version of apollo client, such as 1.9.1 * Guava: 22.0+ * The Apollo client will reference Guava 32 by default. If your project references other versions, make sure the version number is greater than or equal to 22.0 >Note: For Apollo client, you can make a few code changes to downgrade to Java 1.6 if needed, see [Issue 483](https://github.com/apolloconfig/apollo/issues/483) for details ## 1.2 Mandatory settings Apollo client depends on `AppId`, `Apollo Meta Server` and other environment information to work, so please make sure to read the following instructions and do the correct configuration. ### 1.2.1 AppId AppId is the identity of the application and is an important piece of information to get the configuration from the server. There are several ways to set it, from highest to lowest priority, as follows. 1. System Property Apollo 0.7.0+ supports passing in app.id information via System Property, such as ```bash -Dapp.id=YOUR-APP-ID ``` 2. System Environment of the operating system Apollo 1.4.0+ supports passing app.id information via the operating system's System Environment `APP_ID`, as in ```bash APP_ID=YOUR-APP-ID ``` 3. Spring Boot application.properties Apollo 1.0.0+ supports configuration via Spring Boot's application.properties file, such as ```properties app.id=YOUR-APP-ID ``` > This configuration does not work with multiple war packages deployed in the same tomcat 4. app.properties Make sure that the classpath:/META-INF/app.properties file exists and that its contents look like. >app.id=YOUR-APP-ID The file location reference is as follows. ![app-id-location](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/app-id-location.png) > Note: app.id is a unique id used to identify the application identity in the format string. ### 1.2.2 Apollo Meta Server Apollo supports applications with different configurations in different environments, so you need to provide the [Apollo Meta Server](en/design/apollo-design?id=_133-meta-server) information of the current environment to Apollo clients at runtime. By default, the meta server and config service are deployed in the same JVM process, so the address of the meta server is the address of the config service. To achieve high availability of meta server, it is recommended to do dynamic load balancing by SLB (Software Load Balancer). meta server address can also be filled with IP, such as `http://1.1.1.1:8080,http://2.2.2.2:8080`, but it is still recommended for production environment Use the domain name (go SLB), because machine expansion, shrinkage, etc. may lead to changes in the IP list. The following ways to configure apollo meta server information are supported since version 1.0.0, in descending order of priority 1. Via the Java System Property `apollo.meta` * Can be specified via the Java System Property `apollo.meta` * Can be specified in the Java program startup script with `-Dapollo.meta=http://config-service-url` * If you are running a jar file, you need to note that the format is `java -Dapollo.meta=http://config-service-url -jar xxx.jar` * You can also specify it programmatically, e.g. `System.setProperty("apollo.meta", "http://config-service-url");` 2. Through the Spring Boot configuration file * You can specify `apollo.meta=http://config-service-url` in `application.properties` or `bootstrap.properties` of Spring Boot > This configuration does not work with multiple war packages deployed in the same tomcat 3. Through the operating system's System Environment `APOLLO_META` * Can be specified by System Environment `APOLLO_META` of the operating system * Note that the key is all-caps and separated by `_`. 4. Via `server.properties` configuration file * You can specify `apollo.meta=http://config-service-url` in the `server.properties` configuration file * For Mac/Linux, the default file location is `/sopt/settings/server.properties`. * For Windows, the default file location is `C:\opt\settings\server.properties` 5. Via the `app.properties` configuration file * You can specify `apollo.meta=http://config-service-url` in `classpath:/META-INF/app.properties` 6. Via Java system property `${env}_meta` * If the current [env](#_1241-environment) is `dev`, then the user can configure `-Ddev_meta=http://config-service-url` * Using this configuration method, then the Environment must be configured correctly, see [1.2.4.1 Environment](en/client/java-sdk-user-guide?id=_1241-environment) for details 7. Via the OS System Environment `${ENV}_META` (supported since version 1.2.0) * If the current [env](#_1241-environment) is `dev`, then the user can configure the OS System Environment `DEV_META=http://config-service-url` * Note that the key is all-caps * Using this configuration method, then the Environment must be configured correctly, see [1.2.4.1 Environment](en/client/java-sdk-user-guide?id=_1241-environment) for details 8. Via the `apollo-env.properties` file * The user can also create an `apollo-env.properties` and put it under the classpath of the application or under the config directory of the spring boot application * If you use this configuration, then you must configure the Environment correctly, see [1.2.4.1 Environment](en/client/java-sdk-user-guide?id=_1241-environment) * The contents of the file look like this. ```properties dev.meta=http://1.1.1.1:8080 fat.meta=http://apollo.fat.xxx.com uat.meta=http://apollo.uat.xxx.com pro.meta=http://apollo.xxx.com ``` > If the Meta Server address cannot be obtained by any of the above means, Apollo will eventually fallback to `http://apollo.meta` as the Meta Server address #### 1.2.2.1 Customizing Apollo Meta Server Address Location Logic In version 1.0.0, Apollo provides the [MetaServerProvider SPI](https://github.com/apolloconfig/apollo/blob/master/apollo-core/src/main/java/com/ctrip/framework/apollo/core/spi/MetaServerProvider.java), which allows users to inject their own MetaServerProvider to customize the Meta Server address location logic. Since we use the typical [Java Service Loader pattern](https://docs.oracle.com/javase/7/docs/api/java/util/ServiceLoader.html), it is still relatively simple to implement. One thing to note is that apollo will iterate through all MetaServerProviders in order at runtime until a MetaServerProvider provides a non-empty Meta Server address, so users need to pay extra attention to the Order of the custom MetaServerProvider. The rule is that smaller Order has higher priority, so MetaServerProvider with Order=0 will be ranked ahead of MetaServerProvider with Order=1. **If your company has many applications that need to access Apollo, it is recommended to package a jar package and then provide a custom Apollo Meta Server positioning logic so that the applications that access Apollo can be used with zero configuration. For example, write your own `xx-company-apollo-client`, the jar package depends on `apollo-client`, define a custom MetaServerProvider implementation in the jar package by spi, and then the application directly depends on `xx-company-apollo-client`.** The implementation of MetaServerProvider can be found in [LegacyMetaServerProvider](https://github.com/apolloconfig/apollo/blob/master/apollo-core/src/main/java/com/ctrip/framework/apollo/core/internals/LegacyMetaServerProvider.java) and [DefaultMetaServerProvider](https://github.com/apolloconfig/apollo-java/blob/main/apollo-client/src/main/java/com/ctrip/framework/apollo/internals/DefaultMetaServerProvider.java). #### 1.2.2.2 Skip Apollo Meta Server service discovery > For apollo-client version 0.11.0 and above In general, it is recommended to use Apollo's Meta Server mechanism to implement service discovery for Config Service, so that high availability of Config Service can be achieved. However, apollo-client also supports skipping Meta Server service discovery, which is mainly used in the following scenarios: 1. 1. Config Service is deployed on the public cloud, and the address registered to Meta Server is an intranet address, so the local development environment cannot connect directly * If the Config Service is exposed through the public SLB, remember to set the IP whitelist to avoid data leakage. 2. Config Service is deployed in docker environment, and the address registered to Meta Server is docker intranet address, so local development environment cannot connect to it directly. 3. Config Service is deployed in kubernetes, and you want to use kubernetes' own service discovery capability (Service). For the above scenarios, you can skip Meta Server service discovery by directly specifying the Config Service address, in descending order of priority, as follows 1. Via Java System Property `apollo.config-service` (1.9.0+) or `apollo.configService` (before 1.9.0) * Can be specified via Java's System Property `apollo.config-service` (1.9.0+) or `apollo.configService` (before 1.9.0) * In the Java program startup script, you can specify `-Dapollo.config-service=http://config-service-url:port` * If you are running a jar file, you need to note that the format is `java -Dapollo.configService=http://config-service-url:port -jar xxx.jar` * It can also be specified programmatically, such as `System.setProperty("apollo.config-service", "http://config-service-url:port");` 2. Through the operating system's System Environment `APOLLO_CONFIG_SERVICE` (1.9.0+) or `APOLLO_CONFIGSERVICE` (before 1.9.0) * Can be specified by System Environment `APOLLO_CONFIG_SERVICE`(1.9.0+) or `APOLLO_CONFIGSERVICE`(before 1.9.0) of the operating system * Note that the key is all-caps and separated by `_`. 3. Via `server.properties` configuration file * You can specify `apollo.config-service=http://config-service-url:port`(1.9.0+) or `apollo.configService=http://config-service-url:port` in the `server.properties` configuration file (before 1.9.0) * For Mac/Linux, the default file location is `/opt/settings/server.properties` * For Windows, the default file location is `C:\opt\settings\server.properties` ### 1.2.3 Local cache path The Apollo client will cache a copy of the configuration obtained from the server in the local file system, so that if the service is unavailable or the network is down, the configuration can still be restored locally without affecting the normal operation of the application. The local cache path is located in the following path by default, so please make sure `/opt/data` or `C:\opt\data\` directory exists and the application has read/write permission. * **Mac/Linux**: /opt/data/{_appId_}/config-cache * **Windows**: C:\opt\data\\\{_appId_}\config-cache The local configuration file will be placed under the local cache path in the following filename format. **_{appId}+{cluster}+{namespace}.properties_** * AppId is the application's own appId, e.g. 100004458 * Cluster is the cluster used by the application, generally in local mode without configuration, it is default * Namespace is the configuration `namespace` used by the application, usually `application` ![client-local-cache](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/client-local-cache.png) The content of the file is stored in properties format, for example, if there are two keys, one is request.timeout, and the other is batch, then the content of the file is in the following format. ```properties request.timeout=2000 batch=2000 ``` > Note: If deployed in a Kubernetes environment, you can also enable the configMap cache to further improve availability #### 1.2.3.1 Customizing the cache path The following custom cache paths are supported since version 1.0.0, in descending order of priority 1. Via the Java System Property `apollo.cache-dir` (1.9.0+) or `apollo.cacheDir` (before 1.9.0) * Can be specified by Java's System Property `apollo.cache-dir`(1.9.0+) or `apollo.cacheDir`(before 1.9.0) * In the Java program startup script, you can specify `-Dapollo.cache-dir=/opt/data/some-cache-dir`(1.9.0+) or `apollo.cacheDir=/opt/data/some-cache-dir`(before 1.9.0) * If you are running a jar file, note that the format is `java -Dapollo.cache-dir=/opt/data/some-cache-dir -jar xxx.jar` (1.9.0+) or `java -Dapollo.cacheDir=/opt/data/some-cache-dir -jar xxx.jar`(before 1.9.0) * Can also be specified programmatically, e.g. `System.setProperty("apollo.cache-dir", "/opt/data/some-cache-dir");`(1.9.0+) or `System.setProperty("apollo.cacheDir", "/opt/data/some-cache-dir");`(before 1.9.0) 2. Via the Spring Boot configuration file * You can specify `apollo.cache-dir=/opt/data/some-cache-dir` (1.9.0+) or `apollo. cacheDir=/opt/data/some-cache-dir`(before 1.9.0) 3. Via OS System Environment `APOLLO_CACHE_DIR` (1.9.0+) or `APOLLO_CACHEDIR` (before 1.9.0) * Can be specified by the OS System Environment `APOLLO_CACHE_DIR`(1.9.0+) or `APOLLO_CACHEDIR`(before 1.9.0) * Note that the key is all-caps and separated by `_`. 4. Via `server.properties` configuration file * You can specify `apollo.cache-dir=/opt/data/some-cache-dir`(1.9.0+) or `apollo.cacheDir=/opt/data/some-cache-dir`(before 1.9. 0) in the `server.properties` configuration file. 0 or earlier) * For Mac/Linux, the default file location is `/opt/settings/server.properties` * For Windows, the default file location is `C:\opt\settings\server.properties` > Note: The local cache path can also be used for the disaster recovery directory. If the application needs to be expanded when all the config services are down, then the configuration can also be copied from the cache path on the existing machine to the same cache path on the new machine first ### 1.2.4 Optional settings #### 1.2.4.1 Environment The Environment can be configured in any of the following 3 ways. 1. Via Java System Property * Environment can be specified through Java's System Property `env`. * In the Java program startup script, you can specify `-Denv=YOUR-ENVIRONMENT` * If you are running a jar file, note that the format is `java -Denv=YOUR-ENVIRONMENT -jar xxx.jar` * Note that the key is all lowercase 2. Through the operating system's System Environment * You can also specify the System Environment `ENV` of the operating system * Note that the key is all uppercase 3. Through the configuration file * The last recommended way is to specify `env=YOUR-ENVIRONMENT` through the configuration file * For Mac/Linux, the default file location is `/opt/settings/server.properties`. * For Windows, the default file location is `C:\opt\settings\server.properties`. The contents of the file look like : ```properties env=DEV ``` Currently, `env` supports the following values (case-insensitive). * DEV * Development environment * FAT * Feature Acceptance Test environment * UAT * User Acceptance Test environment * PRO * Production environment For more environment definitions, you can refer to [Env.java](https://github.com/apolloconfig/apollo/blob/master/apollo-core/src/main/java/com/ctrip/framework/apollo/core/enums/Env.java) #### 1.2.4.2 Cluster Apollo supports configuration by cluster, which means that for an appId and an environment, there can be different configurations for different clusters. The following ways of clustering are supported since version 1.0.0, in descending order of priority. 1. Via Java System Property `apollo.cluster` * can be specified via Java's System Property `apollo.cluster` * Can be specified in the Java program startup script with `-Dapollo.cluster=SomeCluster` * If you are running a jar file, you need to note that the format is `java -Dapollo.cluster=SomeCluster -jar xxx.jar` * You can also specify it programmatically, e.g. `System.setProperty("apollo.cluster", "SomeCluster");` 2. Through the Spring Boot configuration file * You can specify `apollo.cluster=SomeCluster` in `application.properties` or `bootstrap.properties` of Spring Boot 3. Via Java System Property * You can specify the environment via Java's System Property `idc`. * In the Java program startup script, you can specify `-Didc=xxx` * If you are running a jar file, you should note that the format is `java -Didc=xxx -jar xxx.jar` * Note that the key is all lowercase 4. Through the operating system's System Environment * Can also be specified by the operating system's System Environment `IDC * Note that the key is all uppercase 5. Through the `server.properties` configuration file * You can specify `idc=xxx` in the `server.properties` configuration file * For Mac/Linux, the default file location is `/opt/settings/server.properties`. * For Windows, the default file location is `C:\opt\settings\server.properties` **Cluster Precedence** (cluster order) 1. If `apollo.cluster` and `idc` are both specified. * It will first try to load the configuration from the cluster specified by `apollo.cluster` * If nothing is found, we try to load the configuration from the cluster specified by `idc`. * If nothing is found, we will load from the default cluster (`default`) 2. If only `apollo.cluster` is specified. * We will first try to load the configuration from the cluster specified by `apollo.cluster` * If not found, it will be loaded from the default cluster (`default`) 3. If only `idc` is specified. * We will first try to load the configuration from the cluster specified by `idc` * If not found, it will be loaded from the default cluster (`default`) 4. If neither `apollo.cluster` nor `idc` is specified. * It will load the configuration from the default cluster (`default`) #### 1.2.4.3 Set whether the in-memory configuration items remain in the same order as they are on the page > For versions 1.6.0 and above By default, the in-memory configuration of the apollo client is stored in Properties (Hashtable underneath) and is not intentionally kept in the same order as seen on the page, which has no effect on most scenarios. However, some scenarios will strongly rely on the order of configuration items (such as the routing rules of spring cloud zuul), for this case, you can turn on the OrderedProperties feature to make the in-memory configuration order consistent with what you see on the page. The configuration methods, in descending order of priority, are 1. Via the Java System Property `apollo.properties.order.enable * Can be specified via Java's System Property `apollo.property.order.enable` * You can specify `-Dapollo.property.order.enable=true` in the Java program startup script * If you are running a jar file, you need to note that the format is `java -Dapollo.property.order.enable=true -jar xxx.jar` * You can also specify it programmatically, such as `System.setProperty("apollo.property.order.enable", "true");` 2. Via the Spring Boot configuration file * You can specify `apollo.properties.order.enable=true` in Spring Boot's `application.properties` or `bootstrap.properties` 3. Via the `app.properties` configuration file * You can specify `apollo.properties.order.enable=true` in `classpath:/META-INF/app.properties` #### 1.2.4.4 Configuring access keys > For versions 1.6.0 and above Apollo has added an access key mechanism since version 1.6.0 so that only authenticated clients can access sensitive configurations. If the application has access keys enabled, the client needs to configure the keys, otherwise the configuration cannot be accessed. The configuration methods are as follows, in descending order of priority 1. Via Java System Property `apollo.access-key.secret` (1.9.0+) or `apollo.accessskey.secret` (before 1.9.0) * Can be specified via Java's System Property `apollo.access-key.secret` (1.9.0+) or `apollo.accessskey.secret` (before 1.9.0) * In the Java application startup script, you can specify `-Dapollo.access-key.secret=1cf998c4e2ad4704b45a98a509d15719` (1.9.0+) or `-Dapollo.accesskey.secret=1cf998c4e2ad4704b45a98a509d15719`(before 1.9.0) * If running a jar file, note that the format is `java -Dapollo.access-key.secret=1cf998c4e2ad4704b45a98a509d15719 -jar xxx.jar` (1.9.0+) or `java -Dapollo.accesskey.secret=1cf998c4e2ad4704b45a98a509d15719 -jar xxx.jar`(before 1.9.0) * Can also be specified programmatically, such as `System.setProperty("apollo.access-key.secret", "1cf998c4e2ad4704b45a98a509d15719");`(1.9.0+) or `System.setProperty("apollo.accesskey.secret", "1cf998c4e2ad4704b45a98a509d15719");` (before 1.9.0) 2. Via the Spring Boot configuration file * You can specify `apollo.access-key.secret=1cf998c4e2ad4704b45a98a509d15719` in `application.properties` or `bootstrap.properties` of Spring Boot (1.9.0 +) or `apollo.accesskey.secret=1cf998c4e2ad4704b45a98a509d15719` (before 1.9.0) 3. Through the operating system's System Environment * Can also be specified through the OS System Environment `APOLLO_ACCESS_KEY_SECRET`(1.9.0+) or `APOLLO_ACCESSKEY_SECRET`(before 1.9.0) * Note that the key is all-caps 4. Via `app.properties` configuration file * You can specify `apollo.access-key.secret=1cf998c4e2ad4704b45a98a509d15719`(1.9.0+) or `apollo.accessskey.secret=1cf998c4e2ad4704b45a98a509d15719`(before 1.9.0) #### 1.2.4.5 Custom server.properties path > for version 1.8.0 and above The following ways to customize the server.properties path are supported since version 1.8.0, in descending order of priority 1. Via the Java System Property `apollo.path.server.properties` * Can be specified via the Java System Property `apollo.path.server.properties` * You can specify `-Dapollo.path.server.properties=/some-dir/some-file.properties` in the Java program startup script * If you are running a jar file, you need to note that the format is `java -Dapollo.path.server.properties=/some-dir/some-file.properties -jar xxx.jar` * Can also be specified programmatically, e.g. `System.setProperty("apollo.path.server.properties", "/some-dir/some-file.properties");` 2. Via the operating system's System Environment `APOLLO_PATH_SERVER_PROPERTIES` * can be specified by System Environment `APOLLO_PATH_SERVER_PROPERTIES` of the operating system * Note that the key is all-caps and separated by `_`. #### 1.2.4.6 Enables `propertyNames` caching, which can significantly improve startup speed in a large number of configuration scenarios > For version 1.9.0 and above In scenarios where `@ConfigurationProperties` is used and there are a large number of configuration items, the Spring container can be slow to start. This configuration can be turned on to significantly improve startup speed, and the cache will be automatically cleared when the configuration changes, default is `false`. See: [issue 3800](https://github.com/apolloconfig/apollo/issues/3800) The configuration methods, in descending order of priority, are 1. via Java System Property `apollo.property.names.cache.enable` * can be specified via the Java System Property `apollo.property.names.cache.enable` * You can specify `-Dapollo.property.names.cache.enable=true` in the Java program startup script * If you are running a jar file, note that the format is `java -Dapollo.property.names.cache.enable=true -jar xxx.jar` * You can also specify it programmatically, such as `System.setProperty("apollo.property.names.cache.enable", "true");` 2. via system environment variables * Configure the environment variable `APOLLO_PROPERTY_NAMES_CACHE_ENABLE=true` before starting the program to specify * Note that the key is all-caps and separated by `_`. 3. via the Spring Boot configuration file * You can specify `apollo.properties.names.cache.enable=true` in Spring Boot's `application.properties` or `bootstrap.properties`. 4. via the `app.properties` configuration file * You can specify `apollo.property.names.cache.enable=true` in `classpath:/META-INF/app.properties` #### 1.2.4.7 Apollo-Label ApolloLabel is the label information of the application, an important piece of information to get the configuration from the server side for the grayscale rules. There are several ways to set it, from highest to lowest priority, as follows. 1. System Property Apollo 2.0.0+ supports passing in apollo.label information via System Property, such as ```bash -Dapollo.label=YOUR-APOLLO-LABEL ``` 2. System Environment of the operating system Apollo 2.0.0+ supports passing apollo.label information through the operating system's System Environment ``APP_LABEL``, as in ```bash APOLLO_LABEL=YOUR-APOLLO-LABEL ``` 3. Spring boot `application.properties` Apollo 2.0.0+ supports configuration via Spring Boot's application.properties file, such as ```properties apollo.label=YOUR-APOLLO-LABEL ``` > This configuration is not suitable for multiple war packages deployed in the same tomcat scenario 4. `app.properties` Make sure that the `classpath:/META-INF/app.properties` file exists and that its contents are shaped like. >apollo.label=YOUR-APOLLO-LABEL The file location reference is as follows. ![app-id-location](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/app-id-location.png) > Note: apollo.label is a label used to identify the application identity in the format string. #### 1.2.4.8 Enable Apollo Override System Properties > For version 2.1.0 and above Flag to indicate that Apollo's remote properties should override system properties. Default true. The configuration methods, in descending order of priority, are 1. Via the Java System Property `apollo.override-system-properties` * Can be specified via Java's System Property `apollo.override-system-properties` * You can specify `-Dapollo.override-system-properties=true` in the Java program startup script * If you are running a jar file, you need to note that the format is `java -Dapollo.override-system-properties=true -jar xxx.jar` * You can also specify it programmatically, such as `System.setProperty("apollo.override-system-properties", "true");` 2. Via the Spring Boot configuration file * You can specify `apollo.override-system-properties=true` in Spring Boot's `application.properties` or `bootstrap.properties` 3. Via the `app.properties` configuration file * You can specify `apollo.override-system-properties=true` in `classpath:/META-INF/app.properties` #### 1.2.4.9 Enable Client Monitoring > For version 2.4.0 and above After enabling the following configurations, you can use `ConfigService.getConfigMonitor()` to retrieve client monitoring information and enable automatic reporting. ```properties # 1. Whether to enable the Monitor mechanism, i.e., whether ConfigMonitor is enabled. Default is false. apollo.client.monitor.enabled = true # 2. Whether to expose Monitor data by JMX. When enabled, you can view related information through tools like J-console. Default is false. apollo.client.monitor.jmx.enabled = true # 3. The maximum number of exception logs that Monitor can store. The default is 25, following the FIFO principle. apollo.client.monitor.exception-queue-size = 30 # 4. Specify the Exporter type for exporting metric data to the corresponding monitoring system. # If you introduce apollo-plugin-client-prometheus, set this to "prometheus" to enable it. # This depends on the SPI implementation of MetricsExporter. apollo.client.monitor.external.type = prometheus # 5. Specify the frequency at which the Exporter exports status information from Monitor as metric data. # The default is once every 10 seconds. apollo.client.monitor.external.export-period = 20 ``` #### 1.2.4.10 ConfigMap cache > For version 2.4.0 and above Starting from version 2.4.0, the availability of the client in the Kubernetes environment has been enhanced. After enabling the ConfigMap cache, the client will cache a copy of the configuration information fetched from the server in the ConfigMap. In the case of service unavailability, network issues, and loss of local cache files, the configuration can still be restored from the ConfigMap. Here are the relevant configurations: `apollo.cache.kubernetes.enable`:Whether to enable the ConfigMap cache mechanism, the default is false. `apollo.cache.kubernetes.namespace`:The namespace of the ConfigMap to be used (the namespace in Kubernetes), the default value is "default". The configuration information will be placed in the specified ConfigMap according to the following correspondence: namespace: Use the specified value, if not specified, the default is "default" configMapName: apollo-configcache-{appId} key:{cluster}___{namespace} value: The content is the JSON format string of the corresponding configuration information. > appId is the application's own appId, such as 100004458. > > cluster is the cluster used by the application, which is usually default if not configured locally > > namespace Indicates the configuration namespace used by the application. If '_' appears in the namespace, it will be escaped to '__' when the key is concatenated. > Since this feature is extended, so the client-java dependency is set to optional. You need to import the matching version > Since read and write operations on the ConfigMap are required, the pod where the client is located must have the corresponding permissions. The specific configuration method can be referred to below. How to authorize a Pod's Service Account to have read and write permissions for ConfigMap: 1. Create a Service Account: If there is no Service Account, you need to create one. ```yaml apiVersion: v1 kind: ServiceAccount metadata: name: my-service-account namespace: default ``` 2. Create a Role or ClusterRole: Define a Role or ClusterRole to grant read and write permissions for a specific ConfigMap. If the ConfigMap is used across multiple Namespaces, a ClusterRole should be used. ```yaml apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: namespace: default name: configmap-role rules: - apiGroups: [""] resources: ["configmaps"] verbs: ["get", "list", "watch", "create", "update", "delete"] ``` 3. Bind the Service Account to the Role or ClusterRole: Use RoleBinding or ClusterRoleBinding to bind the Service Account to the Role or ClusterRole created above. ```yaml apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: configmap-reader-binding namespace: default subjects: - kind: ServiceAccount name: my-service-account namespace: default roleRef: kind: Role name: configmap-role apiGroup: rbac.authorization.k8s.io ``` 4. Specify the Service Account in the Pod configuration: Ensure that the Pod's configuration uses the Service Account created above. ```yaml apiVersion: v1 kind: Pod metadata: name: my-pod namespace: default spec: serviceAccountName: my-service-account containers: - name: my-container image: my-image ``` 5. Apply the configuration: Use the kubectl command-line tool to apply these configurations. ```yaml kubectl apply -f service-account.yaml kubectl apply -f role.yaml kubectl apply -f role-binding.yaml kubectl apply -f pod.yaml ``` These steps give the Service Account in the Pod read and write permissions for the specified ConfigMap. If the ConfigMap is cross-namespace, use ClusterRole and ClusterRoleBinding instead of Role and RoleBinding, and ensure that these configurations are applied in all Namespaces that need to access the ConfigMap. # II. Maven Dependency Apollo's client jar package has been uploaded to the central repository, the application only needs to be introduced in the following way when it is actually used. ```xml com.ctrip.framework.apollo apollo-client 1.7.0 ``` # III. Client Usage Apollo supports API approach and Spring integration approach, how to choose which one to use? * The API approach is flexible, fully functional, configuration values are updated in real time (hot release), and supports all Java environments. * Spring approach is easy to access and has N cool ways to play with Spring, such as * Placeholder way. * Direct use in the code, such as: `@Value("${someKeyFromApollo:someDefaultValue}")` * Replace the placeholder in the configuration file, e.g.: `spring.datasource.url: ${someKeyFromApollo:someDefaultValue}` * Directly hosting spring's configuration, such as directly configuring `spring.datasource.url=jdbc:mysql://localhost:3306/somedb?characterEncoding=utf8` in apollo * Spring boot's [@ConfigurationProperties](http://docs.spring.io/spring-boot/docs/current/api/org/springframework/boot/context/properties/ConfigurationProperties.html) method * Versions from v0.10.0 onwards support automatic update of placeholder at runtime, see [PR #972](https://github.com/apolloconfig/apollo/pull/972) for details. (Versions prior to v0.10.0 do not re-inject after configuration changes and require a restart to update. If you need real-time updates of configuration values, you can refer to the subsequent description in [3.2.2 Use of Spring Placeholder](en/client/java-sdk-user-guide?id=_322-use-of-spring-placeholder) * The Spring approach can also be used in combination with the API approach, such as injecting Apollo's Config object, you can get the configuration as usual through the API approach: ```java @ApolloConfig private Config config; //inject config for namespace application ``` * For more interesting practical usage scenarios and sample code, please refer to [apollo-use-cases](https://github.com/ctripcorp/apollo-use-cases) ## 3.1 API Usage The API approach is the easiest and most efficient way to use Apollo configuration without relying on the Spring Framework to use it. ### 3.1.1 Get the configuration of the default namespace (application) ```java Config config = ConfigService.getAppConfig(); //config instance is singleton for each namespace and is never null String someKey = "someKeyFromDefaultNamespace"; String someDefaultValue = "someDefaultValueForTheKey"; String value = config.getProperty(someKey, someDefaultValue); ``` With the above **config.getProperty** you can get the real-time latest configuration value corresponding to someKey. In addition, the configuration values are fetched from memory, so there is no need for the application to do its own caching. ### 3.1.2 Listening for configuration change events Listening for configuration change events is only used when the application really cares about configuration changes and needs to be notified when the configuration changes, e.g. when the database connection string changes and the connection needs to be rebuilt, etc. If you just want to fetch the latest configuration every time, just follow the example above and call **config.getProperty**. ```java Config config = ConfigService.getAppConfig(); //config instance is singleton for each namespace and is never null config.addChangeListener(new ConfigChangeListener() { @Override public void onChange(ConfigChangeEvent changeEvent) { System.out.println("Changes for namespace " + changeEvent.getNamespace()); for (String key : changeEvent.changedKeys()) { ConfigChange change = changeEvent.getChange(key); System.out.println(String.format("Found change - key: %s, oldValue: %s, newValue: %s, changeType: %s", change.getPropertyName(), change. getOldValue(), change.getNewValue(), change.getChangeType())); } } }); ``` ### 3.1.3 Get the configuration of the public Namespace ``` java String somePublicNamespace = "CAT"; Config config = ConfigService.getConfig(somePublicNamespace); //config instance is singleton for each namespace and is never null String someKey = "someKeyFromPublicNamespace"; String someDefaultValue = "someDefaultValueForTheKey"; String value = config.getProperty(someKey, someDefaultValue); ``` ### 3.1.4 Get the configuration of a non-properties format namespace #### 3.1.4.1 Namespace in yaml/yml format Apollo-Client version 1.3.0 starts to make better support for `yaml/yml`, which is used in the same way as properties format. ```java Config config = ConfigService.getConfig("application.yml"); String someKey = "someKeyFromYmlNamespace"; String someDefaultValue = "someDefaultValueForTheKey"; String value = config.getProperty(someKey, someDefaultValue); ``` #### 3.1.4.2 Namespace in non-yaml/yml format To get it, you need to use the `ConfigService.getConfigFile` interface and specify the Format, such as `ConfigFileFormat.XML`. ```java String someNamespace = "test"; ConfigFile configFile = ConfigService.getConfigFile("test", ConfigFileFormat.XML); String content = configFile.getContent(); ``` ### 3.1.5 Read the configuration corresponding to multiple appid and their namespaces.(added in version 2.4.0) Specify the corresponding appid and namespace to retrieve the config, and then obtain the properties. ```java String someAppId = "Animal"; String somePublicNamespace = "CAT"; Config config = ConfigService.getConfig(someAppId, somePublicNamespace); String someKey = "someKeyFromPublicNamespace"; String someDefaultValue = "someDefaultValueForTheKey"; String value = config.getProperty(someKey, someDefaultValue); ``` ### 3.1.6 Retrieve Client Monitoring Metrics > For version 2.4.0 and above Apollo Client significantly enhanced observability since version 2.4.0, providing the ConfigMonitor API as well as metric export options via JMX and Prometheus. For configuration details, see [1.2.4.9 Enable Client Monitoring](#_1249-enable-client-monitoring). #### 3.1.6.1 Retrieve Monitoring Data via ConfigMonitor ```java ConfigMonitor configMonitor = ConfigService.getConfigMonitor(); // Error-related monitoring API ApolloClientExceptionMonitorApi exceptionMonitorApi = configMonitor.getExceptionMonitorApi(); List apolloConfigExceptionList = exceptionMonitorApi.getApolloConfigExceptionList(); // Namespace-related monitoring API ApolloClientNamespaceMonitorApi namespaceMonitorApi = configMonitor.getNamespaceMonitorApi(); List namespace404 = namespaceMonitorApi.getNotFoundNamespaces(); // Bootstrap parameter-related monitoring API ApolloClientBootstrapArgsMonitorApi runningParamsMonitorApi = configMonitor.getBootstrapArgsMonitorApi(); String bootstrapNamespaces = runningParamsMonitorApi.getBootstrapNamespaces(); // Thread pool-related monitoring API ApolloClientThreadPoolMonitorApi threadPoolMonitorApi = configMonitor.getThreadPoolMonitorApi(); ApolloThreadPoolInfo remoteConfigRepositoryThreadPoolInfo = threadPoolMonitorApi.getRemoteConfigRepositoryThreadPoolInfo(); ``` #### 3.1.6.2 Expose Status Information via JMX Enable the relevant configuration: ```properties apollo.client.monitor.enabled = true apollo.client.monitor.jmx.enabled = true ``` After starting the application, use J-console or similar tools to view the metrics. Below is an example using J-console: ![showing Apollo client monitoring metrics in JMX](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/apollo-client-monitor-jmx.jpg) #### 3.1.6.3 Client Export Metrics to External Monitoring Systems Users can customize the integration with monitoring systems such as Prometheus as needed. The client provides an SPI, see [7.3 Exporting Metrics to Custom Monitoring Systems](#_73-exporting-metrics-to-custom-monitoring-systems). for details. *Related Metrics Data Tables* **Namespace Metrics** Metric corresponding API: ApolloClientNamespaceMonitorApi | Metric Name | Tag | Corresponding Monitor-API | | ---------------------------------------------------- | ---------- | --------------------------------------------------- | | apollo_client_namespace_usage_total | namespace | namespaceMetrics.getUsageCount() | | apollo_client_namespace_item_num | namespace | namespaceMetrics.getFirstLoadTimeSpendInMs() | | apollo_client_namespace_not_found | | namespaceMonitorApi.getNotFoundNamespaces() | | apollo_client_namespace_timeout | | namespaceMonitorApi.getTimeoutNamespaces() | | apollo_client_namespace_first_load_time_spend_in_ms | namespace | namespaceMetrics.getLatestUpdateTime | **Thread Pool Metrics** Metric corresponding API: ApolloClientThreadPoolMonitorApi | Metric Name | Tag | Corresponding Monitor-API | | ---------------------------------------------------- | --------------- | --------------------------------------------------- | | apollo_client_thread_pool_pool_size | thread_pool_name | threadPoolInfo.getPoolSize() | | apollo_client_thread_pool_maximum_pool_size | thread_pool_name | threadPoolInfo.getMaximumPoolSize() | | apollo_client_thread_pool_largest_pool_size | thread_pool_name | threadPoolInfo.getLargestPoolSize() | | apollo_client_thread_pool_completed_task_count | thread_pool_name | threadPoolInfo.getCompletedTaskCount() | | apollo_client_thread_pool_queue_remaining_capacity | thread_pool_name | threadPoolInfo.getQueueRemainingCapacity() | | apollo_client_thread_pool_total_task_count | thread_pool_name | threadPoolInfo.getTotalTaskCount() | | apollo_client_thread_pool_active_task_count | thread_pool_name | threadPoolInfo.getActiveTaskCount() | | apollo_client_thread_pool_core_pool_size | thread_pool_name | threadPoolInfo.getCorePoolSize() | | apollo_client_thread_pool_queue_size | thread_pool_name | threadPoolInfo.getQueueSize() | **Exception Metrics** Metric corresponding API: ApolloClientExceptionMonitorApi | Metric Name | Tag | | ----------------------------------- | ----------------------------------------------------- | | apollo_client_exception_num_total | exceptionMonitorApi.getExceptionCountFromStartup() | ## 3.2 Spring integration approach ### 3.2.1 Configuration Apollo also supports integration with Spring (Spring 3.1.1+), and only requires some simple configuration. Apollo currently supports both the more traditional `XML-based` configuration and the currently more popular `Java-based (recommended)` configuration. In case of Spring Boot environments, it is recommended to refer to [3.2.1.3 Spring Boot integration methods (recommended)](en/client/java-sdk-user-guide?id=_3213-spring-boot-integration-methods-recommended) for configuration. Note that if you have previously used `org.springframework.beans.factory.config.PropertyPlaceholderConfigurer`, please replace it with `org.springframework.context.support.PropertySourcesPlaceholderConfigurer`. It is not recommended to use PropertyPlaceholderConfigurer after Spring 3.1, use PropertySourcesPlaceholderConfigurer instead. If you have used `` before, please note that the `spring-context.xsd` version introduced in the xml needs to be 3.1 or higher (usually it will be upgraded automatically as long as no version is specified), and it is recommended to introduce it without the version number, e.g.: `http://www.springframework.org/schema/context/spring-context.xsd` > Note 1: namespace in yaml/yml format supports integration with Spring since version 1.3.0, when injecting you need to fill in the full name with a suffix, such as application.yml > Note 2: Non-properties, non-yaml/yml formatted namespace (e.g. xml, json, etc.) do not support integration with Spring at this time. #### 3.2.1.1 XML-based configuration >Note: You need to add apollo related xml namespace to the configuration file header, otherwise it will report xml syntax errors. 1. Inject the default namespace configuration into Spring ```xml ``` 2. Inject multiple namespace configuration into Spring ```xml ``` 3. Inject multiple namespaces and specify the order If multiple property sources have the same key, then the configuration with the highest order will take effect. If does not specify an order, then the default is the lowest priority. ```xml ``` #### 3.2.1.2 Java-based configuration (recommended) Java-based configuration is currently the more popular approach as opposed to XML-based configuration. Note that `@EnableApolloConfig` should be used together with `@Configuration`, otherwise it will not take effect. 1. Inject the default namespace configuration into Spring ```java // This is the simplest form of configuration and is generally used by applications to instruct Apollo to inject the configuration of application namespace into the Spring environment @Configuration @EnableApolloConfig public class AppConfig { @Bean public TestJavaConfigBean javaConfigBean() { return new TestJavaConfigBean(); } } ``` 2. Inject multiple namespace configuration into Spring ```java @Configuration @EnableApolloConfig public class SomeAppConfig { @Bean public TestJavaConfigBean javaConfigBean() { return new TestJavaConfigBean(); } } // This is a slightly more complex form of configuration, instructing Apollo to inject the configuration of FX.apollo and application.yml namespace into the Spring environment @Configuration @EnableApolloConfig({"FX.apollo", "application.yml"}) public class AnotherAppConfig {} ``` 3. Inject multiple namespaces and specify the order ```java // This is the most complex form of configuration, instructing Apollo to inject the configuration of FX.apollo and application.yml namespace into the Spring environment, and in the order before application @Configuration @EnableApolloConfig(order = 2) public class SomeAppConfig { @Bean public TestJavaConfigBean javaConfigBean() { return new TestJavaConfigBean(); } } @Configuration @EnableApolloConfig(value = {"FX.apollo", "application.yml"}, order = 1) public class AnotherAppConfig {} ``` 4.Support for multiple appid (added in version 2.4.0) ```java // Added support for loading multiple appid their corresponding namespaces. // Note that when using multiple appid, if there are keys that are the same, // only the key from the prioritized loaded appid will be retrieved @Configuration @EnableApolloConfig(value = {"FX.apollo", "application.yml"}, multipleConfigs = {@MultipleConfig(appid = "ORDER_SERVICE", namespaces = {"ORDER.apollo"})} ) public class SomeAppConfig {} ``` #### 3.2.1.3 Spring Boot integration methods (recommended) Spring Boot supports the above two integration methods in addition to configuration via application.properties/bootstrap.properties, which enables configuration to be injected at an earlier stage, such as scenarios that use `@ConditionalOnProperty` or have some spring-boot-starter needs to read the configuration to do something in the startup phase (e.g. [dubbo-spring-boot-project](https://github.com/apache/incubator-dubbo-spring-boot-project)). So for Spring Boot environment it is recommended to access Apollo (requires version 0.10.0 and above) by the following way. It is very simple to use, you just need to configure it in application.properties/bootstrap.properties according to the following sample. 1. Example configuration with default ``application`` namespace injected ```properties # will inject 'application' namespace in bootstrap phase apollo.bootstrap.enabled = true ``` 2. Example configuration for injecting non-default ``application`` namespace or multiple namespaces ```properties apollo.bootstrap.enabled = true # will inject 'application', 'FX.apollo' and 'application.yml' namespaces in bootstrap phase apollo.bootstrap.namespaces = application,FX.apollo,application.yml ``` 3. Load Apollo configuration before initializing the logging system (1.2.0+) Starting with version 1.2.0, if you wish to put logging-related configuration (such as `logging.level.root=info` or parameters in `logback-spring.xml`) in Apollo management as well, then you can additionally configure `apollo.bootstrap.eagerLoad. enabled=true` to put Apollo loading order before logging system loading, for more information you can refer to [PR 1614](https://github.com/apolloconfig/apollo/pull/1614). The reference configuration example is as follows. ```properties # will inject 'application' namespace in bootstrap phase apollo.bootstrap.enabled = true # put apollo initialization before logging system initialization apollo.bootstrap.eagerLoad.enabled = true ``` #### 3.2.1.4 Spring Boot Config Data Loader (Spring Boot 2.4+, Apollo Client 1.9.0+ recommended) For Spring Boot 2.4+ versions there is also support for loading configurations via Config Data Loader mode ##### 3.2.1.4.1 Adding maven dependencies `apollo-client-config-data` already depends on `apollo-client`, so you only need to add this one dependency, not the apollo-client dependency ```xml com.ctrip.framework.apollo apollo-client-config-data 1.9.0 ``` ##### 3.2.1.4.2 Configure `app.id`, `env`, `apollo.meta` (or `apollo.config-service`), `apollo.cluster` as described above ##### 3.2.1.4.3 Configure `application.properties` or `application.yml` Use the default namespace `application` ```properties # old way # apollo.bootstrap.enabled=true # do not configure apollo.bootstrap.namespaces # new way spring.config.import=apollo:// ``` or ```properties # old way # apollo.bootstrap.enabled=true # apollo.bootstrap.namespaces=application # new way spring.config.import=apollo://application ``` Use custom namespace ```properties # old way # apollo.bootstrap.enabled=true # apollo.bootstrap.namespaces=your-namespace # new way spring.config.import=apollo://your-namespace ``` Using multiple namespaces Note: `spring.config.import` loads the configuration from back to front, while `apollo.bootstrap.namespaces` loads it from back to front, just the opposite. To ensure consistency with the original logic, reverse the order of namespaces ```properties # old way # apollo.bootstrap.enabled=true # apollo.bootstrap.namespaces=namespace1,namespace2,namespace3 # new way spring.config.import=apollo://namespace3, apollo://namespace2, apollo://namespace1 ``` #### 3.2.1.5 Spring Boot Config Data Loader (Spring Boot 2.4+, Apollo Client 1.9.0+ recommended) + webClient extension For Spring Boot version 2.4 and above, it also supports loading configuration through Config Data Loader mode Apollo's Config Data Loader also provides a webClient-based http client to replace the original http client, so as to easily extend the http client ##### 3.2.1.5.1 Add maven dependency WebClient can be based on multiple implementations (reactor netty httpclient, jetty reactive httpclient, apache httpclient5), the dependencies to be added are as follows ###### Reactor netty httpclient ```xml com.ctrip.framework.apollo apollo-client-config-data 1.9.0 org.springframework spring-webflux io.projectreactor.netty reactor-netty-http ``` ###### Jetty reactive httpclient ```xml com.ctrip.framework.apollo apollo-client-config-data 1.9.0 org.springframework spring-webflux org.eclipse.jetty jetty-reactive-httpclient ``` ###### Apache httpclient5 Spring boot does not specify the version of apache httpclient5, so you need to manually specify the version here ```xml com.ctrip.framework.apollo apollo-client-config-data 1.9.0 org.springframework spring-webflux org.apache.httpcomponents.client5 httpclient5 5.1 org.apache.httpcomponents.core5 httpcore5-reactive 5.1 ``` ##### 3.2.1.5.2 Configure `app.id`, `env`, `apollo.meta` (or `apollo.config-service`), `apollo.cluster` as described above ##### 3.2.1.5.3 Configuring `application.properties` or `application.yml` The default namespace is used here as an example. Please refer to 3.2.1.4.3 for the configuration of namespace. ```properties spring.config.import=apollo://application apollo.client.extension.enabled=true ``` ##### 3.2.1.5.4 Provide implementation of spi Provides a spi implementation of interface `com.ctrip.framework.apollo.config.data.extension.webclient.customizer.spi.ApolloClientWebClientCustomizerFactory` After configuring `apollo.client.extension.enabled=true`, Apollo's Config Data Loader will try to load the spi's implementation class to customize the webClient ### 3.2.2 Use of Spring Placeholder Spring applications usually use Placeholder to inject configuration in the form of ${someKey:someDefaultValue}, such as ${timeout:100}. The key before the colon is the key, and the default value after the colon. It is recommended to give the default value as much as possible in actual use, so as to avoid runtime errors due to the undefined key. Versions starting from v0.10.0 support automatic update of placeholders at runtime, see [PR #972](https://github.com/apolloconfig/apollo/pull/972). If you need to turn off the automatic update function of placeholder at runtime, you can turn it off in the following two ways: 1. By setting the System Property `apollo.autoUpdateInjectedSpringProperties`, such as passing in `-Dapollo.autoUpdateInjectedSpringProperties=false` at startup 2. By setting the `apollo.autoUpdateInjectedSpringProperties` property in META-INF/app.properties, such as ```properties app.id=SampleApp apollo.autoUpdateInjectedSpringProperties=false ``` #### 3.2.2.1 XML usage Suppose I have a TestXmlBean with two configuration items that need to be injected: ```java public class TestXmlBean { private int timeout; private int batch; public void setTimeout(int timeout) { this.timeout = timeout; } public void setBatch(int batch) { this.batch = batch; } public int getTimeout() { return timeout; } public int getBatch() { return batch; } } ``` Then, I will use the following way to define in XML (assuming that the default application namespace of the application has timeout and batch configuration items): ```xml ``` #### 3.2.2.2 How to use Java Config Suppose I have a TestJavaConfigBean, which can also be injected using @Value through Java Config: ```java public class TestJavaConfigBean { @Value("${timeout:100}") private int timeout; private int batch; @Value("${batch:200}") public void setBatch(int batch) { this.batch = batch; } public int getTimeout() { return timeout; } public int getBatch() { return batch; } } ``` In the Configuration class, use it in the following way (assuming the default application namespace of the application has `timeout` and `batch` configuration items): ```java @Configuration @EnableApolloConfig public class AppConfig { @Bean public TestJavaConfigBean javaConfigBean() { return new TestJavaConfigBean(); } } ``` #### 3.2.2.3 How to use ConfigurationProperties Spring Boot provides [@ConfigurationProperties](http://docs.spring.io/spring-boot/docs/current/api/org/springframework/boot/context/properties/ConfigurationProperties.html) to inject configuration into bean objects . Apollo also supports this method. The following example will inject `redis.cache.expireSeconds` and `redis.cache.commandTimeout` into the `expireSeconds` and `commandTimeout` fields of SampleRedisConfig respectively. ```java @ConfigurationProperties(prefix = "redis.cache") public class SampleRedisConfig { private int expireSeconds; private int commandTimeout; public void setExpireSeconds(int expireSeconds) { this.expireSeconds = expireSeconds; } public void setCommandTimeout(int commandTimeout) { this.commandTimeout = commandTimeout; } } ``` In the Configuration class, use it in the following way (assuming the default application namespace of the application has `redis.cache.expireSeconds` and `redis.cache.commandTimeout` configuration items): ```java @Configuration @EnableApolloConfig public class AppConfig { @Bean public SampleRedisConfig sampleRedisConfig() { return new SampleRedisConfig(); } } ``` It should be noted that if `@ConfigurationProperties` needs to automatically update the injected value when the Apollo configuration changes, you need to use [EnvironmentChangeEvent](https://cloud.spring.io/spring-cloud-static/spring-cloud.html#_environment_changes) or [RefreshScope](https://cloud.spring.io/spring-cloud-static/spring-cloud.html#_refresh_scope). For related code implementation, please refer to [ZuulPropertiesRefresher.java](https://github.com/ctripcorp/apollo-use-cases/blob/master/spring-cloud-zuul/src/main/java/com/ctrip/framework/apollo/use/cases/spring/cloud/zuul/ZuulPropertiesRefresher.java#L48) and [SampleRedisConfig.java](https://github.com/apolloconfig/apollo-demo-java/blob/main/spring-boot-demo/src/main/java/com/apolloconfig/apollo/demo/springboot/config/SampleRedisConfig.java) and [SpringBootApolloRefreshConfig.java](https://github.com/apolloconfig/apollo-demo-java/blob/main/spring-boot-demo/src/main/java/com/apolloconfig/apollo/demo/springboot/refresh/SpringBootApolloRefreshConfig.java) ### 3.2.3 Spring Annotation Support Apollo also adds several new Annotations to simplify usage in the Spring environment. 1. @ApolloConfig * Used to automatically inject the Config object 2. @ApolloConfigChangeListener * Used to automatically register ConfigChangeListener 3. @ApolloJsonValue * Used to automatically inject the configured json string as an object Example of usage is as follows: ```java public class TestApolloAnnotationBean { @ApolloConfig private Config config; //inject config for namespace application @ApolloConfig("application") private Config anotherConfig; //inject config for namespace application @ApolloConfig("FX.apollo") private Config yetAnotherConfig; //inject config for namespace FX.apollo @ApolloConfig("application.yml") private Config ymlConfig; //inject config for namespace application.yml /** * ApolloJsonValue annotated on fields example, the default value is specified as empty list - [] *
    * jsonBeanProperty=[{"someString":"hello","someInt":100},{"someString":"world!","someInt":200}] */ @ApolloJsonValue("${jsonBeanProperty:[]}") private List anotherJsonBeans; @Value("${batch:100}") private int batch; //config change listener for namespace application @ApolloConfigChangeListener private void someOnChange(ConfigChangeEvent changeEvent) { //update injected value of batch if it is changed in Apollo if (changeEvent.isChanged("batch")) { batch = config.getIntProperty("batch", 100); } } //config change listener for namespace application @ApolloConfigChangeListener("application") private void anotherOnChange(ConfigChangeEvent changeEvent) { //do something } //config change listener for namespaces application, FX.apollo and application.yml @ApolloConfigChangeListener({"application", "FX.apollo", "application.yml"}) private void yetAnotherOnChange(ConfigChangeEvent changeEvent) { //do something } //example of getting config from Apollo directly //this will always return the latest value of timeout public int getTimeout() { return config.getIntProperty("timeout", 200); } //example of getting config from injected value //the program needs to update the injected value when batch is changed in Apollo using @ApolloConfigChangeListener shown above public int getBatch() { return this.batch; } private static class JsonBean{ private String someString; private int someInt; } } ``` Use it in the Configuration class as follows: ```java @Configuration @EnableApolloConfig public class AppConfig { @Bean public TestApolloAnnotationBean testApolloAnnotationBean() { return new TestApolloAnnotationBean(); } } ``` ### 3.2.4 Existing configuration migration In many cases, the application may already have a lot of configuration, such as Spring Boot application, there will be configurations such as bootstrap.properties/yml, application.properties/yml, etc. After the application is connected to Apollo, these configurations can be easily migrated to Apollo. The specific steps are as follows: 1. Create a new project for the application in Apollo 2. Configure `META-INF/app.properties` in the application 3. It is recommended to convert the original configuration to the properties format, and then paste it into the application namespace of the application through the text editing mode provided by Apollo, and publish the configuration * If the original format is yml, you can use [YamlPropertiesFactoryBean.getObject](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/beans/factory/config/YamlPropertiesFactoryBean.html#getObject--) into properties format 4. If the original is yml and you want to continue to use yml to edit the configuration, you can create a private application.yml namespace, paste all the original configuration into it, and publish the configuration * Requires `apollo-client` to be version `1.3.0` and above 5. Delete the original configuration files such as bootstrap.properties/yml, application.properties/yml from the project * If you need to keep the local configuration file, it should be noted that some configurations such as `server.port` must ensure that the configuration item has been deleted from the local file Such as: ```properties spring.application.name=reservation-service server.port = 8080 logging.level = ERROR eureka.client.service-url.defaultZone = http://127.0.0.1:8761/eureka/ eureka.client.healthcheck.enabled=true eureka.client.register-with-eureka = true eureka.client.fetch-registry = true eureka.client.eureka-service-url-poll-interval-seconds = 60 eureka.instance.prefer-ip-address = true ``` ![text-mode-spring-boot-config-sample](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/text-mode-spring-boot-config-sample.png ) ## 3.3 Demo There is a sample client project in the project: `apollo-demo`, for details, please refer to [2.3 Java Sample Client Start](en/contribution/apollo-development-guide?id=_23-java-sample-client-startup) in [Apollo Development Guide](en/contribution/apollo-development-guide) section. For more use case demos, please refer to [Apollo usage scenarios and sample code](https://github.com/ctripcorp/apollo-use-cases). # IV. Client design ![client-architecture](https://github.com/apolloconfig/apollo/raw/master/doc/images/client-architecture.png) The above diagram briefly describes the principle of Apollo client implementation. 1. The client and the server maintain a long connection so that it can get the first push of configuration updates. (achieved through Http Long Polling) . 2. The client also regularly pulls the latest configuration of the application from the Apollo Configuration Center server. * This is a fallback mechanism to prevent the configuration from being updated due to the failure of the push mechanism. * The client will report the local version of the timed pull, so in general, for the timed pull operation, the server will return 304 - Not Modified. * Timing frequency defaults to pulling every 5 minutes. Clients can also override this by specifying System Property: `apollo.refreshInterval` at runtime, in minutes. 3. After the client gets the latest configuration of the application from the Apollo Configuration Center server, it will be saved in memory. 4. The client will cache a copy of the configuration obtained from the server in the local file system. In case of service unavailability or network failure, the configuration can still be restored locally. 5. The application can get the latest configuration from the Apollo client, subscribe to configuration update notifications. # V. Local Development Mode Apollo client also supports local development mode, which is mainly used when the development environment cannot connect to Apollo server, such as doing related function development on cruise ships or airplanes. In local development mode, Apollo will only read configuration information from local files, not from Apollo server. You can enable Apollo local development mode by following the steps below. ## 5.1 Modifying the environment Modify the `/opt/settings/server.properties` (Mac/Linux) or `C:\opt\settings\server.properties` (Windows) file to set the env to Local: ```properties env=Local ``` For more ways to configure the environment, please refer to [1.2.4.1 Environment](en/client/java-sdk-user-guide?id=_1241-environment) ## 5.2 Preparing local configuration files In local development mode, Apollo client will read the files from local, so we need to prepare the configuration file beforehand. ### 5.2.1 Local configuration directory The local configuration directory is located at. * **Mac/Linux**: /opt/data/{_appId_}/config-cache * **Windows**: C:\opt\data\\\{_appId_}\config-cache The appId is the appId of the application, e.g. 100004458. Please make sure the directory exists and the application has read access to it. **[Tip]** The recommended way is to use Apollo in normal mode first, so that Apollo will automatically create the directory and generate the configuration file under it. ### 5.2.2 Local configuration files Local configuration files need to be placed in the local configuration directory according to a certain file name format, which is as follows. **_{appId}+{cluster}+{namespace}.properties_** * AppId is the application's own appId, such as 100004458 * Cluster is the cluster used by the application, generally in local mode without configuration, it is default * Namespace is the configuration `namespace` used by the `application`, usually `application` ![client-local-cache](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/client-local-cache.png) The content of the file is stored in properties format, for example, if there are two keys, one is request.timeout and the other is batch, then the content of the file is in the following format. ```properties request.timeout=2000 batch=2000 ``` ## 5.3 Modifying the configuration In local development mode, Apollo does not monitor the file content for changes in real time, so if you modify the configuration, you need to restart the application to take effect. # VI. Test mode The `apollo-mockserver` has been added since version 1.1.0, so that it can well support scenarios where mock configuration is required for unit testing, using the following methods. ## 6.1 Introducing pom dependencies ```xml com.ctrip.framework.apollo apollo-mockserver 1.7.0 ``` ## 6.2 Placing mock data under test's resources The file name convention is `mockdata-{namespace}.properties` ![image](https://user-images.githubusercontent.com/17842829/44515526-5e0e6480-a6f5-11e8-9c3c-4ff2ec737c8d.png) ## 6.3 Writing test classes For more usage demos, see [ApolloMockServerApiTest.java](https://github.com/apolloconfig/apollo/blob/master/apollo-mockserver/src/test/java/com/ctrip/framework/apollo/mockserver/ApolloMockServerApiTest.java) and [ApolloMockServerSpringIntegrationTest.java](https://github.com/apolloconfig/apollo/blob/master/apollo-mockserver/src/test/java/com/ctrip/framework/apollo/mockserver/ApolloMockServerSpringIntegrationTest.java). ```java @RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(classes = TestConfiguration.class) public class SpringIntegrationTest { // startup apollo's mockserver @ClassRule public static EmbeddedApollo embeddedApollo = new EmbeddedApollo(); @Test @DirtiesContext // This annotation is necessary because configuration injection can mess up the application context public void testPropertyInject(){ assertEquals("value1", testBean.key1); assertEquals("value2", testBean.key2); } @Test @DirtiesContext public void testListenerTriggeredByAdd() throws InterruptedException, ExecutionException, TimeoutException { String otherNamespace = "othernamespace"; embeddedApollo.addOrModifyPropery(otherNamespace,"someKey","someValue"); ConfigChangeEvent changeEvent = testBean.futureData.get(5000, TimeUnit.MILLISECONDS); assertEquals(otherNamespace, changeEvent.getNamespace()); assertEquals("someValue", changeEvent.getChange("someKey").getNewValue()); } @EnableApolloConfig("application") @Configuration static class TestConfiguration{ @Bean public TestBean testBean(){ return new TestBean(); } } static class TestBean{ @Value("${key1:default}") String key1; @Value("${key2:default}") String key2; SettableFuture futureData = SettableFuture.create(); @ApolloConfigChangeListener("othernamespace") private void onChange(ConfigChangeEvent changeEvent) { futureData.set(changeEvent); } } } ``` # Ⅶ. apollo-client customization ## 7.1 ConfigService load balancing algorithm > from version 2.1.0 To satisfy users' different demands on ConfigService load balancing algorithm when using apollo-client, we provide **spi** since version 2.1.0 The interface is `com.ctrip.framework.apollo.spi.ConfigServiceLoadBalancerClient`. The Input is multiple ConfigServices returned by meta server, and the output is a ConfigService selected. The default service provider is `com.ctrip.framework.apollo.spi.RandomConfigServiceLoadBalancerClient`, which chooses one ConfigService from multiple ConfigServices using random strategy . ## 7.2 Exporting Metrics to Prometheus > For 2.4.0 and above Metrics can be exported to Prometheus, or different implementations can be written based on SPI to integrate with various monitoring systems. Import the client plugin ```xml com.ctrip.framework.apollo apollo-plugin-client-prometheus 2.4.0 ``` Adjust the configuration ```properties apollo.client.monitor.enabled=true apollo.client.monitor.external.type=prometheus ``` You can obtain the ExporterData via ConfigMonitor (the format depends on the monitoring system you configure, here it supports Prometheus format). Since Prometheus retrieves metrics via pulling, users need to expose the endpoint by themselves and implement a controller like the one below. Example code ```java @RestController @ResponseBody public class TestController { @GetMapping("/metrics") public String metrics() { ConfigMonitor configMonitor = ConfigService.getConfigMonitor(); return configMonitor.getExporterData(); } } ``` After starting the application, let Prometheus listen to the endpoint, and by printing the request logs, you will see information in a format similar to the following. ``` # TYPE apollo_client_thread_pool_active_task_count gauge # HELP apollo_client_thread_pool_active_task_count apollo gauge metrics apollo_client_thread_pool_active_task_count{thread_pool_name="RemoteConfigRepository"} 0.0 apollo_client_thread_pool_active_task_count{thread_pool_name="AbstractApolloClientMetricsExporter"} 1.0 apollo_client_thread_pool_active_task_count{thread_pool_name="AbstractConfigFile"} 0.0 apollo_client_thread_pool_active_task_count{thread_pool_name="AbstractConfig"} 0.0 # TYPE apollo_client_namespace_timeout gauge # HELP apollo_client_namespace_timeout apollo gauge metrics apollo_client_namespace_timeout 0.0 # TYPE apollo_client_thread_pool_pool_size gauge # HELP apollo_client_thread_pool_pool_size apollo gauge metrics apollo_client_thread_pool_pool_size{thread_pool_name="RemoteConfigRepository"} 1.0 apollo_client_thread_pool_pool_size{thread_pool_name="AbstractApolloClientMetricsExporter"} 1.0 apollo_client_thread_pool_pool_size{thread_pool_name="AbstractConfigFile"} 0.0 apollo_client_thread_pool_pool_size{thread_pool_name="AbstractConfig"} 0.0 # TYPE apollo_client_thread_pool_queue_remaining_capacity gauge # HELP apollo_client_thread_pool_queue_remaining_capacity apollo gauge metrics apollo_client_thread_pool_queue_remaining_capacity{thread_pool_name="RemoteConfigRepository"} 2.147483647E9 apollo_client_thread_pool_queue_remaining_capacity{thread_pool_name="AbstractApolloClientMetricsExporter"} 2.147483647E9 apollo_client_thread_pool_queue_remaining_capacity{thread_pool_name="AbstractConfigFile"} 0.0 apollo_client_thread_pool_queue_remaining_capacity{thread_pool_name="AbstractConfig"} 0.0 # TYPE apollo_client_exception_num counter # HELP apollo_client_exception_num apollo counter metrics apollo_client_exception_num_total 1404.0 apollo_client_exception_num_created 1.729435502796E9 # TYPE apollo_client_thread_pool_largest_pool_size gauge # HELP apollo_client_thread_pool_largest_pool_size apollo gauge metrics apollo_client_thread_pool_largest_pool_size{thread_pool_name="RemoteConfigRepository"} 1.0 apollo_client_thread_pool_largest_pool_size{thread_pool_name="AbstractApolloClientMetricsExporter"} 1.0 apollo_client_thread_pool_largest_pool_size{thread_pool_name="AbstractConfigFile"} 0.0 apollo_client_thread_pool_largest_pool_size{thread_pool_name="AbstractConfig"} 0.0 # TYPE apollo_client_thread_pool_queue_size gauge # HELP apollo_client_thread_pool_queue_size apollo gauge metrics apollo_client_thread_pool_queue_size{thread_pool_name="RemoteConfigRepository"} 352.0 apollo_client_thread_pool_queue_size{thread_pool_name="AbstractApolloClientMetricsExporter"} 0.0 apollo_client_thread_pool_queue_size{thread_pool_name="AbstractConfigFile"} 0.0 apollo_client_thread_pool_queue_size{thread_pool_name="AbstractConfig"} 0.0 # TYPE apollo_client_namespace_usage counter # HELP apollo_client_namespace_usage apollo counter metrics apollo_client_namespace_usage_total{namespace="application"} 11.0 apollo_client_namespace_usage_created{namespace="application"} 1.729435502791E9 # TYPE apollo_client_thread_pool_core_pool_size gauge # HELP apollo_client_thread_pool_core_pool_size apollo gauge metrics apollo_client_thread_pool_core_pool_size{thread_pool_name="RemoteConfigRepository"} 1.0 apollo_client_thread_pool_core_pool_size{thread_pool_name="AbstractApolloClientMetricsExporter"} 1.0 apollo_client_thread_pool_core_pool_size{thread_pool_name="AbstractConfigFile"} 0.0 apollo_client_thread_pool_core_pool_size{thread_pool_name="AbstractConfig"} 0.0 # TYPE apollo_client_namespace_not_found gauge # HELP apollo_client_namespace_not_found apollo gauge metrics apollo_client_namespace_not_found 351.0 # TYPE apollo_client_thread_pool_total_task_count gauge # HELP apollo_client_thread_pool_total_task_count apollo gauge metrics apollo_client_thread_pool_total_task_count{thread_pool_name="RemoteConfigRepository"} 353.0 apollo_client_thread_pool_total_task_count{thread_pool_name="AbstractApolloClientMetricsExporter"} 4.0 apollo_client_thread_pool_total_task_count{thread_pool_name="AbstractConfigFile"} 0.0 apollo_client_thread_pool_total_task_count{thread_pool_name="AbstractConfig"} 0.0 # TYPE apollo_client_namespace_first_load_time_spend_in_ms gauge # HELP apollo_client_namespace_first_load_time_spend_in_ms apollo gauge metrics apollo_client_namespace_first_load_time_spend_in_ms{namespace="application"} 108.0 # TYPE apollo_client_thread_pool_maximum_pool_size gauge # HELP apollo_client_thread_pool_maximum_pool_size apollo gauge metrics apollo_client_thread_pool_maximum_pool_size{thread_pool_name="RemoteConfigRepository"} 2.147483647E9 apollo_client_thread_pool_maximum_pool_size{thread_pool_name="AbstractApolloClientMetricsExporter"} 2.147483647E9 apollo_client_thread_pool_maximum_pool_size{thread_pool_name="AbstractConfigFile"} 2.147483647E9 apollo_client_thread_pool_maximum_pool_size{thread_pool_name="AbstractConfig"} 2.147483647E9 # TYPE apollo_client_namespace_item_num gauge # HELP apollo_client_namespace_item_num apollo gauge metrics apollo_client_namespace_item_num{namespace="application"} 9.0 # TYPE apollo_client_thread_pool_completed_task_count gauge # HELP apollo_client_thread_pool_completed_task_count apollo gauge metrics apollo_client_thread_pool_completed_task_count{thread_pool_name="RemoteConfigRepository"} 1.0 apollo_client_thread_pool_completed_task_count{thread_pool_name="AbstractApolloClientMetricsExporter"} 3.0 apollo_client_thread_pool_completed_task_count{thread_pool_name="AbstractConfigFile"} 0.0 apollo_client_thread_pool_completed_task_count{thread_pool_name="AbstractConfig"} 0.0 # EOF ``` At the same time, you can also view the following information on the Prometheus console: ![Prometheus console showing Apollo client metrics](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/apollo-client-monitor-prometheus.png) ## 7.3 Exporting Metrics to Custom Monitoring Systems > Applicable to version 2.4.0 and above Users need to implement a MetricsExporter by extending `AbstractApolloClientMetricsExporter` and implement the following methods: - doInit (Initialization method) - isSupport (Method to check if the external-type configuration is supported) - registerOrUpdateCounterSample (Method to register or update Counter metrics) - registerOrUpdateGaugeSample (Method to register or update Gauge metrics) - response (Method to export the required metric data) Additionally, you need to configure the corresponding SPI files. MetricsExporter Loading Flowchart: ```mermaid sequenceDiagram participant Factory as DefaultMetricsExporterFactory participant Exporter as MetricsExporter participant SPI as SPI Loader %% Step 1: Factory loads all Exporters using SPI Factory->>SPI: loadAllOrdered(MetricsExporter) SPI-->>Factory: List %% Step 2: Factory checks for supported Exporter Factory->>Exporter: isSupport(externalSystemType) Exporter-->>Factory: true / false alt Exporter Found %% Step 3: Factory initializes the Exporter Factory->>Exporter: init(listeners, exportPeriod) Factory-->>Client: Exporter Instance else No Exporter Found %% Step 4: Factory returns null Factory-->>Client: null end ``` ### 7.3.1 SkyWalking Example By configuring: ```properties apollo.client.monitor.enabled=true # Defined within the exporter apollo.client.monitor.external.type=skywalking ``` Create a SkyWalkingMetricsExporter class, which extends AbstractApolloClientMetricsExporter. The basic code after inheritance is as follows: Note: This is just an example, do not use it directly in production. Implement it according to your company's specific situation. ```java public class SkyWalkingMetricsExporter extends AbstractApolloClientMetricsExporter { private static final String SKYWALKING = "skywalking"; protected SkywalkingMeterRegistry registry; // When designing, users should consider if the data structure for storing metrics consumes too much memory. protected Map counterMap; private Map gaugeMap; private Map> gaugeValues; @Override public void doInit() { registry = new SkywalkingMeterRegistry(); counterMap = new ConcurrentHashMap<>(); gaugeValues = new ConcurrentHashMap<>(); gaugeMap = new ConcurrentHashMap<>(); } @Override public boolean isSupport(String form) { return SKYWALKING.equals(form); } @Override public void registerOrUpdateCounterSample(String name, Map tags, double incrValue) { String key = name + tags.toString(); Counter counter = counterMap.get(key); if (counter == null) { counter = createCounter(name, tags); counterMap.put(key, counter); } counter.increment(incrValue); } private Counter createCounter(String name, Map tags) { return Counter.builder(name) .tags(tags.entrySet().stream() .map(entry -> Tag.of(entry.getKey(), entry.getValue())) .collect(Collectors.toList())) .register(registry); } @Override public void registerOrUpdateGaugeSample(String name, Map tags, double value) { String key = name + tags.toString(); Gauge gauge = gaugeMap.get(key); if (gauge == null) { createGauge(name, tags, value); } else { gaugeValues.get(key).set(value); } } public void createGauge(String name, Map tags, double value) { String key = name + tags.toString(); AtomicReference valueHolder = gaugeValues.computeIfAbsent(key, k -> new AtomicReference<>(value)); gaugeMap.computeIfAbsent(key, k -> Gauge.builder(name, valueHolder::get) .tags(tags.entrySet().stream() .map(entry -> Tag.of(entry.getKey(), entry.getValue())) .collect(Collectors.toList())) .register(registry)); } @Override public String response() { // No need to implement in SkyWalking push mode return "This method does not need to be implemented in SkyWalking's push mode"; } } ``` The doInit method is for users to extend during initialization. It will be called within the init method in AbstractApolloClientMetricsExporter. ```java @Override public void init(List collectors, long collectPeriod) { // code doInit(); // code } ``` Import Micrometer: ```xml org.apache.skywalking apm-toolkit-micrometer-1.10 ``` Initialize the SkywalkingMeterRegistry according to Micrometer's mechanism, and use some maps to store metric data. ```java private static final String SKYWALKING = "skywalking"; private SkywalkingMeterRegistry registry; // When designing, users should consider if the data structure for storing metrics consumes too much memory. private Map counterMap; private Map gaugeMap; private Map> gaugeValues; @Override public void doInit() { registry = new SkywalkingMeterRegistry(); counterMap = new ConcurrentHashMap<>(); gaugeValues = new ConcurrentHashMap<>(); gaugeMap = new ConcurrentHashMap<>(); } ``` The isSupport method will be called when DefaultApolloClientMetricsExporterFactory reads the MetricsExporter via SPI, to check if the correct exporter is enabled when there are multiple SPI implementations. For example, when configuring and enabling SkyWalking, and you set the apollo.client.monitor.external.type configuration value as skyWalking, the method will be implemented like this: ```java @Override public boolean isSupport(String form) { return SKYWALKING.equals(form); } ``` The registerOrUpdateCounterSample and registerOrUpdateGaugeSample methods are used to register Counter and Gauge type metrics. You just need to register and update the data as per the parameters passed in. ```java @Override public void registerOrUpdateCounterSample(String name, Map tags, double incrValue) { String key = name + tags.toString(); Counter counter = counterMap.get(key); if (counter == null) { counter = createCounter(name, tags); counterMap.put(key, counter); } counter.increment(incrValue); } private Counter createCounter(String name, Map tags) { return Counter.builder(name) .tags(tags.entrySet().stream() .map(entry -> Tag.of(entry.getKey(), entry.getValue())) .collect(Collectors.toList())) .register(registry); } @Override public void registerOrUpdateGaugeSample(String name, Map tags, double value) { String key = name + tags.toString(); Gauge gauge = gaugeMap.get(key); if (gauge == null) { createGauge(name, tags, value); } else { gaugeValues.get(key).set(value); } } public void createGauge(String name, Map tags, double value) { String key = name + tags.toString(); AtomicReference valueHolder = gaugeValues.computeIfAbsent(key, k -> new AtomicReference<>(value)); gaugeMap.computeIfAbsent(key, k -> Gauge.builder(name, valueHolder::get) .tags(tags.entrySet().stream() .map(entry -> Tag.of(entry.getKey(), entry.getValue())) .collect(Collectors.toList())) .register(registry)); } ``` The response method is for monitoring systems with pull-based metrics retrieval, like Prometheus. However, since SkyWalking uses a push mode, this method does not need to be implemented. Just configure SkyWalking for your use case. ```java @Override public String response() { // No need to implement in SkyWalking's push mode return "This method does not need to be implemented in SkyWalking's push mode"; } ``` Finally, in the project directory under resources/META-INF/services, create the corresponding SPI file to tell the framework to load this class. The file name should be com.ctrip.framework.apollo.monitor.internal.exporter.ApolloClientMetricsExporter. ```text your.package.SkyWalkingMetricsExporter ``` At this point, the client metrics data has been integrated into SkyWalking. ### 7.3.2 Prometheus Example [PrometheusApolloClientMetricsExporter.java](https://github.com/apolloconfig/apollo-java/blob/main/apollo-plugin/apollo-plugin-client-prometheus/src/main/java/com/ctrip/framework/apollo/monitor/internal/exporter/impl/PrometheusApolloClientMetricsExporter.java) ================================================ FILE: docs/en/client/k8s-configmap-user-guide.md ================================================ ### Apollo K8S ConfigMap Integration Automatically sync Apollo configurations to K8S ConfigMap. Project URL: [apollo-configmap](https://github.com/adamswanglin/apollo-configmap) ================================================ FILE: docs/en/client/nodejs-sdks-user-guide.md ================================================ ### Apollo NodeJS client 1 Project address: [node-apollo](https://github.com/Quinton/node-apollo) > Thanks [@Quinton](https://github.com/Quinton) for providing support for the NodeJS Apollo client ### Apollo NodeJS client 2 Project address: [ctrip-apollo](https://github.com/kaelzhang/ctrip-apollo) > Thanks [@kaelzhang](https://github.com/kaelzhang) for providing support for the NodeJS Apollo client ### Apollo NodeJS client 3 Project address: [node-apollo-client](https://github.com/shinux/node-apollo-client) > Thanks [@shinux](https://github.com/shinux) for providing support for the NodeJS Apollo client ### Apollo NodeJS client 4 Project address: [ctrip-apollo-client](https://github.com/lvgithub/ctrip-apollo-client) > Thanks [@lvgithub](https://github.com/lvgithub) for providing support for the NodeJS Apollo client ### Apollo NodeJS client 5 Project address: [apollo-node](https://github.com/lengyuxuan/apollo-node) > Thanks [@lengyuxuan](https://github.com/lengyuxuan) for providing support for the NodeJS Apollo client ### Apollo NodeJS client 6 Project address: [egg-apollo-client](https://github.com/xuezier/egg-apollo-client) > Thanks [@xuezier](https://github.com/xuezier) for providing support for the NodeJS Apollo client ### Apollo NodeJS client 7 Project address: [apollo-node-client](https://github.com/zhangxh1023/apollo-node-client) > Thanks [@zhangxh1023](https://github.com/zhangxh1023) for providing support for the NodeJS Apollo client ### Apollo NodeJS(WebAssembly) client 8 A client for Apollo that supports both Rust and WASM. Project address: [apollo-rust-client](https://github.com/qqiao/apollo-rust-client) > Thanks to [@qqiao](https://github.com/qqiao) for providing support for the NodeJS Apollo client ================================================ FILE: docs/en/client/other-language-client-user-guide.md ================================================ At present, Apollo team only provides Java and .Net clients due to manpower constraints. For applications in other languages, you can directly obtain the configuration through the Http interface through the introduction of this article. Also, if any team/individual is interested, they are welcome to help us to implement the client in other languages, please contact @nobodyiam and @lepdou for details. >Note: There are already clients for Go, Python, NodeJS, PHP, C++ contributed by enthusiastic users, for more information you can refer to "SDK Guide" ## 1.1 Application access to Apollo First you need to access your application in Apollo, you can refer to [application access document](en/portal/apollo-user-guide) for the specific steps. ## 1.2 Reading configuration from Apollo via Http interface with cache This interface will fetch the configuration from the cache and is suitable for more frequent configuration pull requests, such as a simple polling of the configuration every 30 seconds. Since the cache has a delay of at most one second, if you need to work with configuration push notifications to achieve real-time configuration updates, please refer to [1.3 Reading configuration from Apollo via Http interface without cache](en/client/other-language-client-user-guide?id=_13-reading-configuration-from-apollo-via-http-interface-without-cache). ### 1.2.1 Http interface description **URL**: `{config_server_url}/configfiles/json/{appId}/{clusterName}/{namespaceName}?ip={clientIp}` **Method**: GET **Parameter Description**. | Parameter Name | Required | Parameter Value | Remarks | | ----------------- | ---------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | | config_server_url | Yes | The address of the Apollo configuration service | | | appId | Yes | The appId of the application | | | clusterName | Yes | clusterName | Normally, just pass in default. If you want to configure by cluster, you can refer to [cluster-independent configuration instructions](en/portal/apollo-user-guide?id=iii-cluster-independent-configuration-instructions) to do the relevant configuration, and then fill in the corresponding cluster name here. | | namespaceName | is the name of the namespace | If no new namespace has been created, just pass in application. If you have created a Namespace and need to use the configuration of that Namespace, pass the corresponding Namespace name. **For other types of namespace, you need to pass the namespace name with a suffix, such as datasources.json** | ip | | ip | no | ip of the machine where the application will be deployed | This parameter is optional and is used to implement grayscale publishing. If you don't want to pass this parameter, please note that the URL from ? Please note that the entire query parameters starting with the ? sign should not appear in the URL. | ### 1.2.2 Http interface return format The Http interface returns JSON format, UTF-8 encoding, which contains all the configuration items in the corresponding namespace. If the namespace is of type `properties`, the return content sample is as follows: ```json { "portal.elastic.document.type": "biz", "portal.elastic.cluster.name": "hermes-es-fws" } ``` If the namespace is not of type `properties`, the return content sample is as follows: ```json { "content": "{\"portal.elastic.document.type\":\"biz\",\"portal.elastic.cluster.name\":\"hermes-es-fws\"}" } ``` > You can get the raw configuration content without escaping via `{config_server_url}/configfiles/raw/{appId}/{clusterName}/{namespaceName}?ip={clientIp}`. > The configuration in the form of properties can be obtained via `{config_server_url}/configfiles/{appId}/{clusterName}/{namespaceName}?ip={clientIp}` ### 1.2.3 Testing Since it is an Http interface, after the URL is assembled OK, it can be accessed directly through a browser, or a relevant http interface testing tool. ## 1.3 Reading configuration from Apollo via Http interface without cache This interface will get the configuration directly from the database and can be used with configuration push notifications to achieve real-time configuration updates. ### 1.3.1 Http interface description **URL**: `{config_server_url}/configs/{appId}/{clusterName}/{namespaceName}?releaseKey={releaseKey}&messages={messages}&label={label}&ip={clientIp}` **Method**: GET **Parameter Description**. | Parameter Name | Required | Parameter Value | Remarks | | ----------------- |----------|---------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | config_server_url | Yes | The address of the Apollo configuration service | | | appId | Yes | The appId of the application | | | clusterName | Yes | clusterName | Normally, just pass in default. If you want to configure by cluster, you can refer to [cluster-independent configuration instructions](en/portal/apollo-user-guide?id=iii-cluster-independent-configuration-instructions) to do the relevant configuration, and then fill in the corresponding cluster name here. | | namespaceName | Yes | is the name of the namespace | If no new namespace has been created, just pass in application. If you have created a Namespace and need to use the configuration of that Namespace, pass the corresponding Namespace name. **If the namespace is created and needs to use the configuration of the namespace, pass the namespace name. | | releaseKey | No | Previous releaseKey | The releaseKey of the last release object can be passed to the server to compare the version, if there is no change in the version, the server will return 304 to save traffic and computing. | | messages | No | Latest notificationId | To update the memory cache on the server in real-time, if the `releaseKey` parameter is passed without the `messages` parameter, there is a chance that the latest configuration will not be obtained in a multi-instance server environment with memory caching enabled. The `messages` parameter is a JSON string structure {"details":{"key":notificationId}}. The `appId`, `clusterName`, and `namespaceName` need to be concatenated using the + sign to form the key. Assuming `appId=app`, `clusterName=default`, `namespaceName=test`, and `notificationId=11`, the `messages` parameter would be {"details":{"app+default+test":11}}. When using the messages parameter, URL encoding is required.| | label | No | Labels for grayscale configuration | This parameter is optional and is used for label rule matching in grayscale publishing. | | ip | No | The ip of the machine where the application is deployed | This parameter is optional and is used for ip rule matching in grayscale publishing. | ### 1.3.2 Http interface return format This Http interface returns JSON format, UTF-8 encoding. If the configuration has not changed (the incoming releaseKey and the server-side equal), HttpStatus 304 is returned, and the response body is empty. If the configuration has changed, HttpStatus 200 is returned, and the response body is the meta information of the corresponding namespace and all configuration items in it. The return content Sample is as follows. ```json { "appId": "100004458", "cluster": "default", "namespaceName": "application", "configurations": { "portal.elastic.document.type": "biz", "portal.elastic.cluster.name": "hermes-es-fws" }, "releaseKey": "20170430092936-dee2d58e74515ff3" } ``` ### 1.3.3 Testing Since it is an Http interface, after the URL is assembled OK, it can be accessed directly through a browser, or a relevant http interface testing tool. ## 1.4 Application-aware configuration updates Apollo provides push notifications for configuration updates based on Http long polling, and third-party clients can decide whether they need to use this feature depending on their actual needs. If you are not so sensitive to the configuration update time, you can sense the configuration update by refreshing it at regular intervals, the refresh frequency can be determined by the application itself, and it is recommended to be above 30 seconds. If you need to be aware of configuration updates in real time (1 second), you can refer to the following document to implement the configuration update push feature. ### 1.4.1 Configuration update push idea Here we suggest you to refer to Apollo's Java implementation: [RemoteConfigLongPollService.java](https://github.com/apolloconfig/apollo-java/blob/main/apollo-client/src/main/java/com/ctrip/framework/apollo/internals/RemoteConfigLongPollService.java), which has more than 200 lines of code and is relatively simple in general. #### 1.4.1.1 Initialization The first thing we need to do is to determine which namespaces need to be configured for update pushing. Apollo's implementation is to register a namespace when the program gets its configuration for the first time, so we know which namespaces need to be configured for update pushing. The result of the initialization is a Map of notifications, the content is namespaceName -> notificationId (initial value of -1). If you find any new namespace that needs to be configured for update pushing during the run, you can directly stuff it into the notification Map. #### 1.4.1.2 Request Service Once you have the notifications Map, you can request services. Here we describe the logic of requesting services, please refer to the interface description later for specific URL parameters and descriptions. 1. request the remote service, bring your application information and notifications information 2. The server checks whether the notificationId is up-to-date for each namespace and corresponding notificationId passed to it. 3. if they are the latest, hold the request for 60 seconds, if there is no configuration change within 60 seconds, then return HttpStatus 304. if there is a configuration change within 60 seconds, then return the latest notificationId of the corresponding namespace, HttpStatus 200. If the notificationId is found to be older than the server, the latest notificationId of the corresponding namespace, HttpStatus 200, will be returned directly. 5. After the client gets the server side return, determine the return HttpStatus . 6. If the returned HttpStatus is 304, that the configuration has not changed, re-execute step 1. 7. If the returned HttpStatus is 200, the configuration has changed, for the change of namespace to pull configuration from the server again, see [1.3 Read configuration from Apollo through the Http interface without cache](en/client/other-language-client-user-guide?id=_13-reading-configuration-from-apollo-via-http-interface-without-cache). Also update the notificationId in the notifications map. re-run step 1. ### 1.4.2 Http interface description **URL**: `{config_server_url}/notifications/v2?appId={appId}&cluster={clusterName}¬ifications={notifications}` **Method**: GET **Parameter Description**. | Parameter Name | Required | Parameter Value | Remarks | | ----------------- | -------- | ----------------------------------------------- | ------------------------------------------------------------ | | config_server_url | Yes | The address of the Apollo configuration service | | | appId | Yes | The appId of the application | | | clusterName | Yes | clusterName | Normally, just pass in default. If you want to configure by cluster, you can refer to [cluster-independent configuration instructions](en/portal/apollo-user-guide?id=iii-cluster-independent-configuration-instructions) to do the relevant configuration, and then fill in the corresponding cluster name here. | | notifications | yes | notifications information | pass in the local notifications information, note that here need to be in the form of array to json pass in, such as: [{"namespaceName": "application", "notificationId": 100}, {"namespaceName": "FX.apollo", "notificationId": 200}]. **Note that for namespace of properties type, you only need to pass in the namespace name, such as application. for other types of namespace, you need to pass in the namespace name plus the suffix, such as datasources.json** | > Note 1: Since the server side will hold the request for 60 seconds, please make sure that the timeout for the client to access the server side is greater than 60 seconds. > Note 2: Don't forget to [url encode](https://en.wikipedia.org/wiki/Percent-encoding) for the parameters ### 1.4.3 Http interface return format The Http interface returns a JSON format, UTF-8 encoding, containing the namespace with changes and the latest notificationId. The return content Sample is as follows. ```json [ { "namespaceName": "application", "notificationId": 101 } ] ``` ### 1.4.4 Testing Since it is an Http interface, after the URL is assembled OK, it can be accessed directly through a browser, or a relevant http interface test tool. ## 1.5 Configuring access keys Apollo has added an access key mechanism since version 1.6.0, so that only authenticated clients can access sensitive configurations. If the application has access keys enabled, the client needs to add a signature when sending a request, otherwise the configuration cannot be accessed. Header information to be set. | Header | Value | Remarks | | ------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | | Authorization | Apollo ${appId}:${signature} | appId: The appId of the application, signature: Generate a signature by combining the access key with the current timestamp in milliseconds, as well as the path and query components of the visited URL. The specific implementation can be found in [Signature.signature](https://github.com/apolloconfig/apollo/blob/aa184a2e11d6e7e3f519d860d69f3cf30ccfcf9c/apollo-core/src/main/java/com/ctrip/framework/apollo/core/signature/Signature.java#L22) | | Timestamp | Number of milliseconds elapsed from `1970-1-1 00:00:00 UTC+0` to now | See [System.currentTimeMillis](https://docs.oracle.com/javase/7/docs/api/java/lang/System.html#currentTimeMillis()) | ## 1.6 Error Code Description Under normal circumstances, the Http status code returned by the interface is 200, the following lists the non-200 error code descriptions that Apollo will return. ### 1.6.1 400 - Bad Request The client needs to check whether the corresponding parameter is correct according to the prompt. ### 1.6.2 401 - Unauthorized The client is not authorized, e.g. the server has configured the access key, but the client is not configured or configured incorrectly. ### 1.6.3 404 - Not Found The resource to be accessed by the interface does not exist, usually the URL or the parameters of the URL are wrong, or the corresponding namespace has not yet been published for configuration. ### 1.6.4 405 - Method Not Allowed The interface access method is incorrect, for example, the interface should use GET to use POST access, etc. The client needs to check whether the interface access method is correct. ### 1.6.5 500 - Internal Server Error Other types of errors will return 500 by default. For this type of error, if the application cannot find the cause based on the prompt, you can try to check the server logs to troubleshoot the problem. ================================================ FILE: docs/en/client/php-sdks-user-guide.md ================================================ ### Apollo PHP client 1 Project address: [apollo-php-client](https://github.com/multilinguals/apollo-php-client) > Thanks [@t04041143](https://github.com/t04041143) for providing support for the PHP Apollo client ### Apollo PHP client 2 Project address: [apollo-sdk-config](https://github.com/fengzhibin/apollo-sdk-config) Project address: [apollo-sdk-clientd](https://github.com/fengzhibin/apollo-sdk-clientd) > Thanks [@fengzhibin](https://github.com/fengzhibin) for providing PHP Apollo client support ================================================ FILE: docs/en/client/python-sdks-user-guide.md ================================================ ### Apollo Python client 1 Project address: [pyapollo](https://github.com/filamoon/pyapollo) > Thanks [@filamoon](https://github.com/filamoon) for providing support for the Python Apollo client ### Apollo Python client 2 Project address: [BruceWW-pyapollo](https://github.com/BruceWW/pyapollo) > Thanks [@BruceWW](https://github.com/BruceWW) for providing support for the Python Apollo client ### Apollo Python client 3 Project address: [xhrg-product/apollo-client-python](https://github.com/xhrg-product/apollo-client-python) > Thanks [@xhrg-product](https://github.com/xhrg-product) for providing support for the Python Apollo client ### Apollo Python client 4 Project address: [OuterCloud/pyapollo](https://github.com/OuterCloud/pyapollo.git) > Thanks [@OuterCloud](https://github.com/OuterCloud) for providing support for the Python Apollo client ================================================ FILE: docs/en/client/rust-sdks-user-guide.md ================================================ ### Apollo Rust client Project address: [apollo-rust-sdk](https://github.com/liushv0/apollo-rust-sdk) > Thanks [@liushv0](https://github.com/liushv0) for providing support for the Rust Apollo client ### Apollo Rust client 2 A client for Apollo that supports both Rust and WASM. Project address:[apollo-rust-client](https://github.com/qqiao/apollo-rust-client) > Thanks to [@qqiao](https://github.com/qqiao) for providing support for the Rust Apollo client ### Apollo Rust client 3 Project address: [apollo-client](https://github.com/jmjoy/apollo-client) > Thanks [@jmjoy](https://github.com/jmjoy) for providing support for the Rust Apollo client ================================================ FILE: docs/en/community/team.md ================================================ # Apollo Team The Apollo team is comprised of Members and Contributors. Members have direct access to the source of Apollo project and actively evolve the code-base. Contributors improve the project through submission of patches and suggestions to the Members. The number of Contributors to the project is unbounded. All contributions to Apollo are greatly appreciated, whether for trivial cleanups, big new features or other material rewards. For more information about the community governance model, please refer [GOVERNANCE.md](https://github.com/apolloconfig/apollo/blob/master/GOVERNANCE.md). ## Members Members include Project Management Committee members and committers. The list is in alphabet order. ### Project Management Committee(PMC) | GitHub ID | Name | Organization | | ----------- | ------------- | ------------ | | Anilople | Xiaoquan Wang | Some Bank | | hezhangjian | ZhangJian He | Huawei | | JaredTan95 | Jared Tan | DaoCloud | | kezhenxu94 | Zhenxu Ke | Tetrate | | klboke | Kailing Chen | TapTap | | lepdou | Le Zhang | Tencent | | nobodyiam | Jason Song | Ant Group | | zouyx | Joe Zou | Shein | ### Committer | GitHub ID | Name | Organization | | ----------- | ------------- | ------------ | | Accelerater | Zhuohao Li | Daocloud | | hezhangjian | ZhangJian He | Huawei | | Anilople | Xiaoquan Wang | Some Bank | | JaredTan95 | Jared Tan | DaoCloud | | kezhenxu94 | Zhenxu Ke | Tetrate | | klboke | Kailing Chen | TapTap | | lepdou | Le Zhang | Tencent | | nisiyong | Stephen Ni | Qihoo 360 | | nobodyiam | Jason Song | Ant Group | | pengweiqhca | Wei Peng | Tuhu | | vdisk-group | Lvqiu Ye | Hundsun | | zouyx | Joe Zou | Shein | | spaceluke | Zhile Wei | ByteDance | ## Contributors ### Apollo main repository ### apollo.net ## Becoming a Committer Please refer [How to become a Committer](https://github.com/apolloconfig/apollo/blob/master/GOVERNANCE.md#how-to-become-a-committer). ================================================ FILE: docs/en/community/thank-you.md ================================================ # Thank you
    ctrip The Apollo project was born in Ctrip framework R&D department, and received encouragement and support from various parties during the initial development process. Thanks Ctrip for the contribution to the Apollo community.
    jetbrains intellij-idea The Apollo team uses [IntelliJ IDEA](https://www.jetbrains.com/idea) when working on the open source projects. Many thanks to [JetBrains](https://www.jetbrains.com/) for sponsoring our Open Source projects with a license.
    docsify The Apollo team uses [docsify](https://docsify.js.org/) to produce its documentation website.
    jprofiler The Apollo team uses [JProfiler](https://www.ej-technologies.com/products/jprofiler/overview.html) to locate performance problems on the open source projects. Many thanks to [EJ-Technologies](https://www.ej-technologies.com/) for sponsoring our Open Source projects with a license.
    ================================================ FILE: docs/en/contribution/apollo-development-guide.md ================================================ This document describes how to compile and run Apollo locally using the IDE so that it can help you understand the inner workings of Apollo and also prepare you for custom development. #   # I. Preparation ## 1.1 Local Runtime Environment Apollo local development requires the following components. 1. Java: 17+ 2. MySQL: 5.6.5+ (If you plan to use H2 in-memory database/H2 file database, then there is no need to prepare MySQL) 3. IDE: No special requirements MySQL is required to create Apollo database and import the base data. Please refer to the following sections in [distributed-deployment-guide](en/deployment/distributed-deployment-guide) for the specific steps. 1. [Preparation](en/deployment/distributed-deployment-guide?id=i-preparation) 2. [Create database](en/deployment/distributed-deployment-guide?id=_21-creating-the-database) ## 1.2 Apollo general design Please refer to [Apollo Configuration Center Design](en/design/apollo-design) for details. ## 1.3 OpenAPI DTO generation The `apollo-portal` module generates `OpenXxxDTO` classes (for example `OpenAppDTO`) from OpenAPI YAML definitions during the build. If you just cloned the repository or see missing DTO classes reported by the IDE, run a Maven compile phase at the repository root or inside the `apollo-portal` module to regenerate them: ```bash mvn clean compile -pl apollo-portal -am ``` Or run the command directly from the `apollo-portal` directory: ```bash mvn clean compile ``` After the command finishes, the `OpenXxxDTO` classes will appear again under `com.ctrip.framework.apollo.openapi.model`. # II. Local startup ## 2.1 Apollo Assembly When we develop locally, we usually start `apollo-assembly` in the IDE. The following is an example of how to start `apollo-assembly` locally with Intellij Community 2016.2 version. ![ApolloApplication-Overview](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/local-development/ApolloApplication-Overview.png) ### 2.1.1 Create a new running configuration ![NewConfiguration-Application](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/local-development/NewConfiguration-Application.png) ### 2.1.2 Main class configuration `com.ctrip.framework.apollo.assembly.ApolloApplication` > Note: If you want to start `apollo-portal`, `apollo-configservice` and `apollo-adminservice` independently, you can replace Main Class with > `com.ctrip.framework.apollo.portal.PortalApplication` > `com.ctrip.framework.apollo.configservice.ConfigServiceApplication` > `com.ctrip.framework.apollo.adminservice.AdminServiceApplication` ### 2.1.3 VM options configuration ![ApolloApplication-VM-Options](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/local-development/ApolloApplication-VM-Options.png) ``` -Dapollo_profile=github,auth ``` >Note 1: apollo_profile is specified here as `github` and `auth`, where `github` is a profile required by Apollo for database configuration, and `auth` is added from 0.9.0 to support simple authentication using Spring Security provided by apollo. For more information you can refer to [Portal-implement-user-login-function](en/development/portal-how-to-implement-user-login-function) > >Note 2: If you plan to use a MySQL database, you need to add `spring.config-datasource.*` related configuration, > the your-mysql-server:3306 needs to be replaced with the actual mysql server address and port, > ApolloConfigDB and ApolloPortalDB needs to be replaced with the actual database name, > apollo-username and apollo-password need to be replaced with the actual username and password ![ApolloApplication-Mysql-VM-Options](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/local-development/ApolloApplication-Mysql-VM-Options.png) ``` -Dspring.config-datasource.url=jdbc:mysql://your-mysql-server:3306/ApolloConfigDB?useUnicode=true&characterEncoding=UTF8 -Dspring.config-datasource.username=apollo-username -Dspring.config-datasource.password=apollo-password -Dspring.portal-datasource.url=jdbc:mysql://your-mysql-server:3306/ApolloPortalDB?useUnicode=true&characterEncoding=UTF8 -Dspring.portal-datasource.username=apollo-username -Dspring.portal-datasource.password=apollo-password ``` The initialization script for the MySQL database can be found in the scripts/sql/profiles/mysql-default directory of this project. [apolloconfigdb.sql](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/scripts/sql/profiles/mysql-default/apolloconfigdb.sql) [apolloportaldb.sql](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/scripts/sql/profiles/mysql-default/apolloportaldb.sql) >Note 3: The default log output of the program is /opt/logs/apollo-assembly.log, if you need to modify the log file path, you can add the `logging.file.name` parameter, as follows. > >-Dlogging.file.name=/your-path/apollo-assembly.log ### 2.1.4 Run Click Run or Debug for the new run configuration. ![ApolloApplication-Run](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/local-development/ApolloApplication-Run.png) After starting, open [http://localhost:8080](http://localhost:8080) to see that both `apollo-configservice` and `apollo-adminservice` have been started and registered to Eureka. ![ConfigAdminApplication-Eureka](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/local-development/ConfigAdminApplication-Eureka.png) > Note: In addition to confirming the service status in Eureka, you can also confirm the service health through the health check interface at. > > apollo-adminservice: [http://localhost:8090/health](http://localhost:8090/health) > apollo-configservice: [http://localhost:8080/health](http://localhost:8080/health) > > If the service is healthy, the status.code in the return content should be `UP`. > > { > "status": { > "code": "UP", > ... > }, > ... > } After starting, open [http://localhost:8070](http://localhost:8070) to see the Apollo Configuration Center interface. ![PortalApplication-Home](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/local-development/PortalApplication-Home.png) >Note: If `auth` profile is enabled, the default username is apollo and password is admin ### 2.1.5 Demo application access For better development and debugging, we usually create a demo project for our own use. You can refer to [General Application Access Guide](en/portal/apollo-user-guide?id=i-general-application-access-guide) to create your own demo project. ## 2.2 Java sample client startup There is a sample client project: [apollo-demo-java](https://github.com/apolloconfig/apollo-demo-java), the following is an example of how to start it locally with Intellij. ### 2.2.1 Configure the project AppId When creating a demo project in `2.2.5 Demo Application Access`, the system will ask to fill in a globally unique AppId, which we need to configure into the app.properties file of the `apollo-demo` project: `apollo-demo-java/api-demo/src/main/resources/ META-INF/app.properties`. ![apollo-demo-app-properties](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/local-development/apollo-demo-app-properties.jpg) If our own demo project uses an AppId of 100004458, then the file content would be app.id=100004458 >Note: AppId is the unique identity of the application, which is used by Apollo clients to get the application's own private Namespace configuration. > For public Namespace configurations, you can get the configuration without the AppId, but then you lose the ability for the application to override the public Namespace configuration. > More ways to configure AppId can be found in [1.2.1 AppId](en/client/java-sdk-user-guide?id=_121-appid) ### 2.2.2 New run configuration ![NewConfiguration-Application](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/local-development/NewConfiguration-Application.png) ### 2.2.3 Main class configuration `com.apolloconfig.apollo.demo.api.SimpleApolloConfigDemo` ### 2.2.4 VM options configuration ![apollo-demo-vm-options](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/local-development/apollo-demo-vm-options.jpg) -Dapollo.meta=http://localhost:8080 > Note: Here the current environment's meta server address is `http://localhost:8080`, which is also the address of `apollo-configservice`. > For more ways to configure Apollo Meta Server, please refer to [1.2.2 Apollo Meta Server](en/client/java-sdk-user-guide?id=_122-apollo-meta-server) ### 2.2.5 Overview ![apollo-demo-overview](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/local-development/apollo-demo-overview.jpg) ### 2.2.6 Running Click Run or Debug on the newly created run configuration. ![apollo-demo-run](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/local-development/apollo-demo-run.png) After starting, ignore the previous debug message and you will see the following message. Apollo Config Demo. Please input key to get the value. Input quit to exit. > Enter the value you have configured on the Portal, such as `timeout` in our demo project, and you will see the following message. > timeout > [SimpleApolloConfigDemo] Loading key : timeout with value: 100 > The default client log level is `DEBUG`, if you need to adjust it, you can modify the level in `apollo-demo/src/main/resources/log4j2.xml`. > > ```xml > > > ## 2.3 .Net sample client startup The [apollo.net](https://github.com/ctripcorp/apollo.net) project has a sample client project: `ApolloDemo`, here's an example of how to start it locally with VS 2010. ### 2.3.1 Configuring the project AppId When creating a Demo project in `2.2.5 Demo Application Access`, the system will ask to fill in a globally unique AppId, which we need to configure into the App.config file of the `ApolloDemo` project: `apollo.net\ApolloDemo\App.config`. ![apollo-demo-app-config](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/apollo-net-app-config.png) If our own demo project uses an AppId of 100004458, then the contents of the file would be ```xml ``` > Note: AppId is a unique identifier for the application, which Apollo clients use to get the application's own private Namespace configuration. > For public Namespace configurations, the configuration can be obtained without the AppId, but the ability of the application to override the public Namespace configuration is lost. ### 2.3.2 Configuring Service Addresses Apollo client will get the configuration from different servers for different environments, so we need to configure the server address (Apollo.{ENV}.Meta) in app.config or web.config. Suppose the DEV environment's configuration service (apollo-config service) address is 11.22.33.44, then we will do the following configuration. ![apollo-net-server-url-config](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/apollo-net-server-url-config.png) ### 2.3.3 Running Just run `ApolloConfigDemo.cs`. After starting, ignore the previous debugging message and you will see the following prompt. Apollo Config Demo. Please input key to get the value. Input quit to exit. > Enter the value you configured on the Portal, such as `timeout` in our demo project, and you will see the following message. > timeout > Loading key: timeout with value: 100 >Net client will output logs directly to the Console by default, so you can implement your own logging-related features. >You can implement your own logging-related functions. >See [https://github.com/ctripcorp/apollo.net/tree/master/Apollo/Logging/Spi](https://github.com/ctripcorp/apollo.net/tree/master/) for details Apollo/Logging/Spi) # III. Development ## Module dependency diagram ![Module Dependency Diagram](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/module-dependency.png) ## 3.1 Portal implementation for user login Please refer to [Portal implementation of user login function](en/extension/portal-how-to-implement-user-login-function) ## 3.2 Portal access to mail service Please refer to [Portal Enabling Email Service](en/extension/portal-how-to-enable-email-service) ## 3.3 Shared session for Portal cluster deployment Please refer to [Portal Shared Session](en/extension/portal-how-to-enable-session-store) ================================================ FILE: docs/en/contribution/apollo-release-guide.md ================================================ # Apollo Release Guide (Skill-driven) This guide is for teams using Codex / Claude Code to trigger release skills via natural language. The goal is to minimize manual operations, standardize release quality, and keep human confirmation before critical external actions. ## 1. Scope and recommended order The Apollo release process is covered by three skills: 1. `apollo-java-release` for `apolloconfig/apollo-java` 2. `apollo-release` for `apolloconfig/apollo` 3. `apollo-helm-chart-release` for `apolloconfig/apollo-helm-chart` Recommended order: 1. Release `apollo-java-release` first (no dependency on other release flows) 2. Release `apollo-release` second (usually depends on the new Java SDK version) 3. Release `apollo-helm-chart-release` last > If a release does not involve one repository, you can skip that sub-flow. ## 2. Preparation ### 2.1 Permissions and tools - GitHub permissions for PR, Release, Workflow, Discussion, and Milestone operations - Required local commands: `git`, `gh`, `python3`, `jq` - Additional command for Helm chart release: `helm` ### 2.2 Repository and branch state - Keep each target repository clean before starting - Ensure base branches are up to date (`master` for `apollo`, default branch for other repos) ### 2.3 Release inputs to prepare - Release version (for example `2.8.0`) - Next development version (for example `2.9.0-SNAPSHOT`) - Highlights PR list (for example `PR_ID_1,PR_ID_2,PR_ID_3`) ### 2.4 Install release skills (Codex / Claude Code) Before using the flows in this guide, install these three skills first: - `apollo-java-release` - `apollo-release` - `apollo-helm-chart-release` Recommended approach (natural language): 1. Use `skill-installer` in a Codex session 2. Set the source to the full GitHub URL: `https://github.com/apolloconfig/apollo-skills` 3. Install the three skills above Example natural-language request: - “Use `skill-installer` to install `apollo-java-release`, `apollo-release`, and `apollo-helm-chart-release` from `https://github.com/apolloconfig/apollo-skills`.” If you prefer manual setup, place these skill folders into your local skill directory (usually `$CODEX_HOME/skills` or `~/.codex/skills`) and restart the client. ## 3. Release Apollo Java (`apollo-java-release`) ### 3.1 How to trigger (natural language) In the `apollo-java` workspace session, ask with a prompt like: - "Use `apollo-java-release` to publish `X.Y.Z`, next version `A.B.C-SNAPSHOT`, and use these PRs for highlights: `...`" ### 3.2 What the skill automates - Version bump PR (`revision` from SNAPSHOT to release) - Prerelease creation - Trigger `release.yml` and wait for Sonatype Central publish completion - Announcement discussion publishing - Post-release snapshot bump and post-release PR - Prerelease promotion to official release ### 3.3 Checkpoint interaction model - The skill pauses automatically before critical external actions and asks whether to continue - You respond in chat to continue, or request edits before proceeding ## 4. Release Apollo Server (`apollo-release`) ### 4.1 How to trigger (natural language) In the `apollo` workspace session, ask with a prompt like: - "Use `apollo-release` to publish `X.Y.Z`, next version `A.B.C-SNAPSHOT`, with highlights PRs `...`" ### 4.2 What the skill automates - Version bump PR (`pom.xml` `revision`) - Release notes and announcement draft generation from `CHANGES.md` - Prerelease creation (`vX.Y.Z`) - Package build + checksum upload via GitHub Actions - Docker publish workflow trigger - Prerelease promotion to official release - Announcement discussion publishing - Post-release snapshot bump, `CHANGES.md` archive, milestone updates, and post-release PR ### 4.3 Checkpoint interaction model Same model as `apollo-java-release`: - System prompts appear at checkpoints - You can continue or request adjustments (highlights, notes, parameters) ## 5. Release Helm Charts (`apollo-helm-chart-release`) ### 5.1 How to trigger (natural language) In the `apollo-helm-chart` workspace session, ask with a prompt like: - "Use `apollo-helm-chart-release` to publish the current chart version changes" ### 5.2 What the skill automates - Chart version change detection and consistency checks - `helm lint`, `helm package`, and `helm repo index` - File whitelist checks to prevent accidental commits - Branch and commit draft generation - Pause before push / PR for explicit confirmation (no auto-push/no auto-PR by default) ## 6. Unified post-release verification At minimum, verify: 1. Apollo release page includes 3 zip files and 3 sha1 files 2. Docker image tags are available 3. `apollo-java` artifacts are available in Maven Central 4. Helm repository `docs/index.yaml` includes the new chart versions 5. Core smoke tests pass (config publish, gray release, client fetch, portal core flows) ## 7. Common operations (skill usage perspective) ### 7.1 Resume after interruption Ask directly in chat, for example: - "Continue the previous release flow" - "Resume from the next checkpoint" The skill restores from state and does not repeat completed steps. ### 7.2 Dry run first Request a dry run first, for example: - "Run `apollo-release` in dry-run mode first so I can review the plan" Then request the real run after confirmation. ### 7.3 Adjust highlights / wording Before prerelease creation, ask for adjustments, for example: - "Use these PRs for highlights: `...`, then regenerate release notes" After review, continue to the next checkpoint. ================================================ FILE: docs/en/deployment/deployment-architecture.md ================================================ #   # I. Introduction According to different scenarios, apolloconfig deployment architecture will have a variety of, here do not discuss the details, only from the macro perspective of the deployment architecture, to introduce the various deployment options ## 1.1 Flowchart Use flowchart to express the deployment method, here first introduce some basic concepts ### 1.1.1 Dependencies Dependencies are expressed with ```mermaid graph LR 1 --> 2 ``` Indicates that 1 depends on 2, i.e. 2 must exist for 1 to work properly, e.g. ```mermaid flowchart LR Application --> MySQL ``` Means that the application needs to use MySQL to work properly Dependencies can be complex, as well as having multiple layers of dependencies, for example ```mermaid flowchart LR SA[Service A] --> RC[Registration Center] SA[Service A] --> B[Service B] --> MySQL SA[Service A] --> Redis ``` Service A needs registry, Service B, Redis And Service B needs MySQL ### 1.1.2 Inclusion Relationships The containment relationship is specified with ```mermaid graph subgraph a b end ``` Indicates that a contains b, i.e. b is part of a. The containment relationship may be nested, e.g. ```mermaid flowchart LR subgraph Linux-Server subgraph JVM1 Thread1.1 Thread1.2 end subgraph JVM2 Thread2.1 end MySQL Redis end ``` Means that on a Linux server, there are MySQL, Redis and 2 JVMs running, and there are Threads in each JVM # II. Standalone The scenario of standalone deployment is usually for novice learners, or for testing environments with low performance requirements within the company, not for production environments ## 2.1 Standalone, Single Environment All In One This is the simplest and most convenient way to deploy standalone Requires: * 1 linux server: with JRE * 2 databases: 1 `PortalDB` and `ConfigDB` As shown below, all modules are deployed on the same Linux machine, with a total of 3 JVM processes ```mermaid flowchart LR m[Meta Server] e[Eureka] c[Config Service] a[Admin Service] p[Portal] configdb[(ConfigDB)] portaldb[(PortalDB)] subgraph Linux Server subgraph JVM8080 m e c end subgraph JVM8090 a end subgraph JVM8070 p end end c --> configdb a --> configdb p --> portaldb ``` JVM8080: the network port exposed to the public is 8080, there are Meta Server, Eureka, Config Service inside, and Config Service uses ConfigDB JVM8090: the network port exposed to the public is 8090, there is Admin Service inside, and Admin Service uses ConfigDB JVM8070: the network port exposed to the public is 8070, there is Portal inside, and Portal uses PortalDB If you add the dependency between modules, flowchart will become ```mermaid flowchart LR m[Meta Server] e[Eureka] c[Config Service] a[Admin Service] p[Portal] configdb[(ConfigDB)] portaldb[(PortalDB)] subgraph Linux Server subgraph JVM8080 m e c end subgraph JVM8090 a end subgraph JVM8070 p end end c --> configdb a --> configdb p --> portaldb m --> e c --> e a --> e p --> m p --> a ``` Config Service and Admin Service will register themselves to Eureka Portal discovers Admin Service through Meta Server service To make flowchart look more concise, you can just represent the dependencies between processes ```mermaid flowchart LR m[Meta Server] e[Eureka] c[Config Service] a[Admin Service] p[Portal] configdb[(ConfigDB)] portaldb[(PortalDB)] subgraph Linux Server subgraph JVM8080 m e c end subgraph JVM8090 a end subgraph JVM8070 p end end JVM8080 --> configdb JVM8090 --> configdb JVM8070 --> portaldb jvm8090 --> jvm8080 jvm8070 --> jvm8090 ``` Process JVM8070 depends on process JVM8090 and PortalDB Process JVM8090 depends on process JVM8080 and ConfigDB Process JVM8080 depends on process JVM8080 and ConfigDB ## 2.2 Standalone, single environment Separate deployment ### 2.2.1 Stand-alone, single-environment deployment Separate deployment 3 Linux servers The 3 JVM processes can also be spread across 3 Linux machines Required. * 3 linux servers: 3 processes to be deployed separately * 2 databases ```mermaid flowchart LR m[Meta Server] e[Eureka] c[Config Service] a[Admin Service] p[Portal] configdb[(ConfigDB)] portaldb[(PortalDB)] subgraph Linux Server 1 subgraph JVM8080 m e c end end subgraph Linux Server 2 subgraph JVM8090 a end end subgraph Linux Server 3 subgraph JVM8070 p end end JVM8080 --> configdb JVM8090 --> configdb JVM8070 --> portaldb jvm8090 --> jvm8080 jvm8070 --> jvm8090 ``` ### 2.2.2 Single machine, single environment Separate deployment 2 Linux servers But usually we deploy Config Service and Admin Service on a single Linux server Required: * 2 linux servers: 1 to deploy Portal, the other to deploy Config Service and Admin Service * 2 databases ```mermaid flowchart LR m[Meta Server] e[Eureka] c[Config Service] a[Admin Service] p[Portal] configdb[(ConfigDB)] portaldb[(PortalDB)] subgraph Linux Server 1 subgraph JVM8080 m e c end subgraph JVM8090 a end end subgraph Linux Server 2 subgraph JVM8070 p end end JVM8080 --> configdb JVM8090 --> configdb JVM8070 --> portaldb jvm8090 --> jvm8080 jvm8070 --> jvm8090 ``` Later, in order to flowchart more concise, the content in JVM8080 will be simplified, only the Config Service will be displayed, and the Meta Server and Eureka inside will no longer be displayed ```mermaid flowchart LR subgraph JVM8080 m[Meta Server] e[Eureka] c[Config Service] end subgraph new-JVM8080[JVM8080] new-c[Config Service] end JVM8080 --> |simplify| new-JVM8080 ``` So the deployment architecture can be simplified and represented as ```mermaid flowchart LR c[Config Service] a[Admin Service] p[Portal] configdb[(ConfigDB)] portaldb[(PortalDB)] subgraph Linux Server 1 subgraph JVM8080 c end subgraph JVM8090 a end end subgraph Linux Server 2 subgraph JVM8070 p end end JVM8080 --> configdb JVM8090 --> configdb JVM8070 --> portaldb jvm8090 --> jvm8080 jvm8070 --> jvm8090 ``` ## 2.3 Single machine, dual environment A single environment basically can not meet the actual application scenario, for example, the company has SIT test environment and UAT test environment, then you need to deploy two environments to provide configuration services It is easy to think of the deployment architecture as follows, repeat the single machine, single environment deployment architecture 2 times Required: * 2 linux servers * 4 databases ```mermaid flowchart LR subgraph SIT c1[SIT Config Service] a1[SIT Admin Service] p1[SIT Portal] configdb1[(SIT ConfigDB)] portaldb1[(SIT PortalDB)] subgraph SIT Linux Server subgraph sit-jvm-8080[SIT JVM8080] c1 end subgraph sit-jvm-8090[SIT JVM8090] a1 end subgraph sit-jvm-8070[SIT JVM8070] p1 end end sit-jvm-8080 --> configdb1 sit-jvm-8090 --> configdb1 sit-jvm-8070 --> portaldb1 sit-jvm-8090 --> sit-jvm-8080 sit-jvm-8070 --> sit-jvm-8090 end subgraph UAT c2[UAT Config Service] a2[UAT Admin Service] p2[UAT Portal] configdb2[(UAT ConfigDB)] portaldb2[(UAT PortalDB)] subgraph UAT Linux Server subgraph uat-jvm-8080[UAT JVM8080] c2 end subgraph uat-jvm-8090[UAT JVM8090] a2 end subgraph uat-jvm-8070[UAT JVM8070] p2 end end uat-jvm-8080 --> configdb2 uat-jvm-8090 --> configdb2 uat-jvm-8070 --> portaldb2 uat-jvm-8090 --> uat-jvm-8080 uat-jvm-8070 --> uat-jvm-8090 end ``` But this solution, there will be 2 Portal interface, can not be 1 interface to manage 2 environments, the use of experience is not very good, Portal can actually be deployed only 1 set, the recommended deployment architecture is as follows * 3 linux servers: * Portal Linux Server to deploy Portal alone * SIT Linux Server to deploy SIT's Config Service and Admin Service * UAT Linux Server deploys the Config Service and Admin Service of UAT * 3 databases: 1 PortalDB + 1 SIT's ConfigDB + 1 UAT's ConfigDB ```mermaid flowchart LR p[Portal] portaldb[PortalDB] p --> portaldb subgraph Portal Linux Server subgraph JVM8070 p end end subgraph SIT c1[SIT Config Service] a1[SIT Admin Service] configdb1[(SIT ConfigDB)] subgraph SIT Linux Server subgraph sit-jvm-8080[SIT JVM8080] c1 end subgraph sit-jvm-8090[SIT JVM8090] a1 end end sit-jvm-8080 --> configdb1 sit-jvm-8090 --> configdb1 sit-jvm-8090 --> sit-jvm-8080 end subgraph UAT c2[UAT Config Service] a2[UAT Admin Service] configdb2[(UAT ConfigDB)] subgraph UAT Linux Server subgraph uat-jvm-8080[UAT JVM8080] c2 end subgraph uat-jvm-8090[UAT JVM8090] a2 end end uat-jvm-8080 --> configdb2 uat-jvm-8090 --> configdb2 uat-jvm-8090 --> uat-jvm-8080 end jvm8070 --> sit-jvm-8090 jvm8070 --> uat-jvm-8090 ``` ## 2.4 Standalone, three environments Assuming that the usage scenario of 3 environments, SIT, UAT and PP, now needs to be satisfied. On top of the previous dual environments, add 1 more PP environment Linux service and ConfigDB can be added, Portal to manage these 3 environments by modifying the configuration ```mermaid flowchart LR p[Portal] portaldb[PortalDB] p --> portaldb subgraph Portal Linux Server subgraph JVM8070 p end end subgraph SIT c1[SIT Config Service] a1[SIT Admin Service] configdb1[(SIT ConfigDB)] subgraph SIT Linux Server subgraph sit-jvm-8080[SIT JVM8080] c1 end subgraph sit-jvm-8090[SIT JVM8090] a1 end end sit-jvm-8080 --> configdb1 sit-jvm-8090 --> configdb1 sit-jvm-8090 --> sit-jvm-8080 end subgraph UAT c2[UAT Config Service] a2[UAT Admin Service] configdb2[(UAT ConfigDB)] subgraph UAT Linux Server subgraph uat-jvm-8080[UAT JVM8080] c2 end subgraph uat-jvm-8090[UAT JVM8090] a2 end end uat-jvm-8080 --> configdb2 uat-jvm-8090 --> configdb2 uat-jvm-8090 --> uat-jvm-8080 end subgraph PP c3[PP Config Service] a3[PP Admin Service] configdb3[(PP ConfigDB)] subgraph PP Linux Server subgraph pp-jvm-8080[PP JVM8080] c3 end subgraph pp-jvm-8090[PP JVM8090] a3 end end pp-jvm-8080 --> configdb3 pp-jvm-8090 --> configdb3 pp-jvm-8090 --> pp-jvm-8080 end jvm8070 --> sit-jvm-8090 jvm8070 --> uat-jvm-8090 jvm8070 --> pp-jvm-8090 ``` ## 2.5 Single machine, multiple environments The principle is the same as above, 1 Linux server per environment + 1 ConfigDB Then Portal can add the information of the new environment # III. High Availability 1 environment only 1 Config Service process, can not meet the high availability, in order to avoid a single point of downtime affect the availability of the system, the need for multi-instance deployment, that is, the deployment of multiple Java processes on different Linux servers ## 3.1 Minimal High Availability, Single Environment Returning to the common non-high-availability deployment approach, the ```mermaid flowchart LR c[Config Service] a[Admin Service] p[Portal] configdb[(ConfigDB)] portaldb[(PortalDB)] subgraph Linux Server 1 subgraph JVM8080 c end subgraph JVM8090 a end end subgraph Linux Server 2 subgraph JVM8070 p end end JVM8080 --> configdb JVM8090 --> configdb JVM8070 --> portaldb JVM8090 --> JVM8080 JVM8070 --> JVM8090 ``` When Linux Server 1 is down, the client can only read the config-cache on the local disk. If you need to prevent a single Linux from going down and making the Config Service unavailable, you can try adding another Linux machine. Required: * 3 linux servers: 1 to deploy Portal, and 2 to deploy Config Service and Admin Service respectively * 2 databases ```mermaid flowchart LR c-1[Config Service] c-2[Config Service] a-1[Admin Service] a-2[Admin Service] p[Portal] configdb[(ConfigDB)] portaldb[(PortalDB)] JVM8080-1[JVM8080] JVM8080-2[JVM8080] JVM8090-1[JVM8090] JVM8090-2[JVM8090] subgraph Linux Server 1.1 subgraph JVM8080-1[JVM8080] c-1 end subgraph JVM8090-1[JVM8090] a-1 end end subgraph Linux Server 1.2 subgraph JVM8080-2[JVM8080] c-2 end subgraph JVM8090-2[JVM8090] a-2 end end subgraph Linux Server 2 subgraph JVM8070 p end end JVM8080-1 --> configdb JVM8090-1 --> configdb JVM8080-2 --> configdb JVM8090-2 --> configdb JVM8070 --> portaldb JVM8090-1 --> JVM8080-1 JVM8090-2 --> JVM8080-2 JVM8070 --> JVM8090-1 JVM8070 --> JVM8090-2 ``` With this deployment, if Linux Server 1.1 or Linux Server 1.2 is down, the system is still available. ## 3.2 Highly available, single environment On the basis of the above, if the number of clients is large (e.g. tens of thousands of Java processes), you can horizontally extend the Config Service by introducing Linux Server 1.3, Linux Server 1.4, ... Admin Service can be much less than Config Service in terms of number due to only Portal access. Please refer to [Apollo Performance Test Report](en/misc/apollo-benchmark.md) for details on how to evaluate the number of Config Service. ## 3.3 High Availability, Dual Environment As in [2.3 Single machine, dual environment](#_23-single-machine-dual-environment), if you want to make both SIT and UAT highly available, you only need to add more machines to each environment, as shown below, each environment has 2 Linux Servers, if you have performance requirements, you can use more machines in each environment to deploy Config Service that can be ```mermaid flowchart LR p[Portal] portaldb[(PortalDB)] p --> portaldb subgraph Portal Linux Server subgraph JVM8070 p end end subgraph SIT sit-c1[SIT Config Service] sit-a1[SIT Admin Service] sit-c2[SIT Config Service] sit-a2[SIT Admin Service] sit-configdb[(SIT ConfigDB)] subgraph SIT Linux Server 2.1 subgraph sit-c1-jvm-8080[SIT JVM8080] sit-c1 end subgraph sit-c1-jvm-8090[SIT JVM8090] sit-a1 end end subgraph SIT Linux Server 2.2 subgraph sit-c2-jvm-8080[SIT JVM8080] sit-c2 end subgraph sit-c2-jvm-8090[SIT JVM8090] sit-a2 end end sit-c1-jvm-8080 --> sit-configdb sit-c1-jvm-8090 --> sit-configdb sit-c2-jvm-8080 --> sit-configdb sit-c2-jvm-8090 --> sit-configdb sit-c1-jvm-8090 --> sit-c1-jvm-8080 sit-c2-jvm-8090 --> sit-c2-jvm-8080 end subgraph UAT uat-c1[UAT Config Service] uat-a1[UAT Admin Service] uat-c2[UAT Config Service] uat-a2[UAT Admin Service] uat-configdb[(UAT ConfigDB)] subgraph UAT Linux Server 2.1 subgraph uat-c1-jvm-8080[UAT JVM8080] uat-c1 end subgraph uat-c1-jvm-8090[UAT JVM8090] uat-a1 end end subgraph UAT Linux Server 2.2 subgraph uat-c2-jvm-8080[UAT JVM8080] uat-c2 end subgraph uat-c2-jvm-8090[UAT JVM8090] uat-a2 end end uat-c1-jvm-8080 --> uat-configdb uat-c1-jvm-8090 --> uat-configdb uat-c2-jvm-8080 --> uat-configdb uat-c2-jvm-8090 --> uat-configdb uat-c1-jvm-8090 --> uat-c1-jvm-8080 uat-c2-jvm-8090 --> uat-c2-jvm-8080 end jvm8070 --> sit-c1-jvm-8090 jvm8070 --> sit-c2-jvm-8090 jvm8070 --> uat-c1-jvm-8090 jvm8070 --> uat-c2-jvm-8090 ``` ## 3.4 High Availability, Multiple Environments On top of the above, to add an environment such as BETA environment, you need to add 2 and more Linux servers + 1 ConfigDB Portal adds the information of the new environment, pointing to apollo.meta of BETA environment ## 3.5 High Availability, Single Environment, Single Server Room In the actual production environment, many companies isolate their test environment, so the production environment is a single environment, with only one PRO environment When there is only 1 server room, refer to [3.2 Highly available, single environment](#_32-highly-available-single-environment) ## 3.6 Highly available, single environment, dual server rooms If there are 2 server rooms, usually there is network isolation between the server rooms. If it is a co-located server room, idc1 and idc2, you can use the following deployment method ```mermaid flowchart LR idc1-p[idc1 Portal] idc2-p[idc2 Portal] portaldb[(PortalDB)] idc1-p --> portaldb idc2-p --> portaldb configdb[(ConfigDB)] idc1-c1-jvm-8080 --> configdb idc1-c1-jvm-8090 --> configdb idc1-c2-jvm-8080 --> configdb idc1-c2-jvm-8090 --> configdb idc2-c1-jvm-8080 --> configdb idc2-c1-jvm-8090 --> configdb idc2-c2-jvm-8080 --> configdb idc2-c2-jvm-8090 --> configdb subgraph idc1 subgraph idc1 Portal Linux Server subgraph idc1-JVM8070 idc1-p end end idc1-c1[idc1 Config Service] idc1-a1[idc1 Admin Service] idc1-c2[idc1 Config Service] idc1-a2[idc1 Admin Service] subgraph idc1 Linux Server 2.1 subgraph idc1-c1-jvm-8080[idc1 JVM8080] idc1-c1 end subgraph idc1-c1-jvm-8090[idc1 JVM8090] idc1-a1 end end subgraph idc1 Linux Server 2.2 subgraph idc1-c2-jvm-8080[idc1 JVM8080] idc1-c2 end subgraph idc1-c2-jvm-8090[idc1 JVM8090] idc1-a2 end end idc1-c1-jvm-8090 --> idc1-c1-jvm-8080 idc1-c2-jvm-8090 --> idc1-c2-jvm-8080 end subgraph idc2 subgraph idc2 Portal Linux Server subgraph idc2-JVM8070 idc2-p end end idc2-c1[idc2 Config Service] idc2-a1[idc2 Admin Service] idc2-c2[idc2 Config Service] idc2-a2[idc2 Admin Service] subgraph idc2 Linux Server 2.1 subgraph idc2-c1-jvm-8080[idc2 JVM8080] idc2-c1 end subgraph idc2-c1-jvm-8090[idc2 JVM8090] idc2-a1 end end subgraph idc2 Linux Server 2.2 subgraph idc2-c2-jvm-8080[idc2 JVM8080] idc2-c2 end subgraph idc2-c2-jvm-8090[idc2 JVM8090] idc2-a2 end end idc2-c1-jvm-8090 --> idc2-c1-jvm-8080 idc2-c2-jvm-8090 --> idc2-c2-jvm-8080 end idc1-JVM8070 --> idc1-c1-jvm-8090 idc1-JVM8070 --> idc1-c2-jvm-8090 idc2-JVM8070 --> idc2-c1-jvm-8090 idc2-JVM8070 --> idc2-c2-jvm-8090 ``` Each server room has its own set of Portal, Config Service, Admin Service For ConfigDB, under the same city and dual server rooms, the ConfigDB connected is the same, there is no 2 different ConfigDB, for PortalDB is also the same, need to connect the same ConfigDB and PortalDB are not put into idc1 or idc2 in the diagram, you need to choose the suitable MySQL architecture and deployment method by yourself. # IV. Deployment diagram ## 4.1 In Ctrip In ctrip, We deployment strategy is as follows. ![Deployment](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/apollo-deployment.png) * Portal is deployed in the server room of the production environment, through which the configuration of FAT, UAT, PRO and other environments are managed directly * Meta Server, Config Service and Admin Service are deployed separately in each environment, using separate databases * Meta Server, Config Service and Admin Service are deployed in two server rooms in the production environment to achieve duplexing * Meta Server and Config Service are deployed in the same JVM process, and Admin Service is deployed in another JVM process on the same server ## 4.2 Sample deployment diagram Sample deployment diagram contributed by [@lyliyongblue](https://github.com/lyliyongblue) (we recommend right-clicking a new window to open a larger version). ![Deployment](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/lyliyongblue-apollo-deployment.png) ================================================ FILE: docs/en/deployment/distributed-deployment-guide.md ================================================ This document describes how to compile, package, and deploy Apollo Configuration Center in a distributed deployment manner, so that it can be deployed and run separately in development, test, and production environments. > If you just need to try Apollo locally, you can refer to [Quick Start](en/deployment/quick-start) #   # I. Preparation ## 1.1 Runtime environment ## 1.1.1 OS The server side is based on Spring Boot and the startup script theoretically supports all Linux distributions, [CentOS 7](https://www.centos.org/) is recommended. ### 1.1.2 Java * Apollo server: 17+ * Apollo client: 1.8+ * For running in Java 1.7 runtime environment, please use apollo client of 1.x version, such as 1.9.1 Once configured, this can be checked with the following command. ```sh java -version ``` Sample output. ```sh java version "17.0.14" Java(TM) SE Runtime Environment (build 17.0.14+7) Java HotSpot(TM) 64-Bit Server VM (build 17.0.14+7, mixed mode) ``` ## 1.2 MySQL * Version requirement: 5.6.5+ Apollo's table structure uses multiple default declarations for `timestamp`, so version 5.6.5+ is required. After connecting to MySQL, you can check with the following command. ```sql SHOW VARIABLES WHERE Variable_name = 'version'; ``` | Variable_name | Value | | ------------- | ------ | | version | 5.7.11 | > Note 1: MySQL versions can be downgraded to 5.5, see [mysql dependency downgrade discussion](https://github.com/apolloconfig/apollo/issues/481) for details. > Note 2: If you wish to use Oracle, you can refer to [vanpersl](https://github.com/vanpersl)'s [Oracle Adaptation Code](https://github.com/apolloconfig/apollo/compare/v0.8.0...vanpersl:db-oracle) developed on top of Apollo 0.8.0 with `Oracle` version `10.2.0.1.0`. > Note 3: If you wish to use Postgres, you can refer to the [Pg adaptation code](https://github.com/oaksharks/apollo/compare/ac10768ee2e11c488523ca0e845984f6f71499ac...oaksharks:pg) developed by [oaksharks](https://github.com/oaksharks) on top of Apollo 0.9.1 with `Postgres` version 9.3.20, also see [xiao0yy](https://github.com/xiao0yy) developed on the basis of Apollo 0.10.2 [Pg adaptation code](https://github.com/apolloconfig/apollo/issues/1293) with `Postgres` version 9.5. ## 1.3 Environment Distributed deployments require a pre-determined environment for the deployment and how it will be deployed. Apollo currently supports the following environments. * DEV * Development environment * FAT * Test environments, equivalent to alpha environments (functional testing) * UAT * Integration environment, equivalent to a beta environment (regression testing) * PRO * Production environment > If you want to add custom environment names, you can refer to [How to add new environments by Portal Console](en/faq/common-issues-in-deployment-and-development-phase?id=_4-how-to-add-environment-by-portal-console) for the specific steps. > Please note, if your custom environment name is "PROD", it will be forcibly converted to "PRO". Similarly, if the environment name is "FWS", it will be forcibly converted to "FAT". You can refer to [deployment-architecture](en/deployment/deployment-architecture.md) ## 1.4 Network Policy For distributed deployment, `apollo-configservice` and `apollo-adminservice` need to register their IPs and ports to Meta Server (apollo-configservice itself). Apollo clients and Portal will get the address (IP+port) of the service from Meta Server, and then access it directly through the service address. Note that `apollo-configservice` and `apollo-adminservice` are designed based on the intranet trusted network, so for security reasons, **please do not expose `apollo-configservice` and `apollo-adminservice` directly to the public network**. So if the actual deployed machine has multiple NICs (e.g. docker), or there are some NICs with IPs that are not accessible by Apollo clients and Portal (e.g. network security restrictions), then we need to do relevant configurations in `apollo-configservice` and `apollo-adminservice` to solve connectivity issues. ### 1.4.1 Ignoring certain NICs You can modify the startup.sh of `apollo-configservice` and `apollo-adminservice` respectively by passing the -D parameter through the JVM System Property, or by passing the OS Environment Variable, the following example will change the ` docker0` and NICs starting with `veth` are ignored when registering to Eureka. JVM System Property example. ```properties -Dspring.cloud.inetutils.ignoredInterfaces[0]=docker0 -Dspring.cloud.inetutils.ignoredInterfaces[1]=veth.* ``` OS Environment Variable example. ```properties SPRING_CLOUD_INETUTILS_IGNORED_INTERFACES[0]=docker0 SPRING_CLOUD_INETUTILS_IGNORED_INTERFACES[1]=veth.* ``` ### 1.4.2 Specifying the IP to be registered You can modify the startup.sh of `apollo-configservice` and `apollo-adminservice` respectively, passing in the -D parameter via JVM System Property, or via OS Environment Variable, the following example will specify the IP to be registered as `1.2.3.4`. JVM System Property example. ```properties -Deureka.instance.ip-address=1.2.3.4 ``` OS Environment Variable example. ```properties EUREKA_INSTANCE_IP_ADDRESS=1.2.3.4 ``` ### 1.4.3 Specify the URL to register You can modify the startup.sh of `apollo-configservice` and `apollo-adminservice` respectively, passing in the -D parameter via JVM System Property, or via OS Environment Variable, the following example will specify the URL to register URL as `http://1.2.3.4:8080`. >Note: The default registration ports for apollo-configservice and apollo-adminservice are 8080 and 8090 respectively. JVM System Property example. ```properties # apollo-configservice -Deureka.instance.homePageUrl=http://1.2.3.4:8080 -Deureka.instance.preferIpAddress=false # apollo-adminservice -Deureka.instance.homePageUrl=http://1.2.3.4:8090 -Deureka.instance.preferIpAddress=false ``` OS Environment Variable Example. ```properties # apollo-configservice EUREKA_INSTANCE_HOME_PAGE_URL=http://1.2.3.4:8080 EUREKA_INSTANCE_PREFER_IP_ADDRESS=false # apollo-adminservice EUREKA_INSTANCE_HOME_PAGE_URL=http://1.2.3.4:8090 EUREKA_INSTANCE_PREFER_IP_ADDRESS=false ``` ### 1.4.4 Specifying apollo-configservice address directly If Apollo is deployed on the public cloud and the local development environment cannot connect, but you need to do development testing, the client can upgrade to version 0.11.0 and above, and then configure [Skip Apollo Meta Server service discovery](en/client/java-sdk-user-guide?id=_1222-skip-apollo-meta-server-service-discovery) ### 1.4.5 Network Configuration In some companies (e.g. companies in the financial industry), there are many firewalls and network isolation, and it is necessary to open up the network (so that `ip1` can access a port of `ip2`) #### 1.4.5.1 Configure the network from the client to the configuration center For clients that use the configuration center, `Apollo-Client` needs to access all (or the same room within) Meta Server and Config Service (default are port 8080), please do not open the network from `Client` to `Admin Service`. ```mermaid flowchart LR subgraph servers[IP1:8080, IP2:8080, ..., IPn:8080] m[Meta Sever] c[Config Service] end client --> servers ``` If an application needs to use openapi, it also needs to access Portal (default is port 8070). ```mermaid flowchart LR subgraph servers[IP:8070] Portal end openapi-client --> servers ``` #### 1.4.5.2 Configure the network within the configuration center For the configuration center itself, it is also necessary to ensure network connectivity as each service needs to access each other. ```mermaid flowchart LR subgraph config-service-servers[All Config Service's IP:8080] m[Meta Server] c[Config Service] end subgraph admin-service-servers[All Admin Service's IP:8090] a[Admin Service] end subgraph portal-servers[IP:8070] p[Portal] end configdb[(ConfigDB)] portaldb[(PortalDB)] a --> config-service-servers a --> configdb c --> configdb p --> config-service-servers p --> admin-service-servers p --> portaldb ``` # II. Deployment Steps The overall deployment steps are relatively simple. > [@lingjiaju](https://github.com/lingjiaju) recorded a series of Apollo quick start videos, if you feel slightly tedious to read the documentation, you may wish to first look at his [video tutorial](https://pan.baidu.com/s/1blv87EOZS77NWT8Amkijkw#list/path=%2F) . > If you encounter problems during the deployment process, you can refer to [common issues encountered in deployment & development](en/faq/common-issues-in-deployment-and-development-phase), and you can usually find the answers. ## 2.1 Creating the database Apollo server side needs a total of two databases: `ApolloPortalDB` and `ApolloConfigDB`, we prepared the database, table creation and sample data as sql files respectively, and just need to import the database. Note that ApolloPortalDB only needs to deploy one in the production environment, while ApolloConfigDB needs to deploy one set in each environment, such as fat, uat and pro respectively, to deploy 3 sets of ApolloConfigDB. > Note: If you have already created Apollo database locally, please pay attention to backup data. The sql file we prepare will empty the Apollo related tables. ### 2.1.1 Creating ApolloPortalDB #### 2.1.1.1 Manual SQL Import You can import [apolloportaldb.sql](https://github.com/apolloconfig/apollo/blob/master/scripts/sql/profiles/mysql-default/apolloportaldb.sql) through various MySQL clients. Using the native MySQL client as an example. ```sql source /your_local_path/scripts/sql/profiles/mysql-default/apolloportaldb.sql ``` #### 2.1.1.2 Verification After a successful import, you can verify it by executing the following sql statement. ```sql select `Id`, `Key`, `Value`, `Comment` from `ApolloPortalDB`. `ServerConfig` limit 1; ``` | Id | Key | Value | Comment | | ---- | ------------------ | ----- | ------------------------------ | | 1 | apollo.portal.envs | dev | list of supported environments | > Note: ApolloPortalDB only needs to be deployed in a production environment. ### 2.1.2 Creating ApolloConfigDB #### 2.1.2.1 Importing SQL Manually You can import [apolloconfigdb.sql](https://github.com/apolloconfig/apollo/blob/master/scripts/sql/profiles/mysql-default/apolloconfigdb.sql) through various MySQL clients. Using the native MySQL client as an example. ```sql source /your_local_path/scripts/sql/profiles/mysql-default/apolloconfigdb.sql ``` #### 2.1.2.2 Verification After a successful import, you can verify it by executing the following sql statement. ```sql select `Id`, `Key`, `Value`, `Comment` from `ApolloConfigDB`. `ServerConfig` limit 1; ``` | Id | Key | Value | Comment | | ---- | ------------------ | ----------------------------- | ------------------ | | 1 | eureka.service.url | http://127.0.0.1:8080/eureka/ | Eureka Service Url | > Note: ApolloConfigDB needs to be deployed in one set per environment, e.g. 3 sets of ApolloConfigDB for fat, uat and pro respectively #### 2.1.2.4 Importing ApolloConfigDB project data from another environment If the Apollo configuration center is newly deployed, please ignore this step. If the Apollo Configuration Center is not newly deployed, for example, it has been in use for some time and a number of projects and namespaces have been created in the Apollo Configuration Center at that time, then you need to import the necessary project data from other normally running environments in the ApolloConfigDB in the new environment. The following four tables of ApolloConfigDB are mainly involved, and the following data query statements are also attached. 1. App * Import all the apps * e.g.: insert into `New environment of ApolloConfigDB`. `App` select * from `Other Environment's ApolloConfigDB`. `App` where `IsDeleted` = 0; 2. AppNamespace * Import all AppNamespace * e.g. insert into `New environment's ApolloConfigDB`. `AppNamespace` select * from `other environment's ApolloConfigDB`. `AppNamespace` where `IsDeleted` = 0; 3. Cluster * Import the default default cluster * e.g. insert into `New environment's ApolloConfigDB`. `Cluster` select * from `ApolloConfigDB of other environment`. `Cluster` where `Name` = 'default' and `IsDeleted` = 0; 4. Namespace * Import the namespace in the default default cluster * e.g. insert into `ApolloConfigDB` of the new environment. `Namespace` select * from `ApolloConfigDB of other environment`. `Namespace` where `ClusterName` = 'default' and `IsDeleted` = 0; Also don't forget to notify users to set the correct configuration information for their projects in the new environment, especially for some public namespace configurations that have a large impact. > If you are migrating data for a running environment, it is recommended to restart the config service after migration, because the config service has cached data for appnamespace ### 2.1.3 Adjusting server-side configuration Apollo's own configuration is placed inside the database, so you need to make some adjustments for the actual situation, please refer to [III. Server-side configuration description](en/deployment/distributed-deployment-guide?id=iii-server-side-configuration-instructions) for specific parameters. Most of the configurations can use the default values first, but [apollo.portal.envs](en/deployment/distributed-deployment-guide?id=_311-apolloportalenvs-list-of-supportable-environments) and [eureka.service.url](en/deployment/distributed-deployment-guide?id=_321-eurekaserviceurl-eureka-service-url) please make sure configured correctly before proceeding to the following deployment steps. ## 2.2 Virtual/physical machine deployment ### 2.2.1 Get the installation package The installation package can be obtained in two ways. 1. directly downloading the installer * Download the pre-typed installer from the [GitHub Release](https://github.com/apolloconfig/apollo/releases) page * If you don't need to customize Apollo's code, it is recommended to use this way to skip the local packaging process 2. Build via source code * Download the Source code package from the [GitHub Release](https://github.com/apolloconfig/apollo/releases) page or directly clone [source code](https://github.com/ctripcorp/apollo) then build locally * If you need to do custom development for Apollo, you need to use this method #### 2.2.1.1 Download the installation package directly ##### 2.2.1.1.1 Get the apollo-configservice, apollo-adminservice, and apollo-portal installers Download the latest versions of `apollo-configservice-x.x.x-github.zip`, `apollo- adminservice-x.x.x-github.zip` and `apollo-portal-x.x.x-github.zip` can be downloaded. ##### 2.2.1.1.2 Configuring database connection information The Apollo server needs to know how to connect to the database you created earlier. The database connection string information is located in `config/application-github.properties` in the zip file you downloaded in the previous step. ###### 2.2.1.1.2.1 Configuring database connection information for apollo-configservice 1. unzip `apollo-configservice-x.x.x-github.zip`. 2. 2. Open the `application-github.properties` file in the `config` directory with a programmer's editor (e.g. vim, notepad++, sublime, etc.) 3. fill in the correct ApolloConfigDB database connection string information, note that there are no spaces after the username and password! 4. The result of the modification is as follows. ```properties # DataSource spring.datasource.url = jdbc:mysql://localhost:3306/ApolloConfigDB?useSSL=false&characterEncoding=utf8 spring.datasource.username = someuser spring.datasource.password = somepwd ``` > Note: Since ApolloConfigDB is deployed in each environment, you need to configure the database parameters of the corresponding environment for different environment config-services ###### 2.2.1.1.2.2 Configuring database connection information for apollo-adminservice 1. unzip `apollo-adminservice-x.x.x-github.zip`. 2. 2. Open the `application-github.properties` file in the `config` directory with a programmer's editor (e.g. vim, notepad++, sublime, etc.) 3. fill in the correct ApolloConfigDB database connection string information, note that there are no spaces after the username and password! 4. The result of the modification is as follows. ```properties # DataSource spring.datasource.url = jdbc:mysql://localhost:3306/ApolloConfigDB?useSSL=false&characterEncoding=utf8 spring.datasource.username = someuser spring.datasource.password = somepwd ``` > Note: Since ApolloConfigDB is deployed in each environment, you need to configure the database parameters of the corresponding environment for different environment admin-services ###### 2.2.1.1.2.3 Configuring database connection information for apollo-portal 1. unzip `apollo-portal-x.x.x-github.zip`. 2. 2. Open the `application-github.properties` file in the `config` directory with a programmer-specific editor (e.g. vim, notepad++, sublime, etc.) 3. fill in the correct ApolloPortalDB database connection string information, note that there are no spaces after the username and password! 4. The effect after modification is as follows. ```properties # DataSource spring.datasource.url = jdbc:mysql://localhost:3306/ApolloPortalDB?useSSL=false&characterEncoding=utf8 spring.datasource.username = someuser spring.datasource.password = somepwd ``` ###### 2.2.1.1.2.4 Configuring apollo-portal's meta service information Apollo Portal needs to access different meta service (apollo-configservice) addresses in different environments, so we need to provide this information in the configuration. By default, the meta service and config service are deployed in the same JVM process, so the address of the meta service is the address of the config service. > For version 1.6.0 and above, you can configure the Meta Service address through the configuration item in ApolloPortalDB.ServerConfig, see [apollo.portal.meta.servers - List of Meta Service for each environment](en/deployment/distributed-deployment-guide?id=_312-apolloportalmetaservers-list-of-meta-service-for-each-environment) Open the `apollo-env.properties` file in the `config` directory of `apollo-portal-x.x.x-github.zip` using a programmer-specific editor (e.g. vim, notepad++, sublime, etc.). Suppose DEV's apollo-config service is not bound to a domain name at 1.1.1.1:8080, FAT's apollo-config service is bound to the domain name apollo.fat.xxx.com, and UAT's apollo-config service is bound to the domain name apollo.uat.xxx.com, and PRO's apollo-configservice is bound to the domain apollo.xxx.com, then you can modify each environment meta service address as follows, in the format of `${env}.meta=http://${config-service- url:port}`, and if an environment does not need it, you can also directly delete the corresponding configuration item (e.g. lpt.meta): ```properties dev.meta=http://1.1.1.1:8080 fat.meta=http://apollo.fat.xxx.com uat.meta=http://apollo.uat.xxx.com pro.meta=http://apollo.xxx.com ``` In addition to configuring the meta service by means of `apollo-env.properties`, apollo also supports specifying the meta service at runtime (with a higher priority than `apollo-env.properties`): 1. 1. via Java System Property `${env}_meta` * Can be specified via Java System Property `${env}_meta` * e.g. `java -Ddev_meta=http://config-service-url -jar xxx.jar` * Can also be specified programmatically, e.g. `System.setProperty("dev_meta", "http://config-service-url");` 2. through the operating system's System Environment `${ENV}_META` * e.g. `DEV_META=http://config-service-url` * Note that the key is all-caps and separated by `_`. >Note 1: In order to achieve high availability of meta service, it is recommended to do dynamic load balancing by SLB (Software Load Balancer). >Note 2: The meta service address can also be filled with IPs. Before version 0.11.0, only one IP was supported. From version 0.11.0 onwards, multiple addresses separated by commas ([PR #1214](https://github.com/apolloconfig/apollo/pull/1214) ), such as `http://1.1.1.1:8080,http://2.2.2.2:8080`, although production environments are still recommended to use domain names (go slb), as machine expansion, shrinkage, etc. may result in changes to the IP list. #### 2.2.1.2 Building from source code ##### 2.2.1.2.1 Configuring database connection information The Apollo server needs to know how to connect to the database you created earlier, so you need to edit [scripts/build.sh](https://github.com/apolloconfig/apollo/blob/master/scripts/build.sh) and modify ApolloPortalDB and ApolloConfigDB related database connection string information. > Note: The filled-in user needs to have read/write access to ApolloPortalDB and ApolloConfigDB data. ```sh #apollo config db info apollo_config_db_url=jdbc:mysql://localhost:3306/ApolloConfigDB?useSSL=false&characterEncoding=utf8 apollo_config_db_username=username apollo_config_db_password=password (if you don't have a password, just leave it blank) # apollo portal db info apollo_portal_db_url=jdbc:mysql://localhost:3306/ApolloPortalDB?useSSL=false&characterEncoding=utf8 apollo_portal_db_username=username apollo_portal_db_password=password (if you don't have a password, just leave it blank) ``` > Note 1: As ApolloConfigDB is deployed in each environment, so for different environments config-service and admin-service need to use different database parameters to play different packages, portal only need to play once package can > Note 2: If you don't want config-service and admin-service to have a package for each environment, you can also pass in the database connection string information at runtime, which can be found in [Issue 869](https://github.com/apolloconfig/apollo/issues/869) > Note 3: Each environment needs to deploy a separate set of config-service, admin-service and ApolloConfigDB ##### 2.2.1.2.2 Configuring each environment meta service address Apollo Portal needs to access different meta service (apollo-configservice) addresses in different environments, so this information needs to be provided at packaging time. Suppose DEV's apollo-config service is not bound to a domain name with the address 1.1.1.1:8080, FAT's apollo-config service is bound to the domain name apollo.fat.xxx.com, and UAT's apollo-config service is bound to the domain name apollo.uat.xxx.com, and PRO's apollo-configservice is bound to the domain apollo.xxx.com, then edit [scripts/build.sh](https://github.com/apolloconfig/apollo/blob/master/scripts/build.sh) as follows to modify each environment meta service service address in the format ``${env}_meta=http://${config-service-url:port}``, if an environment does not need it, you can also directly delete the corresponding configuration item: ```sh dev_meta=http://1.1.1.1:8080 fat_meta=http://apollo.fat.xxx.com uat_meta=http://apollo.uat.xxx.com pro_meta=http://apollo.xxx.com META_SERVERS_OPTS="-Ddev_meta=$dev_meta -Dfat_meta=$fat_meta -Duat_meta=$uat_meta -Dpro_meta=$pro_meta" ``` In addition to configuring the meta service at packaging time, apollo also supports specifying the meta service at runtime: 1. 1. via Java System Property `${env}_meta` * can be specified via the Java System Property `${env}_meta` * such as `java -Ddev_meta=http://config-service-url -jar xxx.jar` * Can also be specified programmatically, e.g. `System.setProperty("dev_meta", "http://config-service-url");` 2. through the operating system's System Environment `${ENV}_META` * e.g. `DEV_META=http://config-service-url` * Note that the key is all-caps and separated by `_`. >Note 1: In order to achieve high availability of meta service, it is recommended to do dynamic load balancing by SLB (Software Load Balancer). >Note 2: The meta service address can also be filled with IPs. Before version 0.11.0, only one IP was supported. From version 0.11.0 onwards, multiple addresses separated by commas ([PR #1214](https://github.com/apolloconfig/apollo/pull/1214) ), such as `http://1.1.1.1:8080,http://2.2.2.2:8080`, although production environments are still recommended to use domain names (go slb), as machine expansion, shrinkage, etc. may lead to changes in the IP list. ##### 2.2.1.2.3 Perform compilation and packaging After doing the above configuration, you can execute the compilation and packaging. > Note: The initial compilation will download a lot of dependencies from the central Maven repository, so if the network is not good, it is recommended to use a domestic Maven repository source, such as [AliCloud Maven mirror](http://www.cnblogs.com/geektown/p/5705405.html) ```sh ./build.sh ``` This script will package apollo-configservice, apollo-adminservice, apollo-portal in turn. > Note: Since ApolloConfigDB is deployed in each environment, you need to use different packages for config-service and admin-service for different environments with different database connection information, and only one package for portal ##### 2.2.1.2.4 Get the apollo-config-service installation package Located in the `apollo-configservice/target/` directory `apollo-configservice-x.x.x-github.zip` Note that since ApolloConfigDB is deployed in every environment, the config-service for different environments needs to be deployed separately using different packages with different database parameters ##### 2.2.1.2.5 Get apollo-adminservice installation package The `apollo-adminservice-x.x.x-github.zip` located in the `apollo-adminservice/target/` directory Note that since ApolloConfigDB is deployed in each environment, the admin-service for different environments needs to be deployed separately using different packages with different database parameters ##### 2.2.1.2.6 Get apollo-portal installation package `apollo-portal-x.x.x-github.zip` located in the `apollo-portal/target/` directory ### 2.2.2 Deploy Apollo server #### 2.2.2.1 Deploy apollo-configservice Upload the `apollo-configservice-x.x.x-github.zip` of the corresponding environment to the server, decompress it and execute scripts/startup.sh. To stop the service, execute scripts/shutdown.sh. Remember to set a JVM memory according to the actual environment in scripts/startup.sh. The following are our default settings for reference: ```bash export JAVA_OPTS="-server -Xms6144m -Xmx6144m -Xss256k -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=384m -XX:NewSize=4096m -XX:MaxNewSize=4096m -XX:SurvivorRatio=18" ``` > Note 1: If you need to modify the JVM parameters, you can modify the `JAVA_OPTS` section of scripts/startup.sh. > Note 2: To adjust the log output path of the service, you can modify `LOG_DIR` in scripts/startup.sh and apollo-configservice.conf. > Note 3: To adjust the listening port of the service, you can modify the `SERVER_PORT` in scripts/startup.sh. In addition, apollo-configservice also assumes the responsibility of meta server. If you want to modify the port, pay attention to the `eureka.service.url` configuration item in the ApolloConfigDB.ServerConfig table and the meta server information used in apollo-portal and apollo-client. For details, see: [2.2.1.1.2.4 Configuring the meta service information of apollo-portal](en/deployment/distributed-deployment-guide?id=_221124-configuring-apollo-portal39s-meta-service-information) and [1.2.2 Apollo Meta Server](en/client/java-sdk-user-guide?id=_122-apollo-meta-server). > Note 4: If the eureka.service.url of ApolloConfigDB.ServerConfig is only configured with the currently starting machine, the eureka registration failure information will be output in the log during the process of starting apollo-configservice, such as `com.sun.jersey .api.client.ClientHandlerException: java.net.ConnectException: Connection refused`. It should be noted that this is the expected situation, because apollo-configservice needs to register the service with the Meta Server (itself), but because it has not yet woken up during the startup process, it will report this error. The retry action will be performed later, so the registration will be normal after the service is up. > Note 5: Starting from version 2.5.0, apollo-configservice supports graceful shutdown. When the service receives a stop signal, it will wait for in-flight requests to complete before shutting down, with a default timeout of 10 seconds. This feature is enabled via Spring Boot's `server.shutdown=graceful` and `spring.lifecycle.timeout-per-shutdown-phase=${GRACEFUL_SHUTDOWN_TIMEOUT:10s}` configuration. To adjust the timeout, you can set the `GRACEFUL_SHUTDOWN_TIMEOUT` environment variable (e.g., `30s`, `60s`, `2m`) or modify the settings in application.yml. In Kubernetes environments, ensure the Pod's `terminationGracePeriodSeconds` is greater than the configured timeout (recommend at least 10 seconds more). > Note 6: If you read this, I believe that you must be someone who reads the documentation carefully, and you are a little bit closer to success. Keep going, you should be able to complete the distributed deployment of Apollo soon! But do you feel that Apollo's distributed deployment steps are a bit cumbersome? Do you have any advice you would like to share with the author? If the answer is yes, please move to [#1424](https://github.com/apolloconfig/apollo/issues/1424) and look forward to your suggestions! #### 2.2.2.2 Deploy apollo-adminservice Upload the `apollo-adminservice-x.x.x-github.zip` of the corresponding environment to the server, decompress it and execute scripts/startup.sh. To stop the service, execute scripts/shutdown.sh. Remember to set a JVM memory according to the actual environment in scripts/startup.sh. The following are our default settings for reference: ```bash export JAVA_OPTS="-server -Xms2560m -Xmx2560m -Xss256k -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=384m -XX:NewSize=1024m -XX:MaxNewSize=1024m -XX:SurvivorRatio=22" ``` > Note 1: If you need to modify the JVM parameters, you can modify the `JAVA_OPTS` section of scripts/startup.sh. > Note 2: To adjust the log output path of the service, you can modify `LOG_DIR` in scripts/startup.sh and apollo-adminservice.conf. > Note 3: To adjust the listening port of the service, you can modify the `SERVER_PORT` in scripts/startup.sh. > Note 4: Starting from version 2.5.0, apollo-adminservice supports graceful shutdown. When the service receives a stop signal, it will wait for in-flight requests to complete before shutting down, with a default timeout of 10 seconds. This feature is enabled via Spring Boot's `server.shutdown=graceful` and `spring.lifecycle.timeout-per-shutdown-phase=${GRACEFUL_SHUTDOWN_TIMEOUT:10s}` configuration. To adjust the timeout, you can set the `GRACEFUL_SHUTDOWN_TIMEOUT` environment variable (e.g., `30s`, `60s`, `2m`) or modify the settings in application.yml. In Kubernetes environments, ensure the Pod's `terminationGracePeriodSeconds` is greater than the configured timeout (recommend at least 10 seconds more). #### 2.2.2.3 Deploy apollo-portal Upload `apollo-portal-x.x.x-github.zip` to the server, unzip it and execute scripts/startup.sh. To stop the service, execute scripts/shutdown.sh. Remember to set a JVM memory according to the actual environment in startup.sh. The following are our default settings for reference: ```bash export JAVA_OPTS="-server -Xms4096m -Xmx4096m -Xss256k -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=384m -XX:NewSize=1536m -XX:MaxNewSize=1536m -XX:SurvivorRatio=22" ``` > Note 1: If you need to modify the JVM parameters, you can modify the `JAVA_OPTS` section of scripts/startup.sh. > Note 2: To adjust the log output path of the service, you can modify `LOG_DIR` in scripts/startup.sh and apollo-portal.conf. > Note 3: To adjust the listening port of the service, you can modify the `SERVER_PORT` in scripts/startup.sh. ### 2.2.3 Replace built-in eureka with another service registry #### 2.2.3.1 nacos-discovery > For version 1.8.0 and above Enable external nacos service registry to replace built-in eureka > Note: need repackage 1. Modify build.sh/build.bat to change the maven build command for config-service and admin-service to ```shell mvn clean package -Pgithub,nacos-discovery -DskipTests -pl apollo-configservice,apollo-adminservice -am -Dapollo_profile=github,nacos-discovery -Dspring_datasource_url=$apollo_config_db_url -Dspring_datasource_username=$apollo_config_db_username -Dspring_datasource_ password=$apollo_config_db_username password=$apollo_config_db_password ``` 2. Modify the application-github.properties in the config directory of the apollo-config service and apollo-adminservice installation packages, respectively, to configure the nacos server address ```properties nacos.discovery.server-addr=127.0.0.1:8848 # More nacos configurations nacos.discovery.access-key= nacos.discovery.username= nacos.discovery.password= nacos.discovery.secret-key= nacos.discovery.namespace= nacos.discovery.context-path= ``` #### 2.2.3.2 consul-discovery > For version 1.9.0 and above Enable external Consul service registry to replace built-in eureka ##### 2.2.3.2.1 For version 2.1.0 and above 1. Modify `config/application.properties` after decompression of `apollo-configservice-x.x.x-github.zip` and `apollo-adminservice-x.x.x-github.zip`, uncomment ```properties #spring.profiles.active=github,consul-discovery ``` to ```properties spring.profiles.active=github,consul-discovery ``` 2. Modify the application-github.properties in the config directory of the apollo-configservice and apollo-adminservice installation packages, respectively, to configure the consul server address ```properties spring.cloud.consul.host=127.0.0.1 spring.cloud.consul.port=8500 ``` ##### 2.2.3.2.2 For version 2.1.0 below 1. Modify build.sh/build.bat to change the maven build command for config-service and admin-service to ```shell mvn clean package -Pgithub -DskipTests -pl apollo-configservice,apollo-adminservice -am -Dapollo_profile=github,consul-discovery -Dspring_datasource_url=$apollo_config_db_url -Dspring_datasource_username=$apollo_config_db_username -Dspring_datasource_password=$apollo_config_db_password ``` 2. Modify the application-github.properties in the config directory of the apollo-configservice and apollo-adminservice installation packages, respectively, to configure the consul server address ```properties spring.cloud.consul.host=127.0.0.1 spring.cloud.consul.port=8500 ``` #### 2.2.3.3 zookeeper-discovery > For version 2.0.0 and above Enable external Zookeeper service registry to replace built-in eureka ##### 2.2.3.3.1 For version 2.1.0 and above 1. Modify `config/application.properties` after decompression of `apollo-configservice-x.x.x-github.zip` and `apollo-adminservice-x.x.x-github.zip`, uncomment ```properties #spring.profiles.active=github,zookeeper-discovery ``` to ```properties spring.profiles.active=github,zookeeper-discovery ``` 2. Modify the application-github.properties in the config directory of the apollo-config service and apollo-adminservice installation packages, respectively, to configure the zookeeper server address ```properties spring.cloud.zookeeper.connect-string=127.0.0.1:2181 ``` 3. Zookeeper version description * Support Zookeeper 3.5.x or higher; * If apollo-configservice application starts reporting port occupation, please check the following configuration of Zookeeper; > Note: Zookeeper 3.5.0 added a built-in [AdminServer](https://zookeeper.apache.org/doc/r3.5.0-alpha/zookeeperAdmin.html#sc_adminserver_config) ```properties admin.enableServer admin.serverPort ``` ##### 2.2.3.3.2 For version 2.1.0 below 1. Modify build.sh/build.bat to change the maven build command for ``config-service`` and ``admin-service`` to ```shell mvn clean package -Pgithub -DskipTests -pl apollo-configservice,apollo-adminservice -am -Dapollo_profile=github,zookeeper-discovery - Dspring_datasource_url=$apollo_config_db_url -Dspring_datasource_username=$apollo_config_db_username -Dspring_datasource_password=$apollo_config_db_password ``` 2. Modify the application-github.properties in the config directory of the apollo-config service and apollo-adminservice installation packages, respectively, to configure the zookeeper server address ```properties spring.cloud.zookeeper.connect-string=127.0.0.1:2181 ``` 3. Zookeeper version description * Support Zookeeper 3.5.x or higher; * If apollo-configservice application starts reporting port occupation, please check the following configuration of Zookeeper; > Note: Zookeeper 3.5.0 added a built-in [AdminServer](https://zookeeper.apache.org/doc/r3.5.0-alpha/zookeeperAdmin.html#sc_adminserver_config) ```properties admin.enableServer admin.serverPort ``` #### 2.2.3.4 custom-defined-discovery > For version 2.0.0 and above Enable custom-defined-discovery to replace built-in eureka ##### 2.2.3.4.1 For version 2.1.0 and above 1. Modify `config/application.properties` after decompression of `apollo-configservice-x.x.x-github.zip` and `apollo-adminservice-x.x.x-github.zip`, uncomment ```properties #spring.profiles.active=github,custom-defined-discovery ``` to ```properties spring.profiles.active=github,custom-defined-discovery ``` 2. There are two ways to configure the access addresses of the custom config-service and admin-service: one is to write two pieces of data in the mysql database ApolloConfigDB and the table ServerConfig. ```sql INSERT INTO `ApolloConfigDB`.`ServerConfig` (`Key`, `Value`, `Comment`) VALUES ('apollo.config-service.url', 'http://apollo-config-service', 'ConfigService access address '); INSERT INTO `ApolloConfigDB`.`ServerConfig` (`Key`, `Value`, `Comment`) VALUES ('apollo.admin-service.url', 'http://apollo-admin-service', 'AdminService access address '); ``` Another way to modify application-github.properties in the config directory of the apollo-configservice installation package ```properties apollo.config-service.url=http://apollo-config-service apollo.admin-service.url=http://apollo-admin-service ``` ##### 2.2.3.4.2 For version 2.1.0 below 1. Modify build.sh/build.bat and change the maven compilation commands of `config-service` and `admin-service` to ```shell mvn clean package -Pgithub -DskipTests -pl apollo-configservice,apollo-adminservice -am -Dapollo_profile=github,custom-defined-discovery -Dspring_datasource_url=$apollo_config_db_url -Dspring_datasource_username=$apollo_config_db_username -Dspring_datasource_password=$apollo_config_db_password ``` 2. There are two ways to configure the access addresses of the custom config-service and admin-service: one is to write two pieces of data in the mysql database ApolloConfigDB and the table ServerConfig. ```sql INSERT INTO `ApolloConfigDB`.`ServerConfig` (`Key`, `Value`, `Comment`) VALUES ('apollo.config-service.url', 'http://apollo-config-service', 'ConfigService access address '); INSERT INTO `ApolloConfigDB`.`ServerConfig` (`Key`, `Value`, `Comment`) VALUES ('apollo.admin-service.url', 'http://apollo-admin-service', 'AdminService access address '); ``` Another way to modify application-github.properties in the config directory of the apollo-configservice installation package ```properties apollo.config-service.url=http://apollo-config-service apollo.admin-service.url=http://apollo-admin-service ``` #### 2.2.3.5 database-discovery > For version 2.1.0 and above Enable database-discovery to replace built-in eureka Apollo supports the use of internal database table as registry, without relying on third-party registry. 1. Modify `config/application.properties` after decompression of `apollo-configservice-x.x.x-github.zip` and `apollo-adminservice-x.x.x-github.zip`, uncomment ```properties #spring.profiles.active=github,database-discovery ``` to ```properties spring.profiles.active=github,database-discovery ``` 2. (optional) In multi-cluster deployments, if you want apollo client only read Config Service in the same cluster, you can add a property in `config/application-github.properties` of the Config Service and Admin Service installation package ```properties apollo.service.registry.cluster=same name with apollo Cluster ``` 2. (optional) If you want to customize Config Service and Admin Service's uri for Client, for example when deploying on the intranet, if you don't want to expose the intranet ip, you can add a property in `config/application-github.properties` of the Config Service and Admin Service installation package ```properties apollo.service.registry.uri=http://your-ip-or-domain:${server.port}/ ``` ## 2.3 Docker Deployment ### 2.3.1 Version 1.7.0 and above Apollo version 1.7.0 starts uploading Docker images to [Docker Hub](https://hub.docker.com/u/apolloconfig) by default, which can be obtained by following these steps #### 2.3.1.1 Apollo Config Service ##### 2.3.1.1.1 Get the image ```bash docker pull apolloconfig/apollo-config service:${version} ``` ##### 2.3.1.1.2 Run the image Example: ```bash docker run -p 8080:8080 \ -e SPRING_DATASOURCE_URL="jdbc:mysql://fill-in-the-correct-server:3306/ApolloConfigDB?characterEncoding=utf8" \ -e SPRING_DATASOURCE_USERNAME=FillInCorrectUser -e SPRING_DATASOURCE_PASSWORD=FillInCorrectPassword \ -d -v /tmp/logs:/opt/logs --name apollo-configservice apolloconfig/apollo-configservice:${version} ``` Parameter description. * `SPRING_DATASOURCE_URL`: Address of the corresponding environment ApolloConfigDB * `SPRING_DATASOURCE_USERNAME`: The user name of the corresponding environment ApolloConfigDB * `SPRING_DATASOURCE_PASSWORD`: password of the corresponding environment ApolloConfigDB #### 2.3.1.2 Apollo Admin Service ##### 2.3.1.2.1 Getting the image ```bash docker pull apolloconfig/apollo-adminservice:${version} ``` ##### 2.3.1.2.2 Running the image Example: ```bash docker run -p 8090:8090 \ -e SPRING_DATASOURCE_URL="jdbc:mysql://fill-in-the-correct-server:3306/ApolloConfigDB?characterEncoding=utf8" \ -e SPRING_DATASOURCE_USERNAME=FillInCorrectUser -e SPRING_DATASOURCE_PASSWORD=FillInCorrectPassword \ -d -v /tmp/logs:/opt/logs --name apollo-adminservice apolloconfig/apollo-adminservice:${version} ``` Parameter description. * `SPRING_DATASOURCE_URL`: Address of the corresponding environment ApolloConfigDB * `SPRING_DATASOURCE_USERNAME`: The user name of the corresponding environment ApolloConfigDB * `SPRING_DATASOURCE_PASSWORD`: password of the corresponding environment ApolloConfigDB #### 2.3.1.3 Apollo Portal ##### 2.3.1.3.1 Getting the image ```bash docker pull apolloconfig/apollo-portal:${version} ``` ##### 2.3.1.3.2 Running the image Example: ```bash docker run -p 8070:8070 \ -e SPRING_DATASOURCE_URL="jdbc:mysql://fill-in-the-correct-server:3306/ApolloPortalDB?characterEncoding=utf8" \ -e SPRING_DATASOURCE_USERNAME=FillInCorrectUser -e SPRING_DATASOURCE_PASSWORD=FillInCorrectPassword \ -e APOLLO_PORTAL_ENVS=dev,pro \ -e DEV_META=http://fill-in-dev-meta-server:8080 -e PRO_META=http://fill-in-pro-meta-server:8080 \ -d -v /tmp/logs:/opt/logs --name apollo-portal apolloconfig/apollo-portal:${version} ``` Parameter description: * `SPRING_DATASOURCE_URL`: Address of the corresponding environment ApolloPortalDB * `SPRING_DATASOURCE_USERNAME`: The username of the corresponding environment ApolloPortalDB * `SPRING_DATASOURCE_PASSWORD`: The password of the corresponding environment ApolloPortalDB * `APOLLO_PORTAL_ENVS` (optional): corresponds to the [apollo.portal.envs](en/deployment/distributed-deployment-guide?id=_311-apolloportalenvs-list-of-supportable-environments) configuration item in ApolloPortalDB, which can be configured by this environment parameter if it is not configured in the database. * `DEV_META/PRO_META`(optional): Configure the Meta Service address of the corresponding environment, named by `${ENV}_META`, it should be noted that if you configure [apollo.portal.meta.servers](en/deployment/distributed-deployment-guide?id=_312-apolloportalmetaservers-list-of-meta-service-for-each-environment) configuration, then the configuration in apollo.portal.meta.servers prevails. #### 2.3.1.4 Building a Docker image from source If you have modified the apollo server code and wish to build a Docker image from source, you can refer to the following steps. 1. Build the installation package from source: `./scripts/build.sh` 2. 2. build the Docker image: `mvn docker:build -pl apollo-configservice,apollo-adminservice,apollo-portal` ### 2.3.2 Versions before 1.7.0 Apollo project already comes with Docker file, you can refer to [2.2.1 Get installer](#_221-Get-the-installation-package) to configure the installer and then hit the Docker image with the following file. 1. [apollo-configservice](https://github.com/apolloconfig/apollo/blob/master/apollo-configservice/src/main/docker/Dockerfile) 2. [apollo-adminservice](https://github.com/apolloconfig/apollo/blob/master/apollo-adminservice/src/main/docker/Dockerfile) 3. [apollo-portal](https://github.com/apolloconfig/apollo/blob/master/apollo-portal/src/main/docker/Dockerfile) See also the [docker-apollo](https://github.com/kulovecc/docker-apollo) project by Apollo user [@kulovecc](https://github.com/kulovecc) and [@idoop](https://github.com/idoop) for the [docker-apollo](https://github.com/idoop/docker-apollo) project. ## 2.4 Kubernetes Deployment ### 2.4.1 Kubernetes-based native service discovery Apollo version 1.7.0 adds a deployment model based on Kubernetes native service discovery, which greatly simplifies the overall deployment as it no longer uses the built-in Eureka, and also provides Helm Charts for easy deployment. > More design notes can be found in [#3054](https://github.com/apolloconfig/apollo/issues/3054). #### 2.4.1.1 Environment requirements - Kubernetes 1.10+ - Helm 3 #### 2.4.1.2 Adding Apollo Helm Chart repository ```bash $ helm repo add apollo https://charts.apolloconfig.com $ helm search repo apollo ``` #### 2.4.1.3 Deploying apollo-configservice and apollo-adminservice ##### 2.4.1.3.1 Installing apollo-configservice and apollo-adminservice You need to install apollo-configservice and apollo-adminservice in each environment, so it is recommended to include the environment information in the release name, e.g. `apollo-service-dev` ```bash $ helm install apollo-service-dev \ --set configdb.host=1.2.3.4 \ --set configdb.userName=apollo \ --set configdb.password=apollo \ --set configdb.service.enabled=true \ --set configService.replicaCount=1 \ --set adminService.replicaCount=1 \ -n your-namespace \ apollo/apollo-service ``` The general deployment recommendation is to configure via values.yaml ```bash $ helm install apollo-service-dev -f values.yaml -n your-namespace apollo/apollo-service ``` After installation, you will be prompted for the Meta Server address of the corresponding environment, which needs to be recorded. apollo-portal needs to be installed with. ```bash Get meta service url for current release by running these commands: echo http://apollo-service-dev-apollo-configservice:8080 ``` > See [2.4.1.3.3 Configuration Notes](#_24133-Configuration-Notes) for more configuration notes ##### 2.4.1.3.2 Uninstalling apollo-configservice and apollo-adminservice For example to uninstall the `apollo-service-dev` deployment. ```bash $ helm uninstall -n your-namespace apollo-service-dev ``` ##### 2.4.1.3.3 Configuration Notes The following table lists the configurable parameters of the apollo-service-chart and their default values. | Parameter | Description | Default | | ----------------------------------------------- | ------------------------------------------------------------ | ----------------------------------- | | `configdb.host` | The host for apollo config db | `nil` | | `configdb.port` | The port for apollo config db | `3306` | | `configdb.dbName` | The database name for apollo config db | `ApolloConfigDB` | | `configdb.userName` | The user name for apollo config db | `nil` | | `configdb.password` | The password for apollo config db | `nil` | | `configdb.connectionStringProperties` | The connection string properties for apollo config db | `characterEncoding=utf8` | | `configdb.service.enabled` | Whether to create a Kubernetes Service for `configdb.host` or not. Set it to `true` if `configdb.host` is an endpoint outside of the kubernetes cluster | `false` | | `configdb.service.fullNameOverride` | Override the service name for apollo config db | `nil` | | `configdb.service.port` | The port for the service of apollo config db | `3306` | | `configdb.service.type` | The service type of apollo config db: `ClusterIP` or `ExternalName`. If the host is a DNS name, please specify `ExternalName` as the service type, e.g. `xxx.mysql.rds.aliyuncs.com` | `ClusterIP` | | `configService.fullNameOverride` | Override the deployment name for apollo-configservice | `nil` | | `configService.replicaCount` | Replica count of apollo-configservice | `2` | | `configService.containerPort` | Container port of apollo-configservice | `8080` | | `configService.image.repository` | Image repository of apollo-configservice | `apolloconfig/apollo-configservice` | | `configService.image.tag` | Image tag of apollo-configservice, e.g. `1.8.0`, leave it to `nil` to use the default version. _(chart version >= 0.2.0)_ | `nil` | | `configService.image.pullPolicy` | Image pull policy of apollo-configservice | `IfNotPresent` | | `configService.imagePullSecrets` | Image pull secrets of apollo-configservice | `[]` | | `configService.service.fullNameOverride` | Override the service name for apollo-configservice | `nil` | | `configService.service.annotations` | The annotations of the service for apollo-configservice. _(chart version >= 0.9.0)_ | `{}` | | `configService.service.port` | The port for the service of apollo-configservice | `8080` | | `configService.service.targetPort` | The target port for the service of apollo-configservice | `8080` | | `configService.service.type` | The service type of apollo-configservice | `ClusterIP` | | `configService.ingress.enabled` | Whether to enable the ingress for config-service or not. _(chart version >= 0.2.0)_ | `false` | | `configService.ingress.annotations` | The annotations of the ingress for config-service. _(chart version >= 0.2.0)_ | `{}` | | `configService.ingress.hosts.host` | The host of the ingress for config-service. _(chart version >= 0.2.0)_ | `nil` | | `configService.ingress.hosts.paths` | The paths of the ingress for config-service. _(chart version >= 0.2.0)_ | `[]` | | `configService.ingress.tls` | The tls definition of the ingress for config-service. _(chart version >= 0.2.0)_ | `[]` | | `configService.liveness.initialDelaySeconds` | The initial delay seconds of liveness probe | `100` | | `configService.liveness.periodSeconds` | The period seconds of liveness probe | `10` | | `configService.readiness.initialDelaySeconds` | The initial delay seconds of readiness probe | `30` | | `configService.readiness.periodSeconds` | The period seconds of readiness probe | `5` | | `configService.config.profiles` | specify the spring profiles to activate | `github,kubernetes` | | `configService.config.configServiceUrlOverride` | Override `apollo.config-service.url`: config service url to be accessed by apollo-client, e.g. `http://apollo-config-service-dev:8080` | `nil` | | `configService.config.adminServiceUrlOverride` | Override `apollo.admin-service.url`: admin service url to be accessed by apollo-portal, e.g. `http://apollo-admin-service-dev:8090` | `nil` | | `configService.config.contextPath` | specify the context path, e.g. `/apollo`, then users could access config service via `http://{config_service_address}/apollo`. _(chart version >= 0.2.0)_ | `nil` | | `configService.env` | Environment variables passed to the container, e.g.
    `JAVA_OPTS: -Xss256k` | `{}` | | `configService.strategy` | The deployment strategy of apollo-configservice | `{}` | | `configService.resources` | The resources definition of apollo-configservice | `{}` | | `configService.nodeSelector` | The node selector definition of apollo-configservice | `{}` | | `configService.tolerations` | The tolerations definition of apollo-configservice | `[]` | | `configService.affinity` | The affinity definition of apollo-configservice | `{}` | | `adminService.fullNameOverride` | Override the deployment name for apollo-adminservice | `nil` | | `adminService.replicaCount` | Replica count of apollo-adminservice | `2` | | `adminService.containerPort` | Container port of apollo-adminservice | `8090` | | `adminService.image.repository` | Image repository of apollo-adminservice | `apolloconfig/apollo-adminservice` | | `adminService.image.tag` | Image tag of apollo-adminservice, e.g. `1.8.0`, leave it to `nil` to use the default version. _(chart version >= 0.2.0)_ | `nil` | | `adminService.image.pullPolicy` | Image pull policy of apollo-adminservice | `IfNotPresent` | | `adminService.imagePullSecrets` | Image pull secrets of apollo-adminservice | `[]` | | `adminService.service.fullNameOverride` | Override the service name for apollo-adminservice | `nil` | | `adminService.service.annotations` | The annotations of the service for apollo-adminservice. _(chart version >= 0.9.0)_ | `{}` | | `adminService.service.port` | The port for the service of apollo-adminservice | `8090` | | `adminService.service.targetPort` | The target port for the service of apollo-adminservice | `8090` | | `adminService.service.type` | The service type of apollo-adminservice | `ClusterIP` | | `adminService.ingress.enabled` | Whether to enable the ingress for admin-service or not. _(chart version >= 0.2.0)_ | `false` | | `adminService.ingress.annotations` | The annotations of the ingress for admin-service. _(chart version >= 0.2.0)_ | `{}` | | `adminService.ingress.hosts.host` | The host of the ingress for admin-service. _(chart version >= 0.2.0)_ | `nil` | | `adminService.ingress.hosts.paths` | The paths of the ingress for admin-service. _(chart version >= 0.2.0)_ | `[]` | | `adminService.ingress.tls` | The tls definition of the ingress for admin-service. _(chart version >= 0.2.0)_ | `[]` | | `adminService.liveness.initialDelaySeconds` | The initial delay seconds of liveness probe | `100` | | `adminService.liveness.periodSeconds` | The period seconds of liveness probe | `10` | | `adminService.readiness.initialDelaySeconds` | The initial delay seconds of readiness probe | `30` | | `adminService.readiness.periodSeconds` | The period seconds of readiness probe | `5` | | `adminService.config.profiles` | specify the spring profiles to activate | `github,kubernetes` | | `adminService.config.contextPath` | specify the context path, e.g. `/apollo`, then users could access admin service via `http://{admin_service_address}/apollo`. _(chart version >= 0.2.0)_ | `nil` | | `adminService.env` | Environment variables passed to the container, e.g.
    `JAVA_OPTS: -Xss256k` | `{}` | | `adminService.strategy` | The deployment strategy of apollo-adminservice | `{}` | | `adminService.resources` | The resources definition of apollo-adminservice | `{}` | | `adminService.nodeSelector` | The node selector definition of apollo-adminservice | `{}` | | `adminService.tolerations` | The tolerations definition of apollo-adminservice | `[]` | | `adminService.affinity` | The affinity definition of apollo-adminservice | `{}` | ##### 2.4.1.3.4 Configuration example ###### 2.4.1.3.4.1 The host of ConfigDB is the IP outside the k8s cluster ```yaml configdb: host: 1.2.3.4 dbName: ApolloConfigDBName userName: someUserName password: somePassword connectionStringProperties: characterEncoding=utf8&useSSL=false service: enabled: true ``` ###### 2.4.1.3.4.2 The host of ConfigDB is the domain name outside the k8s cluster ```yaml configdb: host: xxx.mysql.rds.aliyuncs.com dbName: ApolloConfigDBName userName: someUserName password: somePassword connectionStringProperties: characterEncoding=utf8&useSSL=false service: enabled: true type: ExternalName ``` ###### 2.4.1.3.4.3 The host of ConfigDB is a service in the k8s cluster ```yaml configdb: host: apollodb-mysql.mysql dbName: ApolloConfigDBName userName: someUserName password: somePassword connectionStringProperties: characterEncoding=utf8&useSSL=false ``` ###### 2.4.1.3.4.4 Specify the apollo-configservice address returned by Meta Server If apollo-client cannot directly access the service of apollo-configservice (for example, it is not in the same k8s cluster), you can refer to the following example to specify the address returned by Meta Server to apollo-client (for example, it can be accessed through nodeport) ```yaml configService: config: configServiceUrlOverride: http://1.2.3.4:12345 ``` ###### 2.4.1.3.4.5 Specify the apollo-adminservice address returned by Meta Server If apollo-portal cannot directly access the service of apollo-adminservice (for example, it is not in the same k8s cluster), you can refer to the following example to specify the address returned by Meta Server to apollo-portal (for example, it can be accessed through nodeport) ```yaml configService: config: adminServiceUrlOverride: http://1.2.3.4:23456 ``` ###### 2.4.1.3.4.6 Expose apollo-configservice service in the form of Ingress configuration custom path `/config` ```yaml # use /config as root, should specify configService.config.contextPath as /config configService: config: contextPath: /config ingress: enabled: true hosts: - paths: - /config ``` ###### 2.4.1.3.4.7 Expose apollo-adminservice service in the form of Ingress configuration custom path `/admin` ```yaml # use /admin as root, should specify adminService.config.contextPath as /admin adminService: config: contextPath: /admin ingress: enabled: true hosts: - paths: - /admin ``` #### 2.4.1.4 Deploy apollo-portal ##### 2.4.1.4.1 Install apollo-portal Suppose there are dev and pro environments, and the meta server addresses are `http://apollo-service-dev-apollo-configservice:8080` and `http://apollo-service-pro-apollo-configservice:8080` respectively : ```bash $ helm install apollo-portal \ --set portaldb.host=1.2.3.4 \ --set portaldb.userName=apollo \ --set portaldb.password=apollo \ --set portaldb.service.enabled=true \ --set config.envs="dev\,pro" \ --set config.metaServers.dev=http://apollo-service-dev-apollo-configservice:8080 \ --set config.metaServers.pro=http://apollo-service-pro-apollo-configservice:8080 \ --set replicaCount=1 \ -n your-namespace \ apollo/apollo-portal ``` General deployment recommendations are configured through values.yaml: ```bash $ helm install apollo-portal -f values.yaml -n your-namespace apollo/apollo-portal ``` > For more configuration item descriptions, please refer to [2.4.1.4.3 Configuration item description]( ##### 2.4.1.4.2 Uninstalling apollo-portal For example, to uninstall an `apollo-portal` deployment. ```bash $ helm uninstall -n your-namespace apollo-portal ``` ##### 2.4.1.4.3 Description of configuration items The following table lists the configurable parameters of the apollo-portal chart and their default values. | Parameter | Description | Default | | ------------------------------------- | ------------------------------------------------------------ | ---------------------------- | | `fullNameOverride` | Override the deployment name for apollo-portal | `nil` | | `replicaCount` | Replica count of apollo-portal | `2` | | `containerPort` | Container port of apollo-portal | `8070` | | `image.repository` | Image repository of apollo-portal | `apolloconfig/apollo-portal` | | `image.tag` | Image tag of apollo-portal, e.g. `1.8.0`, leave it to `nil` to use the default version. _(chart version >= 0.2.0)_ | `nil` | | `image.pullPolicy` | Image pull policy of apollo-portal | `IfNotPresent` | | `imagePullSecrets` | Image pull secrets of apollo-portal | `[]` | | `service.fullNameOverride` | Override the service name for apollo-portal | `nil` | | `service.annotations` | The annotations of the service for apollo-portal. _(chart version >= 0.9.0)_ | `{}` | | `service.port` | The port for the service of apollo-portal | `8070` | | `service.targetPort` | The target port for the service of apollo-portal | `8070` | | `service.type` | The service type of apollo-portal | `ClusterIP` | | `service.sessionAffinity` | The session affinity for the service of apollo-portal | `ClientIP` | | `ingress.enabled` | Whether to enable the ingress or not | `false` | | `ingress.annotations` | The annotations of the ingress | `{}` | | `ingress.hosts.host` | The host of the ingress | `nil` | | `ingress.hosts.paths` | The paths of the ingress | `[]` | | `ingress.tls` | The tls definition of the ingress | `[]` | | `liveness.initialDelaySeconds` | The initial delay seconds of liveness probe | `100` | | `liveness.periodSeconds` | The period seconds of liveness probe | `10` | | `readiness.initialDelaySeconds` | The initial delay seconds of readiness probe | `30` | | `readiness.periodSeconds` | The period seconds of readiness probe | `5` | | `env` | Environment variables passed to the container, e.g.
    `JAVA_OPTS: -Xss256k` | `{}` | | `strategy` | The deployment strategy of apollo-portal | `{}` | | `resources` | The resources definition of apollo-portal | `{}` | | `nodeSelector` | The node selector definition of apollo-portal | `{}` | | `tolerations` | The tolerations definition of apollo-portal | `[]` | | `affinity` | The affinity definition of apollo-portal | `{}` | | `config.profiles` | specify the spring profiles to activate | `github,auth` | | `config.envs` | specify the env names, e.g. `dev,pro` | `nil` | | `config.contextPath` | specify the context path, e.g. `/apollo`, then users could access portal via `http://{portal_address}/apollo` | `nil` | | `config.metaServers` | specify the meta servers, e.g.
    `dev: http://apollo-configservice-dev:8080`
    `pro: http://apollo-configservice-pro:8080` | `{}` | | `config.files` | specify the extra config files for apollo-portal, e.g. `application-ldap.yml` | `{}` | | `portaldb.host` | The host for apollo portal db | `nil` | | `portaldb.port` | The port for apollo portal db | `3306` | | `portaldb.dbName` | The database name for apollo portal db | `ApolloPortalDB` | | `portaldb.userName` | The user name for apollo portal db | `nil` | | `portaldb.password` | The password for apollo portal db | `nil` | | `portaldb.connectionStringProperties` | The connection string properties for apollo portal db | `characterEncoding=utf8` | | `portaldb.service.enabled` | Whether to create a Kubernetes Service for `portaldb.host` or not. Set it to `true` if `portaldb.host` is an endpoint outside of the kubernetes cluster | `false` | | `portaldb.service.fullNameOverride` | Override the service name for apollo portal db | `nil` | | `portaldb.service.port` | The port for the service of apollo portal db | `3306` | | `portaldb.service.type` | The service type of apollo portal db: `ClusterIP` or `ExternalName`. If the host is a DNS name, please specify `ExternalName` as the service type, e.g. `xxx.mysql.rds.aliyuncs.com` | `ClusterIP` | ##### 2.4.1.4.4 Configuration example ###### 2.4.1.4.4.1 The host of PortalDB is the IP outside the k8s cluster ```yaml portaldb: host: 1.2.3.4 dbName: ApolloPortalDBName userName: someUserName password: somePassword connectionStringProperties: characterEncoding=utf8&useSSL=false service: enabled: true ``` ###### 2.4.1.4.4.2 The host of PortalDB is the domain name outside the k8s cluster ```yaml portaldb: host: xxx.mysql.rds.aliyuncs.com dbName: ApolloPortalDBName userName: someUserName password: somePassword connectionStringProperties: characterEncoding=utf8&useSSL=false service: enabled: true type: ExternalName ``` ###### 2.4.1.4.4.3 The host of PortalDB is a service in the k8s cluster ```yaml portaldb: host: apollodb-mysql.mysql dbName: ApolloPortalDBName userName: someUserName password: somePassword connectionStringProperties: characterEncoding=utf8&useSSL=false ``` ###### 2.4.1.4.4.4 Configure environment information ```yaml config: envs: dev, pro metaServers: dev: http://apollo-service-dev-apollo-configservice:8080 pro: http://apollo-service-pro-apollo-configservice:8080 ``` ###### 2.4.1.4.4.5 Expose services as Load Balancer ```yaml service: type: LoadBalancer ``` ###### 2.4.1.4.4.6 Expose services as Ingress ```yaml ingress: enabled: true hosts: - paths: - / ``` ###### 2.4.1.4.4.7 Expose services in the form of Ingress configuration custom path `/apollo` ```yaml # use /apollo as root, should specify config.contextPath as /apollo ingress: enabled: true hosts: - paths: - /apollo config: ... contextPath: /apollo ... ``` ###### 2.4.1.4.4.8 Expose services in the form of Ingress configuration session affinity ```yaml ingress: enabled: true annotations: kubernetes.io/ingress.class: nginx nginx.ingress.kubernetes.io/affinity: "cookie" nginx.ingress.kubernetes.io/affinity-mode: "persistent" nginx.ingress.kubernetes.io/session-cookie-conditional-samesite-none: "true" nginx.ingress.kubernetes.io/session-cookie-expires: "172800" nginx.ingress.kubernetes.io/session-cookie-max-age: "172800" hosts: - host: xxx.somedomain.com # host is required to make session affinity work paths: - / ``` ###### 2.4.1.4.4.9 Enable LDAP support ```yaml config: ... profiles: github,ldap ... files: application-ldap.yml: | spring: ldap: base: "dc=example,dc=org" username: "cn=admin,dc=example,dc=org" password: "password" search-filter: "(uid={0})" urls: - "ldap://xxx.somedomain.com:389" ldap: mapping: object-class: "inetOrgPerson" login-id: "uid" user-display-name: "cn" email: "mail" ``` #### 2.4.1.5 Building a Docker image from source If you have modified the code of the apollo server and want to build a Docker image from source code, you can refer to the steps in [2.3.1.4 Building a Docker Image from Source Code](#_2314-Building-a-Docker-image-from-source). ### 2.4.2 Based on the built-in Eureka service discovery Thanks to [AiotCEO](https://github.com/AiotCEO) for providing k8s deployment support, please refer to [apollo-on-kubernetes](https://github.com/apolloconfig/apollo-on-kubernetes). Thanks to [qct](https://github.com/qct) for Helm Chart deployment support, please refer to [qct/apollo-helm](https://github.com/qct/apollo-helm) for usage instructions. # III. Server-side configuration instructions > The following configurations are supported not only in the database, but also through -D parameters, application.properties, etc., and -D parameters, application.properties, etc. have higher priority than the configuration in the database ## 3.1 Adjusting ApolloPortalDB configuration Configuration items are uniformly stored in ApolloPortalDB.ServerConfig table, and can also be configured through `Administrator Tools - System Parameters` page, without special instructions, the modification will take effect in real time after one minute. ### 3.1.1 apollo.portal.envs - list of supportable environments The default value is dev, if portal needs to manage multiple environments, just separate them by commas (case insensitive), e.g. ``` DEV,FAT,UAT,PRO ``` After the modification needs to reboot to take effect. >Note 1: A set of Portal can manage multiple environments, but each environment needs to deploy a separate set of Config Service, Admin Service and ApolloConfigDB, please refer to: [2.1.2 Creating ApolloConfigDB](en/deployment/distributed-deployment-guide?id=_212-creating-apolloconfigdb), [3.2 Adjusting ApolloConfigDB configuration](en/deployment/distributed-deployment-guide?id=_32-adjusting-apolloconfigdb-configuration), [2.2.1.1.2 Configuring database connection information](en/deployment/distributed-deployment-guide?id=_22112-configuring-database-connection-information), and if you are adding an environment to Apollo Configuration Center that has been running for a while, don't forget to refer to [2.1.2.4 Importing ApolloConfigDB project data from another environment](en/deployment/distributed-deployment-guide?id=_2124-importing-apolloconfigdb-project-data-from-another-environment) to do the initialization of the new environment. >Note 2: Adding the environment to the database only does not work, you also need to add the meta server address corresponding to the new environment for apollo-portal, refer to: [2.2.1.1.2.4 Configuring the meta service information of apollo-portal](en/deployment/distributed-deployment-guide?id=_221124-configuring-apollo-portal39s-meta-service-information). portal's meta-service information). apollo-client also needs to be configured accordingly when used in a new environment, refer to: [1.2.2 Apollo Meta Server](en/client/java-sdk-user-guide?id=_122-apollo-meta-server). >Note 3: If you wish to add a custom environment name, you can refer to [Portal How to add environment](en/faq/common-issues-in-deployment-and-development-phase?id=_4-how-to-add-environment-by-portal-console) . >Note 4: Version 1.1.0 added the system information page (`Administrator Tools` -> `System Information`), you can check whether the configuration is correct through this page ### 3.1.2 apollo.portal.meta.servers - List of Meta Service for each environment > For version 1.6.0 and above Apollo Portal needs to access different meta service (apollo-configservice) addresses in different environments, so we need to provide this information in the configuration. By default, the meta service and config service are deployed in the same JVM process, so the address of the meta service is the address of the config service. Sample example is as follows. ```json { "DEV": "http://1.1.1.1:8080", "FAT": "http://apollo.fat.xxx.com", "UAT": "http://apollo.uat.xxx.com", "PRO": "http://apollo.xxx.com" } ``` A reboot is required to take effect after the modification. > This configuration has a higher priority than the Meta Service address set in other ways. For more information, please refer to [2.2.1.1.2.4 Configuring meta service information for apollo-portal](en/deployment/distributed-deployment-guide?id=_221124-configuring-apollo-portal39s-meta-service-information). ### 3.1.3 Organizations - Department list All new apps in the Portal need to select departments, so you need to configure optional department information here, sample example is as follows ```json [{"orgId": "TEST1", "orgName": "Sample Department 1"},{"orgId": "TEST2", "orgName": "Sample Department 2"}] ``` ### 3.1.4 superAdmin - Portal super administrator The superAdmin has all privileges and needs to be set up carefully. If you don't have access to your company's SSO system, you can use the default value apollo (default user) for now. When you have access to it, change it to the actual account used, with multiple accounts separated by English commas (,). ### 3.1.5 consumer.token.salt - consumer token salt You can set a token salt if you will use the open platform API, but ignore it if you don't. ### 3.1.6 wiki.address The address of the "help" link on the portal, the default is the Apollo github wiki home page, you can set it yourself. ### 3.1.7 admin.createPrivateNamespace.switch Whether to allow project admins to create private namespace. set to `true` to allow creation, set to `false` to prevent project admins from seeing the option to create private namespace on the page. [Learn more about Namespace](en/design/apollo-core-concept-namespace) ### 3.1.8 emergencyPublish.supported.envs Configure a list of environments that allow emergency publishing, with multiple envs separated by commas. When the config service turns on the only one person can modify switch (`namespace.lock.switch`) for one publish, only one person can modify and another publish a configuration publish. In order to avoid not being able to publish the configuration in case of emergency (e.g. out-of-hours, holidays), you can configure this to allow some environments to operate emergency publishing, i.e. the same person can modify and publish the configuration. ### 3.1.9 configView.memberOnly.envs A list of environments that display configuration information only to project members, with multiple envs separated by commas. For environments that are set to display configuration information only to project members, only the administrator of the project or users with edit or publish privileges for that namespace can see the configuration information and publish history for that private namespace. Public namespaces are always visible to all users. > Supported since version 1.1.0, see [PR 1531](https://github.com/apolloconfig/apollo/pull/1531) ### 3.1.10 role.create-application.enabled - whether to enable control over creating project permissions > For versions 1.5.0 and above Default is false, all users can create projects If set to true, only super administrators and accounts with create-application privileges can create projects. Super administrators can assign project creation privileges to users via `Administrator Tools - System Rights Management`. ### 3.1.11 role.manage-app-master.enabled - Enables or disables the project administrator to assign privileges > For versions 1.5.0 and above Default is false, all project administrators can add/remove administrators for the project If set to true, only super administrators and accounts with project administrator assignment privileges can add/remove administrators for a specific project, and super administrators can assign project-specific administrator assignment privileges to users via `Administrator Tools - System Rights Management ### 3.1.12 admin-services.access.tokens - set the access token required by apollo-portal to access the apollo-adminservice for each environment > for version 1.7.1 and above If the corresponding environment apollo-adminservice has [access control enabled](en/deployment/distributed-deployment-guide?id=_326-admin-servicesaccesscontrolenabled-configure-whether-apollo-adminservice-has-access-control-enabled), then you need to configure apollo-portal access here access token required for this environment apollo-adminservice, otherwise access will fail . The format is json, as follows. ```json { "dev" : "098f6bcd4621d373cade4e832627b4f6", "pro" : "ad0234829205b9033196ba818f7a872b" } ``` ### 3.1.13 searchByItem.switch - whether the console search box supports searching by configuration item The default is true, which makes it easy to quickly search for configurations by configuration item If set to false, this feature is disabled ### 3.1.14 apollo.portal.search.perEnvMaxResults - set the Administrator Tool-Global Search for Value function's maximum number of search results for a single individual environment > For versions 2.4.0 and above Default is 200, which means that each environment will return up to 200 results in a single search operation. Modifying this parameter may affect the performance of the search function, so before modifying it, you should conduct sufficient testing and adjust the value of `apollo.portal.search.perEnvMaxResults` appropriately according to the actual business requirements and system resources to balance the performance and the number of search results. ## 3.2 Adjusting ApolloConfigDB configuration Configuration items are uniformly stored in the ApolloConfigDB.ServerConfig table. It should be noted that each environment's ApolloConfigDB.ServerConfig needs to be configured separately, and the modification takes effect in real time for one minute afterwards. ### 3.2.1 eureka.service.url - Eureka Service Url > Not applicable to Kubernetes-based native service discovery scenarios Both apollo-configservice and apollo-adminservice need to register with the eureka service, so the eureka service address needs to be configured. According to the current implementation, apollo-configservice itself is an eureka service, so you only need to fill in the address of apollo-configservice, separated by commas if there is more than one (be careful not to forget the /eureka/ suffix). It should be noted that each environment only fills in the eureka service address of its own environment, for example, apollo-config service for FAT is 1.1.1.1:8080 and 2.2.2.2:8080, apollo-config service for UAT is 3.3.3.3:8080 and 4.4.4.4: 8080, and the apollo-configigservice for PRO is 5.5.5.5:8080 and 6.6.6.6:8080, then. 1. set eureka.service.url in the ApolloConfigDB.ServerConfig table of the FAT environment to ``` http://1.1.1.1:8080/eureka/,http://2.2.2.2:8080/eureka/ ``` 2. set eureka.service.url in ApolloConfigDB.ServerConfig table of UAT environment to ``` http://3.3.3.3:8080/eureka/,http://4.4.4.4:8080/eureka/ ``` 3. Set eureka.service.url in the ApolloConfigDB.ServerConfig table of the PRO environment to ``` http://5.5.5.5:8080/eureka/,http://6.6.6.6:8080/eureka/ ``` >Note 1: Here you need to fill in the address of all the eureka services in this environment, because eureka needs to copy each other's registration information >Note 2: If you want to register Config Service and Admin Service to the company's unified Eureka, you can refer to [Deployment & Development FAQ - Registering Config Service and Admin Service to a separate Eureka Server](en/faq/common-issues-in-deployment-and-development-phase?id=_8-register-config-service-and-admin-service-to-a-separate-eureka-server) section >Note 3: In multi-cluster deployments, you often want the config service and admin service to register only with the eureka in the same room. To achieve this, you need to use the cluster field in the `ServerConfig` table, and the config service and admin service will read the `/opt/settings/server.properties` (Mac/Linux) or `C:\opt\settings\server.properties` (Windows), and if the idc has a corresponding eureka.service.url configuration, then will only register with eureka for that server room. For example, if the config service and admin service are deployed to two IDCs, `SHAOY` and `SHAJQ`, then in order to register the services in these two server rooms only with that server room, you can add two new records in the `ServerConfig` table and fill in the `SHAOY` and `SHAJQ` server room eureka addresses respectively. If there are config service and admin service that are not deployed in `SHAOY` and `SHAJQ`, this default configuration will be used. | Key | Cluster | Value | Comment | | ------------------ | ------- | --------------------------- | ---------------------------- | | eureka.service.url | default | http://1.1.1.1:8080/eureka/ | Default Eureka Service Url | | eureka.service.url | SHAOY | http://2.2.2.2:8080/eureka/ | Eureka Service Url for SHAOY | | eureka.service.url | SHAJQ | http://3.3.3.3:8080/eureka/ | Eureka Service Url for SHAJQ | ### 3.2.2 namespace.lock.switch - Only one person can modify the switch at a time for release review This is a functional switch, if configured to true, then only one person can modify and another publish at a time for a configuration release. > This option is recommended for production environments ### 3.2.3 `config-service.cache.enabled` - whether to enable configuration caching This is a function switch, if configured to true, config service will cache the loaded configuration information to speed up the performance of subsequent configuration fetches. The default is false. Please evaluate the total configuration size and adjust the config service memory configuration before turning it on. > Ensure that the `app.id`、`apollo.cluster` of the configuration in the application is in the correct case when caching is enabled, otherwise it will not fetch the correct configuration, You can also refer to the `config-service.cache.key.ignore-case` configuration for compatibility processing. > `config-service.cache.enabled` configuration adjustment requires a restart of the config service to take effect #### 3.2.3.1 config-service.cache.key.ignore-case - whether to ignore the case of the configuration cache key > For versions 2.2.0 and above This configuration takes effect when config-service.cache.enabled is set to true, and controls whether the configuration cache key ignores case. The default value is false, which means that cache keys are strictly case-sensitive. In this case, it is necessary to ensure that the capitalization of app.id and apollo.cluster configured in the application is correct, otherwise the correct configuration cannot be obtained. It can be configured as true to ignore case sensitivity. > This configuration is used to be compatible with the configuration acquisition logic when the cache is not enabled, because MySQL database queries are case-insensitive by default. If the cache is enabled and MySQL is used, it is recommended to configure it as true. If the database used by your Apollo is case-sensitive, you must keep the default configuration as false, otherwise the configuration cannot be obtained. #### 3.2.3.2 config-service.cache.stats.enabled - Whether to enable caching metric statistics function > For versions 2.4.0 and above > `config-service.cache.stats.enabled` The adjustment configuration must be restarted config service to take effect. This configuration works when `config-service.cache.stats.enabled` is true, it is used to control the opening of the cache statistics function. The default is false, that is, it will not enable the cache statistics function, when it is set to true, it will enable the cache metric statistics function. View metric reference index[Monitoring related-5.2 Metrics](en/design/apollo-design#5.2-Metrics),such as `http://${someIp:somePort}/prometheus` ### 3.2.4 `item.key.length.limit`- Maximum length limit for configuration item key The default configuration is 128. ### 3.2.5 `item.value.length.limit` - Maximum length limit for configuration item value The default configuration is 20000. #### 3.2.5.1 appid.value.length.limit.override - The maximum length limit of the configuration item value of the appId dimension This configuration is used to override the configuration of `item.value.length.limit` to control the maximum length limit of the value at the appId granularity. The configured value is in a json format, and the key of the json is appId. The format is as follows: ``` appid.value.length.limit.override = {"appId-demo1":200,"appId-demo2":300} ``` The above configuration specifies that the maximum length limit of the value in all namespaces under `appId-demo1` is 200, and the maximum length limit of the value in all namespaces under `appId-demo2` is 300 When a new namespace is created under `appId-demo1` or `appId-demo2`, it will automatically inherit the maximum length limit of the value of the namespace, unless the maximum length limit of the value of the configuration item of the namespace is overridden by `namespace.value.length.limit.override`. #### 3.2.5.2 `namespace.value.length.limit.override` - Maximum length limit for namespace's configuration item value This configuration is used to override the `item.value.length.limit` or `appid.value.length.limit.override` configuration to achieve fine-grained control of the namespace's value maximum length limit, the configured value is a json format, the key of the json is the id value of the namespace in the database, the format is as follows. ``` namespace.value.length.limit.override = {1:200,3:20} ``` The above configuration specifies a maximum length limit of 200 for the value of namespace with id=1 and 20 for the value of namespace with id=3 in the ApolloConfigDB.Namespace table ### 3.2.6 `admin-services.access.control.enabled` - Configure whether apollo-adminservice has access control enabled > For versions 1.7.1 and above Default is false, if configured to true, then apollo-portal needs to be [properly configured](en/deployment/distributed-deployment-guide?id=_3112-admin-servicesaccesstokens-set-the-access-token-required-by-apollo-portal-to-access-the-apollo-adminservice-for-each-environment) to access the access token of that environment, otherwise access will be denied ### 3.2.7 `admin-services.access.tokens` - Configure the list of access tokens allowed to access apollo-adminservice > For versions 1.7.1 and above If this configuration item is empty, then access control will not take effect. If multiple tokens are allowed, the tokens are separated by commas Example. ```properties admin-services.access.tokens=098f6bcd4621d373cade4e832627b4f6 admin-services.access.tokens=098f6bcd4621d373cade4e832627b4f6,ad0234829205b9033196ba818f7a872b ``` ### 3.2.8 `apollo.access-key.auth-time-diff-tolerance` - Configure server-side AccessKey checksum tolerance for time deviation > For version 2.0.0 and above The default value is 60, in seconds. Since the key authentication needs to verify the time, there may be time deviation between the time of the client and the time of the server, if the deviation is too large, the authentication will fail, this configuration can configure the tolerated time deviation size, the default is 60 seconds. ### 3.2.9 apollo.eureka.server.security.enabled - Configure whether to enable Eureka login authentication > For version 2.1.0 and above The default value is false, if you want to improve security (such as when apollo is exposed to the public network), you can enable login authentication for eureka by setting this configuration to true. Note that if eureka login authentication is enabled, the addresses in [eureka.service.url](#_321-eurekaserviceurl-eureka-service-url) needs to be configured with a user name and password, such as: ``` http://some-user-name:some-password@1.1.1.1:8080/eureka/, http://some-user-name:some-password@2.2.2.2:8080/eureka/ ``` Among them, `some-user-name` and `some-password` need to be consistent with the configuration items of `apollo.eureka.server.security.username` and `apollo.eureka.server.security.password`. A reboot is required to take effect after the modification. ### 3.2.10 apollo.eureka.server.security.username - Configure the username of Eureka server > For version 2.1.0 and above Configure the login username of eureka server, which needs to be used together with [apollo.eureka.server.security.enabled](#_329-apolloeurekaserversecurityenabled-configure-whether-to-enable-eureka-login-authentication). A reboot is required to take effect after the modification. > Note that the username cannot be configured as apollo. ### 3.2.11 apollo.eureka.server.security.password - Configure the password of Eureka server > For version 2.1.0 and above Configure the login password of eureka server, which needs to be used together with [apollo.eureka.server.security.enabled](#_329-apolloeurekaserversecurityenabled-configure-whether-to-enable-eureka-login-authentication). A reboot is required to take effect after the modification. ### 3.2.12 apollo.release-history.retention.size - Number of retained configurations release history > For version 2.2.0 and above The default value is -1, which means there is no limit on the number of retained release history. If the configuration is set to a positive integer(The minimum value is 1, which means at least one record of history must be kept to ensure the basic configuration functionality), only the specified number of recent release histories will be kept. This is to prevent excessive database pressure caused by too many release histories. It is recommended to configure this value based on the business needs for configuration rollback. This configuration item is global and cleaned up based on appId + clusterName + namespaceName + branchName. ### 3.2.13 apollo.release-history.retention.size.override - Number of retained configurations release history at a granular level > For version 2.2.0 and above This configuration is used to override the `apollo.release-history.retention.size` configuration and achieve granular control over the number of retained release histories for appId+clusterName+namespaceName+branchName. The value of this configuration is in JSON format, with the JSON key being the concatenated value of appId, clusterName, namespaceName, and branchName using a `+` sign. The format is as follows: ``` json { "kl+bj+namespace1+bj": 10, "kl+bj+namespace2+bj": 20 } ``` The above configuration specifies that the retention size for release history of appId=kl, clusterName=bj, namespaceName=namespace1, and branchName=bj is 10, and the retention size for release history of appId=kl, clusterName=bj, namespaceName=namespace2, and branchName=bj is 20. In general, branchName equals clusterName. It is only different during gray release, where the branchName needs to be confirmed by querying the ReleaseHistory table in the database. ### 3.2.14 instance.config.audit.max.size - The size of the queue for clients to pull audit records > For version 2.5.0 and above The default value is 10000 and the minimum value is 10. It is used to control the queue size for the client to pull audit records. When the queue size is exceeded, the earliest audit record will be discarded. After the modification, you need to restart for it to take effect. ### 3.2.15 instance.cache.max.size - The maximum number of caches for the instance > For version 2.5.0 and above The default value is 50000, and the minimum value is 10. It is used to control the maximum number of instance caches. When the cache exceeds the maximum capacity, the cache eviction mechanism is triggered. After the modification, you need to restart for it to take effect. ### 3.2.16 instance.config.cache.max.size - The maximum number of caches for the instance config > For version 2.5.0 and above The default value is 50000 and the minimum value is 10. It is used to control the maximum number of caches for the instance config. When the cache exceeds the maximum capacity, the cache eviction mechanism is triggered. After the modification, you need to restart for it to take effect. ### 3.2.17 instance.config.audit.time.threshold.minutes - The interval between instances pulling audit records > For version 2.5.0 and above The time threshold unit is minutes, the default is 10, and the minimum is 5. It is used to control when saving/updating the client pull configuration audit record. When the interval between two request records is greater than this value, the pull record will be saved/updated. When it is less than this value, the pull record will not be saved/updated. ### 3.2.14 config-service.incremental.change.enabled - whether to enables incremental config sync for the client > for server versions 2.5.0 and above && java client versions 2.4.0 and above This is a feature toggle. When set to true, the Config Service caches previously loaded configurations and sends incremental updates to clients, reducing server network load. Default is false. Assess total configuration size and adjust Config Service memory settings before enabling. > Ensure that the `app.id`、`apollo.cluster` of the configuration in the application is in the correct case when caching is enabled, otherwise it will not fetch the correct configuration, You can also refer to the `config-service.cache.key.ignore-case` configuration for compatibility processing. > `config-service.incremental.change.enabled` configuration adjustment requires a restart of the config service to take effect ================================================ FILE: docs/en/deployment/quick-start-docker.md ================================================ If you are familiar with Docker, you can use the Docker way to deploy Apollo quickly, so that you can quickly understand Apollo. if you are not very familiar with Docker, please refer to the [regular way to deploy Quick Start](en/deployment/quick-start). If you are deploying Apollo in your company, please refer to the [distributed-deployment-guide](en/deployment/distributed-deployment-guide). > Since Docker doesn't support windows very well, it is not recommended to use Docker way to deploy in windows environment, unless you know windows docker very well ## I. Preparation ### 1.1 Installing Docker You can refer to [Docker Installation Guide](https://yeasy.gitbooks.io/docker_practice/content/install/) for the specific steps, and test whether the installation is successful with the following command ``` docker -v ``` To speed up Docker image download, it is recommended to [configure image gas pedal](https://yeasy.gitbooks.io/docker_practice/content/install/mirror.html). ### 1.2 Download Docker Quick Start configuration file Download [docker-compose.yml](https://github.com/apolloconfig/apollo-quick-start/blob/master/docker-compose.yml) and [sql folder](https://github.com/apolloconfig/apollo-quick-start/tree/master/sql) to a local directory, such as docker-quick-start. > If you are using a machine with an ARM architecture, such as a Mac M1, you will need to download [docker-compose-arm64.yml](https://github.com/apolloconfig/apollo-quick-start/blob/master/docker-compose-arm64.yml) ```bash - docker-quick-start - docker-compose.yml - sql - apolloconfigdb.sql - apolloportaldb.sql ``` ## II. Starting Apollo Configuration Center Execute `docker-compose up` in the docker-quick-start directory, the first execution will trigger downloading images and other operations, you need to be patient and wait for some time. > If you are using a machine with an ARM architecture, such as a Mac M1, execute `docker-compose -f docker-compose-arm64.yml up` Search all the logs starting with `apollo-quick-start` and see the following logs indicating a successful start. ```log apollo-quick-start | ==== starting service ==== apollo-quick-start | Service logging file is . /service/apollo-service.log apollo-quick-start | Started [45] apollo-quick-start | Waiting for config service startup ....... apollo-quick-start | Config service started. You may visit http://localhost:8080 for service status now! apollo-quick-start | Waiting for admin service startup...... apollo-quick-start | Admin service started apollo-quick-start | ==== starting portal ==== apollo-quick-start | Portal logging file is . /portal/apollo-portal.log apollo-quick-start | Started [254] apollo-quick-start | Waiting for portal startup ....... apollo-quick-start | Portal started. You can visit http://localhost:8070 now! ``` > Note 1: The database port is mapped to 13306, so if you want to access the database on the host, you can do so via localhost:13306, username is root and password is left blank. > Note 2: If you want to view more service logs, you can log in via `docker exec -it apollo-quick-start bash`, then go to `/apollo-quick-start/service` and `/apollo-quick-start/portal` to view the log information. ## III. Using Apollo Configuration Center You can refer to [Quick Start - IV. Using Apollo Configuration Center](en/deployment/quick-start?id=iv-using-apollo-configuration-center) for the steps related to using it. Note that the Demo client needs to be run in a Docker environment with the following command. ```bash docker exec -i apollo-quick-start /apollo-quick-start/demo.sh client ``` By default apollo-configservice will only register the internal IP, only clients started by the above command will be able to connect, if you want external clients to be able to access it, please refer to the [network policy](en/deployment/distributed-deployment-guide?id=_14-network-policy). ================================================ FILE: docs/en/deployment/quick-start.md ================================================ To help you quickly get started with the Apollo Configuration Center, we have prepared a Quick Start here, which can deploy and start Apollo Configuration Center in your local environment in a few minutes. If you are familiar with Docker, you can refer to [Apollo Quick Start Docker Deployment](en/deployment/quick-start-docker) to deploy Apollo via Docker. Apollo Quick Start Docker. However, it should be noted that Quick Start is only for local testing, if you want to deploy to production environment, please refer to [distributed-deployment-guide](en/deployment/distributed-deployment-guide) separately. > Note: Quick Start requires a bash environment, Windows users please install [Git Bash](https://git-for-windows.github.io/), we recommend using the latest version, older versions may encounter unknown problems. You can also start directly through the IDE environment, see [Apollo Development Guide](en/contribution/apollo-development-guide) for details. #   # I. Preparation ## 1.1 Java * Apollo server: 17+ * Apollo client: 1.8+ * For running in Java 1.7 runtime environment, please use apollo client of 1.x version, such as 1.9.1 Once configured, this can be checked with the following command. ```sh java -version ``` Sample output. ```sh java version "17.0.14" Java(TM) SE Runtime Environment (build 17.0.14+7) Java HotSpot(TM) 64-Bit Server VM (build 17.0.14+7, mixed mode) ``` Windows users please make sure that JAVA_HOME environment variable is set. ## 1.2 MySQL * If you plan to use H2 in-memory database/H2 file database, there is no need for MySQL, and you can skip this step. * Version requirement: 5.6.5+ Apollo's table structure uses multiple default declarations for `timestamp`, so version 5.6.5+ is required. After connecting to MySQL, you can check with the following command. ```sql SHOW VARIABLES WHERE Variable_name = 'version'; ``` | Variable_name | Value | | ------------- | ------ | | version | 5.7.11 | ## 1.3 Downloading the Quick Start installation package We have prepared a Quick Start installation package, you just need to download it locally and you can use it directly, eliminating the need to compile and package the process. The installation package is 50M, if you can't access github, you can download it from Baidu.com. 1. Download from GitHub * Checkout or download the [apollo-quick-start project](https://github.com/apolloconfig/apollo-quick-start) * **Since the Quick Start project is relatively large, it is placed in a different repository, so please note the project address** * https://github.com/apolloconfig/apollo-quick-start 2. Download from Baidu.com * Downloaded via [weblink](https://pan.baidu.com/s/1Ieelw6y3adECgktO0ea0Gg), extraction code: 9wwe * After downloading to local, unzip apollo-quick-start.zip locally 3. why is the installation package so large as 58M? * Because it is a self-starting jar package, which contains all the dependent jar packages and a built-in tomcat container ### 1.3.1 Manually packaged Quick Start installation package Quick Start is only for local testing, so generally users do not need to download the source code to package it themselves, but just download the already typed package. However, there are some users who want to repackage the package after modifying the code, then you can refer to the following steps. 1. Modify the apollo-configservice, apollo-adminservice and apollo-portal pom.xml, comment out spring-boot-maven-plugin and maven-assembly-plugin 2. Execute `mvn clean package -pl apollo-assembly -am -DskipTests=true` in the root directory. 3. Copy the jar package under apollo-assembly/target and rename it to apollo-all-in-one.jar # II. Initialization and Startup #### Precautions 1. The Apollo server process needs to use ports 8070, 8080, 8090 respectively, please ensure these three ports are not currently in use. 2. The `github` in the SPRING_PROFILES_ACTIVE environment variable in the script is a required profile, `database-discovery` specifies the use of database service discovery, `auth` is a profile that provides simple authentication for the portal, it can be removed if authentication is not required or other authentication methods are used. ## 2.1 Use H2 in-memory database, automatic initialization No configuration is required, just use the following command to start > Note: When using the in-memory database, any operation will be lost after the Apollo process restarts ```bash export SPRING_PROFILES_ACTIVE="github,database-discovery,auth" unset SPRING_SQL_CONFIG_INIT_MODE unset SPRING_SQL_PORTAL_INIT_MODE java -jar apollo-all-in-one.jar ``` ## 2.2 Use H2 file database, automatic initialization #### Precautions 1. The path `~/apollo/apollo-config-db` and `~/apollo/apollo-portal-db` in the environment variable in the script can be replaced with other custom paths, you need to ensure that this path has read and write permissions ### 2.2.1 First startup Use the SPRING_SQL_CONFIG_INIT_MODE="always" and SPRING_SQL_PORTAL_INIT_MODE="always" environment variable for initialization at the first startup ```bash export SPRING_PROFILES_ACTIVE="github,database-discovery,auth" # config db export SPRING_SQL_CONFIG_INIT_MODE="always" export SPRING_CONFIG_DATASOURCE_URL="jdbc:h2:file:~/apollo/apollo-config-db;mode=mysql;DB_CLOSE_ON_EXIT=FALSE;DB_CLOSE_DELAY=-1;BUILTIN_ALIAS_OVERRIDE=TRUE;DATABASE_TO_UPPER=FALSE" # portal db export SPRING_SQL_PORTAL_INIT_MODE="always" export SPRING_PORTAL_DATASOURCE_URL="jdbc:h2:file:~/apollo/apollo-portal-db;mode=mysql;DB_CLOSE_ON_EXIT=FALSE;DB_CLOSE_DELAY=-1;BUILTIN_ALIAS_OVERRIDE=TRUE;DATABASE_TO_UPPER=FALSE" java -jar apollo-all-in-one.jar ``` ### 2.2.2 Subsequent startup Remove the SPRING_SQL_CONFIG_INIT_MODE and SPRING_SQL_PORTAL_INIT_MODE environment variable to avoid repeated initialization at subsequent startup ```bash export SPRING_PROFILES_ACTIVE="github,database-discovery,auth" # config db unset SPRING_SQL_CONFIG_INIT_MODE export SPRING_CONFIG_DATASOURCE_URL="jdbc:h2:file:~/apollo/apollo-config-db;mode=mysql;DB_CLOSE_ON_EXIT=FALSE;DB_CLOSE_DELAY=-1;BUILTIN_ALIAS_OVERRIDE=TRUE;DATABASE_TO_UPPER=FALSE" # portal db unset SPRING_SQL_PORTAL_INIT_MODE export SPRING_PORTAL_DATASOURCE_URL="jdbc:h2:file:~/apollo/apollo-portal-db;mode=mysql;DB_CLOSE_ON_EXIT=FALSE;DB_CLOSE_DELAY=-1;BUILTIN_ALIAS_OVERRIDE=TRUE;DATABASE_TO_UPPER=FALSE" java -jar apollo-all-in-one.jar ``` ## 2.3 Use mysql database, automatic initialization #### Precautions 1. The your-mysql-server:3306 in the environment variable in the script needs to be replaced with the actual mysql server address and port, ApolloConfigDB and ApolloPortalDB needs to be replaced with the actual database name 2. The "apollo-username" and "apollo-password" in the environment variables in the script need to fill in the actual username and password ### 2.3.1 First startup Use the SPRING_SQL_INIT_MODE="always" environment variable for initialization at the first startup ```bash export SPRING_PROFILES_ACTIVE="github,database-discovery,auth" # config db export SPRING_SQL_CONFIG_INIT_MODE="always" export SPRING_CONFIG_DATASOURCE_URL="jdbc:mysql://your-mysql-server:3306/ApolloConfigDB?useUnicode=true&characterEncoding=UTF8" export SPRING_CONFIG_DATASOURCE_USERNAME="apollo-username" export SPRING_CONFIG_DATASOURCE_PASSWORD="apollo-password" # portal db export SPRING_SQL_PORTAL_INIT_MODE="always" export SPRING_PORTAL_DATASOURCE_URL="jdbc:mysql://your-mysql-server:3306/ApolloPortalDB?useUnicode=true&characterEncoding=UTF8" export SPRING_PORTAL_DATASOURCE_USERNAME="apollo-username" export SPRING_PORTAL_DATASOURCE_PASSWORD="apollo-password" java -jar apollo-all-in-one.jar ``` ### 2.3.2 Subsequent startup Remove the SPRING_SQL_CONFIG_INIT_MODE and SPRING_SQL_PORTAL_INIT_MODE environment variable to avoid repeated initialization at subsequent startup ```bash export SPRING_PROFILES_ACTIVE="github,database-discovery,auth" # config db unset SPRING_SQL_CONFIG_INIT_MODE export SPRING_CONFIG_DATASOURCE_URL="jdbc:mysql://your-mysql-server:3306/ApolloConfigDB?useUnicode=true&characterEncoding=UTF8" export SPRING_CONFIG_DATASOURCE_USERNAME="apollo-username" export SPRING_CONFIG_DATASOURCE_PASSWORD="apollo-password" # portal db unset SPRING_SQL_PORTAL_INIT_MODE export SPRING_PORTAL_DATASOURCE_URL="jdbc:mysql://your-mysql-server:3306/ApolloPortalDB?useUnicode=true&characterEncoding=UTF8" export SPRING_PORTAL_DATASOURCE_USERNAME="apollo-username" export SPRING_PORTAL_DATASOURCE_PASSWORD="apollo-password" java -jar apollo-all-in-one.jar ``` ## 2.4 Use mysql database, manual initialization ### 2.4.1 Manually initialize ApolloConfigDB and ApolloPortalDB You can import [apolloconfigdb.sql](https://github.com/apolloconfig/apollo/blob/master/scripts/sql/profiles/mysql-default/apolloconfigdb.sql) to ApolloConfigDB through various MySQL clients. You can import [apolloportaldb.sql](https://github.com/apolloconfig/apollo/blob/master/scripts/sql/profiles/mysql-default/apolloportaldb.sql) to ApolloPortalDB through various MySQL clients. ### 2.4.2 Run #### Precautions 1. The your-mysql-server:3306 in the environment variable in the script needs to be replaced with the actual mysql server address and port, ApolloConfigDB and ApolloPortalDB needs to be replaced with the actual database name 2. The "apollo-username" and "apollo-password" in the environment variables in the script need to fill in the actual username and password ```bash export SPRING_PROFILES_ACTIVE="github,database-discovery,auth" # config db unset SPRING_SQL_CONFIG_INIT_MODE export SPRING_CONFIG_DATASOURCE_URL="jdbc:mysql://your-mysql-server:3306/ApolloConfigDB?useUnicode=true&characterEncoding=UTF8" export SPRING_CONFIG_DATASOURCE_USERNAME="apollo-username" export SPRING_CONFIG_DATASOURCE_PASSWORD="apollo-password" # portal db unset SPRING_SQL_PORTAL_INIT_MODE export SPRING_PORTAL_DATASOURCE_URL="jdbc:mysql://your-mysql-server:3306/ApolloPortalDB?useUnicode=true&characterEncoding=UTF8" export SPRING_PORTAL_DATASOURCE_USERNAME="apollo-username" export SPRING_PORTAL_DATASOURCE_PASSWORD="apollo-password" java -jar apollo-all-in-one.jar ``` # III. Note Quick Start is only used to help you quickly experience Apollo project, please refer to: [distributed-deployment-guide](en/deployment/distributed-deployment-guide) for details. It should be noted that Quick Start does not support adding environments, but only through distributed deployment, please refer to: [distributed-deployment-guide](en/deployment/distributed-deployment-guide) # IV. Using Apollo Configuration Center ## 4.1 Using the sample project ### 4.1.1 Initialize the sample configuration 1. Open http://localhost:8070 > Quick Start integrates with [Spring Security simple authentication](en/extension/portal-how-to-implement-user-login-function?id=implementation-1-simple-authentication-using-spring-security-provided-by-apollo), for more information you can refer to [Portal implementing user login function](en/extension/portal-how-to-implement-user-login-function) login 2. Enter username apollo and password admin and log in ![Home](https://cdn.jsdelivr.net/gh/apolloconfig/apollo-quick-start@master/images/apollo-sample-home-en.jpg) 3. Click "Create project", enter the `SampleApp` information, and submit. ![Create project](https://cdn.jsdelivr.net/gh/apolloconfig/apollo-quick-start@master/images/apollo-create-sample-app-en.jpg) 4. Go to the SampleApp configuration interface, click on "Add Configuration", enter the `timeout` information, and submit. ![Add configuration](https://cdn.jsdelivr.net/gh/apolloconfig/apollo-quick-start@master/images/apollo-create-sample-config-en.jpg) 5. Click on the "Release" button, and fill in the release information. ![Configuration page](https://cdn.jsdelivr.net/gh/apolloconfig/apollo-quick-start@master/images/sample-app-config-en.jpg) ![Release page](https://cdn.jsdelivr.net/gh/apolloconfig/apollo-quick-start@master/images/sample-app-release-detail-en.jpg) ### 4.1.2 Running the client application We have prepared a simple [Demo client](https://github.com/apolloconfig/apollo-demo-java/blob/main/api-demo/src/main/java/com/apolloconfig/apollo/demo/api/SimpleApolloConfigDemo.java) to demonstrate getting configuration from Apollo Configuration Center. The program is simple: the user enters the name of a key, and the program outputs the value corresponding to that key. If the key is not found, undefined is output. Also, the client listens for configuration change events and outputs the changed configuration information once there is a change. Run `./demo.sh client` to start the demo client and ignore the previous debugging information, you can see the following prompt. ```sh Apollo Config Demo. Please input key to get the value. Input quit to exit. > ``` Enter ``timeout`` and you will see the following message. ```sh > timeout Loading key : timeout with value: 1000 ``` > If you encounter problems running the client, you can view more detailed logging information by changing the level in ``client/log4j2.xml`` to DEBUG > ```xml > > > > ``` ### 4.1.3 Modify the configuration and publish Return to the configuration interface, change the value of `timeout` to 2000, and release the configuration. ![Modify configuration](https://cdn.jsdelivr.net/gh/apolloconfig/apollo-quick-start@master/images/sample-app-modify-config-en.jpg) ### 4.1.4 Client view the modified value If the client has been running, it will listen for configuration changes after the configuration is published and output the modified configuration information as follows. ```sh Changes for namespace application Change - key: timeout, oldValue: 1000, newValue: 2000, changeType: MODIFIED ``` Type ``timeout`` again to see the corresponding value and you will see the following message. ```sh > timeout Loading key : timeout with value: 2000 ``` ## 4.2 Using the new project ### 4.2.1 App access to Apollo This part can be found in [Java Application Access Guide](en/client/java-sdk-user-guide) ### 4.2.2 Run the client application Since a new project is used, the client needs to modify the appId information. Edit ``client/META-INF/app.properties`` and change `app.id` to your newly created app id. ```properties app.id=your appId ``` ``` Run `./demo.sh client` to start the demo client. ``` ================================================ FILE: docs/en/deployment/third-party-tool-btpanel.md ================================================ ## aaPanel Docker One-Click Install Go to [aaPanel official website](https://www.aapanel.com/new/download.html), Select the script to download and install(Skip this step if you already have it installed) # Deploy Apollo using aaPanel ## Prerequisite To install aaPanel, go to the [aaPanel](https://www.aapanel.com/new/download.html#install) official website and select the corresponding script to download and install. ## Deployment aaPanel(Applicable versions 7.0.11 and above) Deployment guidelines 1. Log in to aaPanel and click `Docker` in the menu bar ![Docker](../images/deployment/btpanel/install.png) 2. The first time you will be prompted to install the `Docker` and `Docker Compose` services, click Install Now. If it is already installed, please ignore it. ![install](../images/deployment/btpanel/install2.png) 3. After the installation is complete, find `Apollo` in `One-Click Install` and click `install` ![install-Apollo](../images/deployment/btpanel/install-Apollo.png) 4. configure basic information such as the domain name, ports to complete the installation Note: The domain is optional. If a domain is provided, it can be managed through [Website] --> [Proxy Project]. In this case, you do not need to check [Allow external access]. However, if no domain is provided, you must check [Allow external access] to enable port-based access. ![addApollo](../images/deployment/btpanel/addApollo.png) 5. After installation, enter the domain name or IP+ port set in the previous step in the browser to access. - Name: application name, default `Apollo-characters` - Version selection: default `latest` - Domain name: If you need to access directly through the domain name, please configure the domain name here and resolve the domain name to the server - Allow external access: If you need direct access through `IP+Port`, please check. If you have set up a domain name, please do not check here. - Web port: Default `8070`, can be modified by yourself - Communication port: Default `8080`, can be modified by yourself - Metadata port: Default `8090`, can be modified by yourself - **Security Note:** - Ensure these ports are not exposed directly to the internet - Configure your firewall to restrict access to these ports - Verify that these ports are not already in use by other services 6. After submission, the panel will automatically initialize the application, which will take about `1-3` minutes. It can be accessed after the initialization is completed. ## Access Methods 1. **Domain Name Access** - URL Format: `http://` - Example: `http://demo.apollo.org` **Enable HTTPS (Suggestion):** 1. Obtain an SSL certificate (recommended providers: Let's Encrypt, Certbot) 2. In aaPanel, go to [Website] -> [SSL] 3. Install your SSL certificate 4. Force HTTPS redirect for enhanced security After SSL configuration: - URL Format: `https://` - Example: `https://demo.apollo.org` 2. **IP and Port Access** - URL Format: `http://:8070` - Note: Requires "Allow external access" to be enabled ![console](../images/deployment/btpanel/console.png) > Default credentials: username `apollo`, password `admin`, please change the default password immediately. ================================================ FILE: docs/en/deployment/third-party-tool-rainbond.md ================================================ #   # Background Current documentation describes how to install a high availability Apollo cluster with one-click through [Rainbond](https://www.rainbond.com/?channel=apollo), a cloud native application management platform. This approach is suitable for users who are less familiar with Kubernetes, containerization and other complex technologies, and lowers the barrier to deploy Apollo in Kubernetes. ## Combination of Rainbond and Apollo Rainbond is an easy to use open source cloud native application management platform. With the help of it, users can complete the deployment, operation and maintenance of microservices in a graphical interface. With the help of Kubernetes and containerization technology, automatic operation and maintenance capabilities such as fault self-healing and elastic expansion can be endowed to users' applications. Rainbond has a built-in native Service Mesh microservice framework, and also has a good integration experience with other microservice frameworks such as Spring Cloud and Dubbo. Therefore, a large number of Rainbond users may also be users of the Apollo configuration center. Instead of worrying about how to deploy a Apollo cluster, the Rainbond team made Apollo a one-click application template for free download and installation by open source users. This installation method greatly reduces the deployment burden of users using Apollo clusters. Currently, versions 1.9.2 is supported. The current installation mode integrates a set of `PRO` environments by default. Instructions to add additional environments is described in the advanced features section later. ## About application template Application template is a package manager for Rainbond cloud native application management platform. Users can install applications into Rainbond with one-click. No matter how complex the application is, the application template abstracts it into an package, which is installed with docker images of all the components, configuration information, and relationships between all the components. # Prerequisite - Deployed Rainbond cloud native application management platform. For example: The [Quick Start](https://www.rainbond.com/docs/quick-start/quick-install/?channel=apollo) can be used to run in a PC within a container. - Internet connection. # Quick Start ## Access the built-in open source app store Select the **App Store** on the left, switch to the **Open Source App Store**, and search **apollo** to find the Apollo application. ![apollo-1](https://static.goodrain.com/wechat/apollo/apollo-1.png) ## One-click install Click **Install** on the right of Apollo to enter the installation page. After filling in simple information, click **OK** to start the installation, and the page automatically jumps to the topology view. ![apollo-2](https://static.goodrain.com/wechat/apollo/apollo-2.png) Parameters: | Options | Instructions | | ---- | ---------------------------------- | | team name | User-defined workspace isolated by namespace | | cluster name | Select which K8s cluster Apollo will be deployed to | | application | Select the application to which Apollo will be deployed, which contains several associated components | | version | Select the version of Apollo, the usable version is 1.9.2 | After a few minutes, the Apollo is installed and up and running. ![apollo-3](https://static.goodrain.com/wechat/apollo/apollo-3.png) ## Testing Access the default domain provided by component `Apollo-portal-1.9.2` to log into the Apollo console and verify in system information that the `PRO` environment is ready. ![apollo-4](https://static.goodrain.com/wechat/apollo/apollo-4.png) ## Configuration In Rainbond, Apollo clusters can be configured based on a graphical interface. It mainly includes three parts: environment variables, configuration file mounting and plugin configuration. - Environment variables: You can customize environment variables through environment configuration in different component pages. For example, `Apollo-portal-1.9.2` adds `APOLLO_PORTAL_ENVS=pro` by default to define the environment managed by the current portal. - Configuration files: You can set configuration files for components in environment configuration on different component pages. - `Apollo-portal-1.9.2` mount `/apollo-portal/config/apollo-env.properties` is used to define meta addresses for different environments. - `Apollo-config-1.9.2` mount `/apollo-configservice/config/application-github.properties` used to declare the current config and admin service address. - Plugin configuration: The downstream call address is defined in Rainbond by installing the `Egress Network Governance Plugin` for `Apollo-portal-1.9.2` `Apollo-config-1.9.2`, which is an implementation of Service Mesh microservice governance. By defining the domain name of the downstream service, you can access the specified port of the downstream service. For example, in the `Apollo-portal-1.9.2` plugin, the domain name to access port `Apollo-config-1.9.2` 's 8080 is `Apollo-config-pro`, which is why the configuration only defines the domain name and does not need to define the port. # Advanced ## Instance number scaling The `Apollo-portal-1.9.2` `Apollo-config-1.9.2` `Apollo-admin-1.9.2` components included in the Apollo configuration center are deployed using a Deployment controller, Service discovery and communication are realized through Rainbond's built-in Service Mesh microservice framework. Therefore, all three components can scale multiple instances with one click for clustered deployment. Take `Apollo-portal-1.9.2` as an example, click **scale**, modify the number of **instances**, and click **Set**. ![apollo-5](https://static.goodrain.com/wechat/apollo/apollo-5.png) ## Add envs The Apollo configuration center supports multiple environments and manages them using a unified Portal page. Apollo clusters based on Rainbond's one-click installation ship with the 'PRO' environment by default. In the Rainbond scenario, I'll show you how to append a set of 'DEV' environments. Access the `Apollo-config-dev` and `Apollo-admin-dev` components, respectively. 1. Deploy another Set of Apollo clusters and remove `Apollo-portal-1.9.2` `ApolloPortalDB` components from the new cluster. Change the name of `Apollo-config-1.9.2` `Apollo-admin-1.9.2` component for ease of administration. Add a dependency from `Apollo-portal-1.9.2` to `Apollo-config-dev` `Apollo-admin-dev`. The topology is shown as follows: > Note that this step will trigger a connection information environment variable conflict, remember to redefine your preferred name for the `Apollo-config-dev` `Apollo-admin-dev` component's internal port. ![apollo-6](https://static.goodrain.com/wechat/apollo/apollo-6.png) 2. In **environment configuration** page, modify `Apollo-config-Dev` configuration file `/apollo-configservice/config/application-github.properties`, Change the service addresses of config and admin to the expected values. ![apollo-10](https://static.goodrain.com/wechat/apollo/apollo-10.png) 3. Go to the `Apollo-config-dev` `Apollo-portal-1.9.2` plugin page, modify the configuration for its `Egress Network Governance Plugin` , Rainbond built-in microservice framework, define the downstream service address by setting Domains. In the case of `Apollo-portal-1.9.2`, an access domain name to `apollo-config-dev` `apollo-admin-dev` needs to be configured. ![apollo-7](https://static.goodrain.com/wechat/apollo/apollo-7.png) After the configuration is complete, click **update configuration**, `Apollo-config-Dev` can be accessed through the domain name `apollo-config-dev`. Similarly, `Apollo-config-dev` needs to be configured with an access domain name to `apollo-admin-dev`. Update the configuration after the configuration is complete. 4. Modify the configuration of `Apollo-portal-1.9.2` to join the new `DEV` environment. Modify the value of environment variable `APOLLO_PORTAL_ENVS` to add to the `dev` environment. ![apollo-8](https://static.goodrain.com/wechat/apollo/apollo-8.png) Modify the configuration file `/apollo-portal/config/apollo-env.properties` and write the meta address of the `dev` environment. ![apollo-9](https://static.goodrain.com/wechat/apollo/apollo-9.png) Update the `Apollo-portal-1.9.2` component to make all configurations take effect. View system information to verify that the environment is added. ![apollo-11](https://static.goodrain.com/wechat/apollo/apollo-11.png) ================================================ FILE: docs/en/design/apollo-core-concept-namespace.md ================================================ ### 1. What is Namespace? Namespace is a collection of configuration items, similar to the concept of a configuration file. ### 2. What is an `application` Namespace? When Apollo creates a project, it creates an `application` namespace by default. Spring Boot students know that Spring Boot projects have a default configuration file application.yml. here application.yml is equivalent to the `application` Namespace. For 90% of applications, the `application` namespace is sufficient for everyday configuration scenarios. #### The code for client to get the `application` Namespace is as follows. ``` java Config config = ConfigService.getAppConfig(); ``` #### The code for the client to get a `non-application` Namespace is as follows. ``` java Config config = ConfigService.getConfig(namespaceName); ``` ### 3. What are the formats of Namespace? Configuration files come in various formats such as properties, xml, yml, yaml, json, etc. Similarly Namespace also has these formats. In the Portal UI, you can see that the Namespace for `application` has a `properties` tag, indicating that `application` is in properties format. >Note1: For the namespace in non-properties format, you need to call `ConfigService.getConfigFile(String namespace, ConfigFileFormat configFileFormat)` to get it when you use the client, if you use the [Http interface direct call](en/client/other-language-client-user-guide), the corresponding namespace parameter needs to be passed in the namespace name plus the suffix name, such as datasources.json. >Note 2: apollo-client version 1.3.0 has better support for yaml/yml, which is consistent with the properties format: `Config config = ConfigService.getConfig("application.yml");`, Spring's The Spring injection is also consistent with properties. ### 4. Classification of Namespace access rights There are two types of access permissions for Namespace. * private (private) * public (public) The access rights here are relative to the Apollo client. #### 4.1 Private Permissions Namespace with private access can only be accessed by the application it belongs to. If an application tries to get the private namespace of another application, Apollo will report a `404 ` exception. #### 4.2 Public Permissions Namespace with public privileges can be fetched by any application. ### 5. Types of Namespace There are three types of Namespace. * private type * public type * Associated types (inherited types) #### 5.1 Private Types Namespace of private type has private permission. For example, the `application` Namespace mentioned above is a private type. #### 5.2 Public Types ##### 5.2.1 Meaning Namespace of a public type has public privileges. A public type Namespace is equivalent to a configuration that is outside the application and identifies the public Namespace by its name, so the name of the public Namespace must be globally unique. ##### 5.2.2 Usage Scenarios * Department-level shared configurations * Configurations shared at the group level * Configurations shared between several projects * Configuration for middleware clients #### 5.3 Association Types ##### 5.3.1 Meaning An association type may also be called an inherited type, and the association type has private privileges. The Namespace of an association type inherits from the Namespace of a public type and is used to override certain configurations of the public Namespace. For example, the public Namespace has two configuration items ``` k1 = v1 k2 = v2 ``` Then application A has an associated type of Namespace associated with this public Namespace and overrides the configuration item k1 with a new value of v3. Then when application A actually runs, the configuration of the public Namespace is obtained as ``` k1 = v3 k2 = v2 ``` ##### 5.3.2 Usage Scenarios Give a real-world usage scenario. Assume that the configuration of the RPC framework (e.g., timeout) has the following requirements. * Provide a company-wide default configuration that can be dynamically adjusted * RPC client projects can customize certain configuration items and can be dynamically adjusted 1. If you remove `dynamically adjustable` from the above two requirements, the approach is simple. There is a configuration file in the rpc-client.jar package, and each time you modify the configuration file, you can resend a new version of the jar package. Similarly, the same is true for client-side projects. 2. If only the client project is supported, the configuration can be dynamically adjusted. The client project can configure some configuration items on Apollo `application` Namespace. When initializing the service, just read the configuration from Apollo. The disadvantage of this is that each project has to customize some keys, which is not uniform. 3. 3. so how to support the above requirements perfectly? The answer is to use a combination of Apollo's public type Namespace and the associated type Namespace. The code in rpc-client.jar reads the configuration of the `rpc-client` Namespace. If you need to adjust the default configuration, just change the configuration of the public type `rpc-client` Namespace. If a client project wants to customize or dynamically modify some configuration items, simply associate `rpc-client` with Apollo's own project to create a Namespace of the associated type `rpc-client`. Then you can modify the configuration items under the Namespace of the associated type `rpc-client`. One thing we need to point out here is that rpc-client.jar is running in the application container, so the configuration of the `rpc-client` Namespace that rpc-client gets is the namespace of the associated type of the application plus the namespace of the public type Namespace. #### 5.4 Example As shown in the following figure, there are three applications: Application A, Application B, and Application C. * Application A has two Namespace of private type: application and NS-Private, and one Namespace of associated type: NS-Public. * Application B has one Namespace of private type: application, and one Namespace of public type: NS-Public. * Application C has only one Namespace of type private: application ![Namespace Example](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/namespace-model-example.png) ##### 5.4.1 Application A gets the Apollo configuration ```java //application Config appConfig = ConfigService.getAppConfig(); appConfig.getProperty("k1", null); // k1 = v11 appConfig.getProperty("k2", null); // k2 = v21 // NS-Private Config privateConfig = ConfigService.getConfig("NS-Private"); privateConfig.getProperty("k1", null); // k1 = v3 privateConfig.getProperty("k3", null); // k3 = v4 // NS-Public, overrides the case of public type configuration, k4 is overridden Config publicConfig = ConfigService.getConfig("NS-Public"); publicConfig.getProperty("k4", null); // k4 = v6 cover publicConfig.getProperty("k6", null); // k6 = v6 publicConfig.getProperty("k7", null); // k7 = v7 ``` ##### 5.4.2 Application B to get Apollo configuration ``` java //application Config appConfig = ConfigService.getAppConfig(); appConfig.getProperty("k1", null); // k1 = v12 appConfig.getProperty("k2", null); // k2 = null appConfig.getProperty("k3", null); // k3 = v32 // NS-Private, since there is no NS-Private Namespace so we get the default value Config privateConfig = ConfigService.getConfig("NS-Private"). privateConfig.getProperty("NS-Private"); privateConfig.getProperty("k1", "default value"); //NS-Public Config publicConfig = ConfigService.getConfig("NS-Public"); publicConfig.getProperty("k4", null); // k4 = v5 publicConfig.getProperty("k6", null); // k6 = v6 publicConfig.getProperty("k7", null); // k7 = v7 ``` ##### 5.4.3 Applying C to get Apollo configuration ``` java //application Config appConfig = ConfigService.getAppConfig(); appConfig.getProperty("k1", null); // k1 = v12 appConfig.getProperty("k2", null); // k2 = null appConfig.getProperty("k3", null); // k3 = v33 // NS-Private, since there is no NS-Private Namespace so we get the default value Config privateConfig = ConfigService.getConfig("NS-Private"). privateConfig.getProperty("NS-Private"); privateConfig.getProperty("k1", "default value"); //NS-Public, the public type Namespace, which any project can get Config publicConfig = ConfigServi ``` ##### 5.4.4 ChangeListener As you can see in the above code example, the Client Namespace is mapped to a Config object. Listeners for namespace configuration changes are registered on the Config object. So the Namespace code that monitors the application in application A is as follows: ```java Config appConfig = ConfigService.getAppConfig(); appConfig.addChangeListener(new ConfigChangeListener() { public void onChange(ConfigChangeEvent changeEvent) { //do something } }) ``` The Namespace code for monitoring NS-Private in application A is as follows: ```java Config privateConfig = ConfigService.getConfig("NS-Private"); privateConfig.addChangeListener(new ConfigChangeListener() { public void onChange(ConfigChangeEvent changeEvent) { //do something } }) ``` The Namespace code for monitoring NS-Public in application A, application B, and application C is as follows: ```java Config publicConfig = ConfigService.getConfig("NS-Public"); publicConfig.addChangeListener(new ConfigChangeListener() { public void onChange(ConfigChangeEvent changeEvent) { //do something } }) ``` ================================================ FILE: docs/en/design/apollo-design.md ================================================   #   # I、General design ## 1.1 Base model The following is the base model of Apollo. 1. Users modify and publish the configuration in the configuration center 2. The configuration center notifies Apollo clients of configuration updates 3. Apollo client pulls the latest configuration from the configuration center, updates the local configuration and notifies the application ![basic-architecture](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/basic-architecture.png) ## 1.2 Architecture Module The following figure provides an overview of Apollo's architecture modules. For a detailed description, you can refer to [Apollo Configuration Center Architecture Anatomy](https://mp.weixin.qq.com/s/-hUaQPzfsl9Lm3IqQW3VDQ). ![overall-architecture](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/overall-architecture.png) The above diagram briefly describes the general design of Apollo, which we can see from bottom to top. * Config Service provides configuration reading, pushing, etc., and serves Apollo clients ```mermaid sequenceDiagram Client ->> Config Service: request Config Service ->> ConfigDB: request ConfigDB -->> Config Service: ack Config Service -->> Client: ack ``` - Admin Service provides configuration modification, publishing and other functions, the service object is Apollo Portal (management interface). ```mermaid sequenceDiagram Portal ->> Admin Service: r/w, publish appId/cluster/namespace Admin Service ->> ConfigDB: r/w, publish appId/cluster/namespace ConfigDB -->> Admin Service: ack Admin Service -->> Portal: ack ``` - Config Service and Admin Service are both multi-instance, stateless deployments, so they need to register themselves with Eureka and keep a heartbeat - On top of Eureka we erected a layer of Meta Server to encapsulate Eureka's service discovery interface ```mermaid sequenceDiagram Client or Portal ->> Meta Server: discovery service's instances Meta Server ->> Eureka: discovery service's instances Eureka -->> Meta Server: service's instances Meta Server -->> Client or Portal: service's instances ``` - Client accesses Meta Server through domain name to get Config Service service list (IP+Port), and then accesses the service directly through IP+Port, and at the same time will do load balance, error retry on Client side. ```mermaid sequenceDiagram Client ->> Meta Server: discovery Config Service's instances Meta Server -->> Client: Config Service's instances(Multiple IP+Port) loop until success Client ->> Client: load balance choose a Config Service instance Client ->> Config Service: request Config Service -->> Client: ack end ``` - Portal accesses Meta Server through domain name to get Admin Service service list (IP+Port), and then directly accesses the service through IP+Port, and at the same time will do load balance, error retry on Portal side. ```mermaid sequenceDiagram Portal ->> Meta Server: discovery Admin Service's instances Meta Server -->> Portal: Admin Service's instances(Multiple IP+Port) loop until success Portal ->> Portal: load balance choose a Admin Service instance Portal ->> Admin Service: request Admin Service -->> Portal: ack end ``` - To simplify deployment, we will actually deploy the three logical roles Config Service, Eureka and Meta Server in the same JVM process. ```mermaid graph subgraph JVM Process 1[Config Service] 2[Eureka] 3[Meta Server] end ``` The actual deployment architecture can be found in [deployment-architecture](en/deployment/deployment-architecture.md) ### 1.2.1 Why Eureka Why do we use Eureka as a service registry instead of the traditional zk and etcd? I have roughly summarized the reasons as follows. * It provides a complete Service Registry and Service Discovery implementation * First of all, it provides a complete implementation and has also withstood the test of Netflix's own production environment, so it's relatively painless to use. * Integration with Spring Cloud * Our project itself uses Spring Cloud and Spring Boot, and Spring Cloud has a very comprehensive set of open source code to integrate with Eureka, so it's very easy to use. * In addition, Eureka supports starting in our application's own container, which means that after our application is started, it acts as both an Eureka and a service provider. This greatly improves the availability of the service. * **This is the main reason why we chose Eureka over zk, etc. In order to improve the availability of the configuration center and reduce the complexity of deployment, we need to minimize external dependencies as much as possible.** * Open Source * The last point is open source. Since the code is open source, it is very easy for us to understand how it is implemented and troubleshoot problems. ## 1.3 Overview of the modules ### 1.3.1 Config Service * Provides configuration acquisition interface ```mermaid sequenceDiagram Client ->> Config Service: get content of appId/cluster/namespace opt if namespace is not cached Config Service ->> ConfigDB: get content of appId/cluster/namespace ConfigDB -->> Config Service: content of appId/cluster/namespace end Config Service -->> Client: content of appId/cluster/namespace ``` * provide configuration update push interface (based on Http long polling) * server-side use of [Spring DeferredResult](http://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/context/request/async/DeferredResult.html) to achieve asynchronization, thus greatly increasing the number of long connections * currently using tomcat embed default configuration is up to 10,000 connections (can be adjusted), using a 4C8G virtual machine measured can support 10,000 connections, so meet the demand (an application instance will only launch a long connection). * Interface service object is Apollo client ### 1.3.2 Admin Service * Provide configuration management interface * Provides interfaces for configuration modification, publishing, retrieval, etc. * Interface service object is Portal ### 1.3.3 Meta Server * Portal accesses Meta Server via domain name to get Admin Service service list (IP+Port) * Client accesses Meta Server via domain name to get the Config Service service list (IP+Port) * Meta Server gets the service information of Config Service and Admin Service from Eureka, which is equivalent to an Eureka Client * Adding a Meta Server role is mainly to encapsulate the details of service discovery, for Portal and Client, always get the service information of Admin Service and Config Service through an Http interface, without caring about the actual service registration and discovery components behind * Meta Server is just a logical role, in deployment and Config Service is in a JVM process, so IP, port and Config Service consistent ### 1.3.4 Eureka * Based on [Eureka](https://github.com/Netflix/eureka) and [Spring Cloud Netflix](https://cloud.spring.io/spring-cloud-netflix/) to provide service registration and discovery * Config Service and Admin Service will register services with Eureka and keep a heartbeat * For simplicity, Eureka is currently deployed with Config Service in a single JVM process (via Spring Cloud Netflix) ### 1.3.5 Portal * Provides a web interface for users to manage configuration * Get the list of Admin Service services (IP+Port) through Meta Server and access the services through IP+Port * Do load balance, error retry on Portal side ### 1.3.6 Client * Client program provided by Apollo to provide configuration acquisition, real-time update and other functions for the application * Get the Config Service service list (IP+Port) through Meta Server, access the service through IP+Port * Do load balance, error retry on Client side ## 1.4 E-R Diagram ### 1.4.1 Main E-R Diagram ![apollo-erd](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/apollo-erd.png) * **App** * App Information * **AppNamespace** * Metainformation of Namespace under App * **Cluster** * Cluster information * **Namespace** * Namespace under Cluster * **Item** * Namespace configuration, each Item is a key, value combination * **Release** * Namespace release configuration, each release contains all the configuration of the Namespace at the time of release * **Commit** * **Commit** * Namespace configuration change log * **Audit** * Audit information that records which entity was manipulated by which user and when, using which method. ### 1.4.2 Permission Related E-R Diagram ![apollo-erd-role-permission](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/apollo-erd-role-permission.png) * **User** * Apollo portal users * **UserRole** * User and role relationships * **Role** * Role * **RolePermission** * The relationship between roles and permissions * **Permission** * Permissions * Corresponds to specific entity resources and operations, such as modifying the configuration of NamespaceA, publishing the configuration of NamespaceB, etc. * **Consumer** * Third-party applications * **ConsumerToken** * token issued to the third-party application * **ConsumerRole** * Third-party application and role relationship * **ConsumerAudit** * Third-party application access audit # II. Server-side design ## 2.1 Real-time push design after configuration release An important feature in the configuration center is the real-time push to the client after the configuration is published. Here we briefly look at how this piece is designed to be implemented. An important feature in the configuration center is the real-time push to the client after the configuration is published. Let's take a brief look at how this piece is designed to be implemented. ![release-message-notification-design](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/release-message-notification-design.png) The above diagram briefly describes the general process of a configuration release. 1. the user operates the configuration release in the Portal 2. the Portal calls the interface of Admin Service to operate the release 3. Admin Service sends a ReleaseMessage to each Config Service after releasing the configuration 4. Config Service receives the ReleaseMessage and notifies the corresponding client ### 2.1.1 Implementation of Sending ReleaseMessage After the configuration is released, Admin Service needs to notify all Config Service that there is a configuration release, so that Config Service can notify the corresponding client to pull the latest configuration. Conceptually, this is a typical messaging scenario where Admin Service acts as a producer to send out messages and each Config Service acts as a consumer to consume the messages. The decoupling of Admin Service and Config Service can be well achieved by a Message Queue component. In terms of implementation, considering the actual usage scenario of Apollo and in order to minimize external dependencies, we did not use external messaging middleware, but implemented a simple message queue through the database. The implementation is as follows. 1. Admin Service inserts a message record into the ReleaseMessage table after the configuration release, and the message content is the AppId+Cluster+Namespace of the configuration release, see [DatabaseMessageSender](https://github.com/apolloconfig/apollo/blob/master/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/message/DatabaseMessageSender.java) 2. Config Service has a thread that scans the ReleaseMessage table once per second to see if there are new messages recorded, see [ReleaseMessageScanner](https://github.com/apolloconfig/apollo/blob/master/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/message/ReleaseMessageScanner.java) 3. Config Service notifies all message listeners if it finds a new message record [ReleaseMessageListener](https://github.com/apolloconfig/apollo/blob/master/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/message/ReleaseMessageListener.java) , as in [NotificationControllerV2](https://github.com/apolloconfig/apollo/blob/master/apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/controller/NotificationControllerV2.java), for the registration process of the message listener see [ConfigServiceAutoConfiguration](https://github.com/apolloconfig/apollo/blob/master/apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/ConfigServiceAutoConfiguration.java) 4. After NotificationControllerV2 gets the AppId+Cluster+Namespace of the configuration release, it will notify the corresponding client The schematic diagram is as follows. release-message-design ### 2.1.2 Config Service Notification Client Implementation The previous section briefly described how NotificationControllerV2 learns that a configuration has been released, but how does NotificationControllerV2 notify the client when it learns that a configuration has been released? The implementation is as follows. 1. the client initiates an Http request to the `notifications/v2` interface of the Config Service, which is [NotificationControllerV2](https://github.com/apolloconfig/apollo/blob/master/apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/controller/NotificationControllerV2.java), see [ RemoteConfigLongPollService](https://github.com/apolloconfig/apollo-java/blob/main/apollo-client/src/main/java/com/ctrip/framework/apollo/internals/RemoteConfigLongPollService.java) 2. NotificationControllerV2 does not return the result immediately, but via [Spring DeferredResult](http://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/context/request/async/DeferredResult.html) to put the request on hold 3. if no configuration of interest to the client is published within 60 seconds, then the Http status code 304 is returned to the client 4. If there is a configuration published that the client cares about, NotificationControllerV2 will call [setResult](http://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/context/request/async/DeferredResult.html#setResult-T-) method of DeferredResult, passing in the namespace information with configuration changes, while the request is returned immediately. After the client gets the namespace with configuration changes from the returned result, it will immediately request the Config Service to get the latest configuration of the namespace. # III. Client Design ![client-architecture](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/client-architecture.png) The above diagram briefly describes the principle of Apollo client implementation. 1. the client and the server maintain a long connection, so that they can get the first push of configuration updates. (achieved through Http Long Polling) 2. 2. the client also regularly pulls the latest configuration of the application from the Apollo Configuration Center server. * This is a fallback mechanism to prevent the configuration from being updated due to the failure of the push mechanism. * The client will report the local version of the timed pull, so in general, for the timed pull operation, the server will return 304 - Not Modified * Timing frequency defaults to pulling every 5 minutes. Clients can also override this by specifying System Property: `apollo.refreshInterval` at runtime, in minutes. 3. After the client gets the latest configuration of the application from the Apollo Configuration Center server, it will be saved in memory 4. the client will cache a copy of the configuration fetched from the server on the local file system * 4. the client will cache a copy of the configuration obtained from the server in the local file system. In case of service unavailability or network failure, the configuration can still be restored locally 5. applications can get the latest configuration from the Apollo client, subscribe to configuration update notifications ## 3.1 Principle of integration with Spring Apollo not only supports API to get the configuration, but also supports integration with Spring/Spring Boot, the integration principle is briefly described as follows. Spring has added `ConfigurableEnvironment` and `PropertySource` since version 3.1. * ConfigurableEnvironment * Spring's ApplicationContext will contain an Environment (implementing the ConfigurableEnvironment interface) * ConfigurableEnvironment itself contains a number of PropertySource * PropertySource * PropertySource * can be interpreted as a number of Key - Value configuration of properties The structure at runtime looks like this. ![Overview](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/environment.png) Note that there is an order of priority between PropertySource, if there is a Key present in more than one property source, then the property source in front of it takes precedence. So for the above example. * env.getProperty("key1") -> value1 * **env.getProperty("key2") -> value2** * env.getProperty("key3") -> value4 With the above principles understood, the means of integrating Apollo with Spring/Spring Boot comes into play: during the application startup phase, Apollo fetches the configuration from the remote end, then assembles it into a PropertySource and inserts it into the first one, as shown in the following diagram. ![Overview](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/environment-remote-source.png) The related code can be found in [PropertySourcesProcessor](https://github.com/apolloconfig/apollo-java/blob/main/apollo-client/src/main/java/com/ctrip/framework/apollo/spring/config/PropertySourcesProcessor.java) # IV. Usability considerations
    Scene Impact Downgrade reason
    A Config Service goes offline No effect Config Service is stateless, the client reconnects to other Config Service
    All Config Services offline Client cannot read the latest configuration, Portal has no effect When the client restarts, the local cache configuration file can be read. If it is a newly expanded machine, you can obtain the cached configuration file from other machines. For details, please refer to Java Client Usage Guide - 1.2.3 Local Cache Path
    A certain Admin Service goes offline No effect Admin Service is stateless, Portal reconnects to other Admin Service
    All Admin Services are offline The client is not affected, Portal cannot update the configuration
    A Portal goes offline No effect Portal domain name binds multiple servers through SLB, and points to an available server after retrying
    All Portals offline The client is not affected, Portal cannot update the configuration
    A data center goes offline No effect Multiple data centers are deployed, data is fully synchronized, and Meta Server/Portal domain names are automatically switched to other surviving data centers through SLB
    Database down The client is not affected, Portal cannot update the configuration After the Config Service is enabled configuration cache, read the configuration Fetch is not affected by database downtime
    # V. Monitoring related ## 5.1 Tracing ### 5.1.1 CAT Apollo client and server currently support [CAT](https://github.com/dianping/cat) automatic management, so if your company has deployed CAT internally, Apollo will automatically enable CAT management as long as cat-client is introduced . If you don't use CAT, don't worry, as long as cat-client is not introduced, Apollo will not enable CAT management. Apollo also provides Tracer-related SPI, which can be easily connected to its own company's monitoring system. For more information, please refer to [v0.4.0 Release Note](https://github.com/apolloconfig/apollo/releases/tag/v0.4.0) ### 5.1.2 SkyWalking You can refer to the [apollo-skywalking-pro sample](https://github.com/hepyu/k8s-app-config/tree/master/product) contributed by [@hepyu](https://github.com/hepyu/standard/apollo-skywalking-pro). ## 5.2 Metrics Since version 1.5.0, the Apollo server supports exposing metrics in prometheus format through `/prometheus`, such as `http://${someIp:somePort}/prometheus` ================================================ FILE: docs/en/design/apollo-introduction.md ================================================ #   # 1. What is Apollo ## 1.1 Background With the increasing complexity of program functions, the configuration of the program is increasing: switches of various functions, parameter configuration, server address... The expectations for program configuration are also getting higher and higher: real-time effective after configuration modification, grayscale release, sub-environment, sub-cluster management configuration, perfect authority, audit mechanism... In such a large environment, traditional methods such as configuration files and databases have become increasingly unable to meet developers' needs for configuration management. Apollo Configuration Center came into being! ## 1.2 Introduction to Apollo Apollo (Apollo) is a reliable distributed configuration management center, which was born in the Ctrip framework R&D department. It can centrally manage the configuration of different environments and different clusters of applications. After the configuration is modified, it can be pushed to the application side in real time, and it has standardized Features such as permissions and process governance are suitable for microservice configuration management scenarios. Apollo supports 4 dimensions to manage the configuration in Key-Value format: 1. application 2. environment 3. cluster (cluster) 4. namespace At the same time, Apollo is developed based on the open source model, open source address: https://github.com/ctripcorp/apollo ## 1.2 Basic Concepts of Configuration Since Apollo is positioned in the configuration center, it is necessary to briefly introduce what configuration is here. According to our understanding, the configuration has the following properties: * **Configuration is a read-only variable independent of the program** * Configuration is independent of the program first, the same program will behave differently under different configurations. * Second, the configuration is read-only to the program, the program changes its behavior by reading the configuration, but the program should not change the configuration. * Common configurations are: DB Connection Str, Thread Pool Size, Buffer Size, Request Timeout, Feature Switch, Server Urls, etc. * **Configuration accompanies the entire life cycle of the application** * Configuration runs through the entire life cycle of the application. The application is initialized by reading the configuration at startup, and the behavior is adjusted according to the configuration at runtime. * **Configuration can be loaded in multiple ways** * There are also many ways to load the configuration, the common ones are the internal hard code of the program, configuration files, environment variables, startup parameters, database-based, etc. **Configuration requires governance** * Permission control * Since the configuration can change the behavior of the program, incorrect configuration can even cause disasters, so the modification of the configuration must have a relatively complete permission control * Different environments, cluster configuration management * The same program often needs to have different configurations in different environments (development, testing, production) and different clusters (such as different data centers), so it is necessary to have complete environment and cluster configuration management * Framework class component configuration management * There is also a special kind of configuration - framework class component configuration, such as the configuration of CAT client. * Although this type of framework component is developed and maintained by other teams, the runtime is in the actual business application, so it can be considered that the framework component is also a part of the application in essence. * The configuration corresponding to such components also needs to have a relatively complete management method. # 2. Why Apollo It is precisely based on the particularity of configuration that Apollo has been determined to become a configuration publishing platform with governance capabilities from the beginning of its design. Currently, it provides the following features: * **Unified management of the configuration of different environments and different clusters** * Apollo provides a unified interface to centrally manage the configuration of different environments, clusters, and namespaces. * The same code is deployed in different clusters and can have different configurations, such as the address of zookeeper, etc. * It is convenient to support multiple different applications to share the same configuration through namespace (namespace), and also allows applications to override the shared configuration * **Configuration changes take effect in real time (hot release)** * After the user modifies the configuration in Apollo and publishes it, the client can receive the latest configuration in real time (1 second) and notify the application **Version release management** * All configuration releases have a version concept, which can easily support configuration rollback **Greyscale Release** * Support configured grayscale publishing. For example, after clicking publish, it will only take effect on some application instances. After observing for a period of time, there is no problem before pushing to all application instances. * **Permission management, release audit, operation audit** * The management of application and configuration has a complete authority management mechanism, and the management of configuration is also divided into two links: editing and publishing, thereby reducing human errors. * All operations have audit logs, which can easily track problems * **Client configuration information monitoring** * You can easily see which instances the configuration is being used on the interface **Global Search Configuration Items** - A fuzzy search of the key and value of a configuration item finds in which application, environment, cluster, namespace the configuration item with the corresponding value is used - It is easy for administrators and SRE roles to quickly and easily find and change the configuration values of resources by highlighting, paging and jumping through configurations **Java and .Net native clients available** * Provides native clients of Java and .Net for easy application integration * Support Spring Placeholder, Annotation and Spring Boot's ConfigurationProperties for easy application use (requires Spring 3.1.1+) * Also provides Http interface, non-Java and .Net applications can also be easily used **Provide open platform API** * Apollo itself provides a relatively complete unified configuration management interface, which supports multi-environment, multi-data center configuration management, permissions, process governance and other features. However, Apollo does not impose too many restrictions on configuration modifications for the sake of generality. As long as it conforms to the basic format, it can be saved. It will not perform targeted verification for different configuration values, such as database username and password, Redis service address, etc. * For this type of application configuration, Apollo supports the application side to modify and publish the configuration in Apollo through the open platform API, and has complete authorization and permission control **Easy to deploy** * As a basic service, the configuration center has very high availability requirements, which requires Apollo to have as few external dependencies as possible * Currently the only external dependency is MySQL, so deployment is very simple, as long as Java and MySQL are installed, Apollo can run * Apollo also provides a packaging script, which can generate all the required installation packages with one click, and supports custom runtime parameters # 3. Apollo at a glance ## 3.1 Basic model The following is the basic model of Apollo: 1. The user modifies and publishes the configuration in the configuration center 2. The configuration center notifies the Apollo client that there is a configuration update 3. The Apollo client pulls the latest configuration from the configuration center, updates the local configuration and notifies the application ![basic-architecture](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/basic-architecture.png) ## 3.2 Interface overview ![apollo-home-screenshot](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/apollo-home-screenshot.jpg) The picture above is the configuration home page of a project in the Apollo Configuration Center * The environment list module at the top left of the page displays all environments and clusters, and users can switch at any time. * The configuration information of two namespaces (application and FX.apollo) is displayed in the center of the page, which is displayed and edited in table mode by default. Users can also switch to text mode to view and edit as files. * Operations such as publishing, rollback, grayscale, authorization, viewing change history and publishing history can be easily performed on the page ## 3.3 Add or modify configuration items Users can easily add/modify configuration items through the configuration center interface. For more usage instructions, please refer to [Application Access Guide](en/portal/apollo-user-guide) ![edit-item-entry](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/edit-item-entry.png) Enter configuration information: ![edit-item](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/edit-item.png) ## 3.4 Release configuration Publish the configuration through the configuration center: ![publish-items](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/publish-items-entry.png) Fill in the release information: ![publish-items](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/publish-items.png) ## 3.5 Client get configuration (Java API sample) After the configuration is released, it can be obtained on the client side. Taking Java as an example, the sample code for obtaining the configuration is as follows. Apollo client also supports integration with Spring. For more client usage instructions, please refer to [Java Client Usage Guide](en/client/java-sdk-user-guide) and [.Net Client Usage Guide](en/client/dotnet-sdk-user-guide). ```java Config config = ConfigService.getAppConfig(); Integer defaultRequestTimeout = 200; Integer requestTimeout = config.getIntProperty("requestTimeout", defaultRequestTimeout); ``` ## 3.6 Client monitoring configuration changes By obtaining the configuration code above, the application can obtain the latest configuration in real time. However, in some scenarios, the application also needs to be notified when the configuration changes, such as the switching of database connections, so Apollo also provides the function of monitoring configuration changes. The Java example is as follows: ```java Config config = ConfigService.getAppConfig(); config.addChangeListener(new ConfigChangeListener() { @Override public void onChange(ConfigChangeEvent changeEvent) { for (String key : changeEvent.changedKeys()) { ConfigChange change = changeEvent.getChange(key); System.out.println(String.format( "Found change - key: %s, oldValue: %s, newValue: %s, changeType: %s", change.getPropertyName(), change.getOldValue(), change.getNewValue(), change.getChangeType())); } } }); ``` ## 3.7 Spring integration example Apollo and Spring can also be easily integrated. You only need to mark `@EnableApolloConfig` to get configuration information through `@Value`: ```java @Configuration @EnableApolloConfig public class AppConfig {} ``` ```java @Component public class SomeBean { //The value of timeout will be updated automatically @Value("${request.timeout:200}") private int timeout; } ``` # 4. Apollo in depth Through the above introduction, I believe you have a preliminary understanding of Apollo, and I believe that most of the usage scenarios have been covered. The next section will mainly introduce Apollo's cluster management, namespace management and the corresponding configuration acquisition rules. ## 4.1 Core Concepts Before introducing advanced features, we need to understand a few core concepts in Apollo. 1. **application (application)** * Apollo clients need to know who the current application is at runtime, so they can go get the corresponding configuration. * Each application needs to have a unique identity -- appId, we believe that the application identity follows the code, so it needs to be configured in the code, see [Java Client Usage Guide](en/client/java-sdk-user-guide) for more information. 2. **environment (environment)** * Configure the corresponding environment, Apollo client needs to know which environment the current application is in at runtime, so that it can go get the application configuration * We believe that the environment is independent of the code, the same code deployed in different environments should be able to access the configuration of different environments * So the environment is specified by default by reading the configuration on the machine (the env property in server.properties), but for development convenience, we also support runtime specification by System Property, etc. For more information, see [Java Client User Guide](en/client/java-sdk-user-guide). 3. **cluster (cluster)** * Grouping of different instances of an application, for example, typically by data center, dividing the application instances in the Shanghai server room into one cluster, and dividing the application instances in the Beijing server room into another cluster. * For different clusters, the same configuration can have different values, such as zookeeper address. * Clusters are specified by default by reading the configuration on the machine (idc property in server.properties), but they are also supported at runtime by System Property, see [Java Client Usage Guide](en/client/java-sdk-user-guide) for more information. 4. **namespace (namespace)** * A grouping of different configurations under an application. Namespace can be simply compared to a file, where different types of configurations are stored in different files, such as database configuration files, RPC configuration files, the application's own configuration files, etc. * Applications can directly read the configuration namespace of public components, such as DAL, RPC, etc. * The application can also adjust the configuration of the public component by inheriting the configuration namespace of the public component, such as the initial database connection number of DAL ## 4.2 Customizing Cluster > This section is only required if the application needs to apply different configurations to different clusters, if there is no relevant need, you can skip this section] For example, if we have applications deployed in both data center A and data center B, then if we want the configuration of the two data centers to be different, we can solve it by creating a new cluster. ### 4.2.1 New Cluster Only the project administrator has access to the new Cluster. The administrator can see the "Add Cluster" button on the left side of the page. ![create-cluster](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/create-cluster.png) When you click it, you will enter the Add Cluster page. Generally, you can divide the clusters by data center, such as SHAJQ, SHAOY, etc. However, custom clusters are also supported. However, custom clusters are also supported, for example, you can create a cluster for a machine in room A and a machine in room B, using one set of configurations. ![create-cluster-detail](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/create-cluster-detail.png) ### 4.2.2 Adding configuration in Cluster and publishing After the cluster is successfully added, you can add configuration to the cluster. First, you need to switch to SHAJQ cluster as shown in the figure below, and then the configuration addition process is the same as [3.3 Adding/Modifying Configuration Items](#_33-add-or-modify-configuration-items), so we won't go over it here. ![cluster-created](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/cluster-created.png) ### 4.2.3 Specifying the Cluster to which the application instance belongs Apollo will use the data center where the application instance is located as the cluster by default, so no additional configuration is needed if the two are the same. If the cluster and data center are not the same, then you need to specify the runtime cluster by using System Property. * -Dapollo.cluster=SomeCluster * Note here that `apollo.cluster` is all lowercase ## 4.3 Customizing Namespace > [This section is only required for public component configuration or shared configuration of multiple applications, you can skip this section if you don't have any relevant requirements] If the application has public components (such as hermes-producer, cat-client, etc.) for other applications to use, you need to implement the configuration of public components through custom namespace. ### 4.3.1 New Namespace Take hermes-producer as an example, you need to create a new namespace first, only the project administrator has permission to create namespace, the administrator can see the "Add Namespace" button on the left side of the page. ![create-namespace](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/create-namespace.png) Apollo will prefix the namespace with the department the application belongs to, such as FX. ![create-namespace-detail](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/create-namespace-detail.png) ### 4.3.2 Associating to environments and clusters After the namespace is created, you need to select which environments and clusters to use it under ![link-namespace-detail](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/link-namespace-detail.png) ### 4.3.3 Add configuration items to the Namespace Next, add a configuration item under this new namespace ![add-item-in-new-namespace](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/add-item-in-new-namespace.png) Once added, you can see the configuration in the FX.Hermes.Producer namespace. ![item-created-in-new-namespace](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/item-created-in-new-namespace.png) ### 4.3.4 Publish namespace configuration ![publish-items-in-new-namespace](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/publish-items-in-new-namespace.png) ### 4.3.5 Client-side fetching of Namespace configuration Apollo client also supports integration with Spring, see [Java Client User Guide](en/client/java-sdk-user-guide) and [Net Client Net client](en/client/dotnet-sdk-user-guide). ```java Config config = ConfigService.getConfig("FX.Hermes.Producer"); Integer defaultSenderBatchSize = 200; Integer senderBatchSize = config.getIntProperty("sender.batchsize", defaultSenderBatchSize); ``` ### 4.3.6 Client listens for Namespace configuration changes ```java Config config = ConfigService.getConfig("FX.Hermes.Producer"); config.addChangeListener(new ConfigChangeListener() { @Override public void onChange(ConfigChangeEvent changeEvent) { System.out.println("Changes for namespace " + changeEvent.getNamespace()); for (String key : changeEvent.changedKeys()) { ConfigChange change = changeEvent.getChange(key); System.out.println(String.format( "Found change - key: %s, oldValue: %s, newValue: %s, changeType: %s", change.getPropertyName(), change.getOldValue(), change.getNewValue(), change.getChangeType())); } } }); ``` ### 4.3.7 Sample Spring integration ``` java @Configuration @EnableApolloConfig("FX.Hermes.Producer") public class AppConfig {} ``` ```java @Component public class SomeBean { //the value of timeout will be updated automatically @Value("${request.timeout:200}") private int timeout; } ``` ## 4.4 Configuring fetch rules > [This section is only needed if the application is customized with a cluster or namespace, if there is no related need, you can skip this section] After the concept of cluster, the rules of configuration become important. For example, if the application is deployed in server room A, but no new cluster is created in Apollo, what is the behavior of Apollo at this time? Or if cluster=SomeCluster is specified at runtime, but no new cluster is created in Apollo, what is the behavior of Apollo at this time? The next step is to introduce the rules for configuration acquisition. ### 4.4.1 Application's own configuration acquisition rules When the application uses the following statement to get the configuration, we call it getting the application's own configuration, that is, the configuration of the application namespace of the application itself. ```java Config config = ConfigService.getAppConfig(); ``` The rules for getting the configuration for this case, in short, are as follows. 1. first look for the configuration of the runtime cluster (specified by apollo.cluster) 2. if not found, then look for the configuration of the data center cluster 3. if still not found, then return the configuration of the default cluster The diagram is as follows. ![application-config-precedence](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/application-config-precedence.png) So if the application is deployed in datacenter A, but the user has not created a cluster in Apollo, then the configuration obtained is that of the default cluster (default). If the application is deployed in data center A, and SomeCluster is specified at runtime, but no cluster is created in Apollo, then the configuration obtained is that of data center A cluster, and if data center A cluster is not configured, then the configuration obtained is that of default cluster (default). ### 4.4.2 Public component configuration acquisition rules Take `FX.Hermes.Producer` as an example, hermes producer is a public component published by hermes. When the following statement is used to get the configuration, we call it getting the configuration of the public component. ```java Config config = ConfigService.getConfig("FX.Hermes.Producer"); ``` The rules for getting the configuration for this case, in short, are as follows. 1. first get the configuration of the `FX.Hermes.Producer` namespace under the current application Then get the configuration of `FX.Hermes.Producer` namespace under the hermes application 3. The concatenation of the above two configurations is the final used configuration, if there is any part with the same key, the current application takes precedence The diagram is as follows. ![public-namespace-config-precedence](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/public-namespace-config-precedence.png) In this way, the configuration management of the framework class components is achieved. The framework component provider provides the default values for the configuration, and the application can override them if it has special needs. ## 4.5 Overall design ![overall-architecture](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/overall-architecture.png) The above diagram briefly describes the overall design of Apollo, which we can see from bottom to top. * Config Service provides configuration reading, pushing and other functions, and the service object is Apollo client * Admin Service provides configuration modification and publishing, and the service object is Apollo Portal (management interface). * Config Service and Admin Service are both multi-instance, stateless deployments, so they need to register themselves to Eureka and keep the heartbeat * On top of Eureka, we erected a Meta Server to encapsulate Eureka's service discovery interface * Client accesses Meta Server via domain name to get Config Service service list (IP+Port), and then accesses the service directly via IP+Port, while doing load balance and error retry on the Client side. * Portal accesses Meta Server through domain name to get the Admin Service service list (IP+Port), and then accesses the service directly through IP+Port, and at the same time does load balance and error retry on the Portal side. * To simplify deployment, we will actually deploy the three logical roles Config Service, Eureka and Meta Server in the same JVM process ### 4.5.1 Why Eureka Why do we use Eureka as a service registry instead of the traditional zk, etcd? I roughly summarized the following reasons. * It provides a complete Service Registry and Service Discovery implementation * First of all, it provides a complete implementation and has also withstood the test of Netflix's own production environment, so it's relatively painless to use. * Seamless integration with Spring Cloud * Our project itself uses Spring Cloud and Spring Boot, and Spring Cloud has a very comprehensive set of open source code to integrate with Eureka, so it's very easy to use. * In addition, Eureka supports starting in our application's own container, which means that after our application is started, it acts as both an Eureka and a service provider. This greatly improves the availability of the service. * **This is the main reason why we chose Eureka over zk, etc. In order to improve the availability of the configuration center and reduce the complexity of deployment, we need to minimize external dependencies as much as possible.** * Open Source * The last point is open source. Since the code is open source, it is very easy for us to understand how it is implemented and troubleshoot problems. ## 4.6 Client Design ![client-architecture](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/client-architecture.png) The above diagram briefly describes how the Apollo client is implemented. 1. the client maintains a long connection with the server so that it can be the first to get configuration updates pushed to it. The client also regularly pulls the latest configuration of the application from the Apollo Configuration Center server. * This is a fallback mechanism to prevent the configuration from not being updated due to the failure of the push mechanism. * The client will report the local version when it is pulled, so in general, the server will return 304 - Not Modified for the timed pull operation. * Timing frequency defaults to pulling every 5 minutes. Clients can also override this by specifying System Property: `apollo.refreshInterval` at runtime, in minutes. 3. After the client gets the latest configuration of the application from the Apollo Configuration Center server, it will be saved in memory 4. the client will cache a copy of the configuration fetched from the server on the local file system * In case of service unavailability or network failure, the configuration can still be restored locally 5. the application gets the latest configuration from the Apollo client and subscribes to configuration update notifications ### 4.6.1 Apollo-Configuration auto-push mechanism As mentioned earlier, Apollo client and server maintain a long connection to get the first push of configuration updates. The long connection is actually implemented through Http Long Polling, specifically. * The client initiates an Http request to the server * The server will hold the connection for 60 seconds * If there is a configuration change that the client cares about within 60 seconds, the held client request will return immediately and inform the client of the configuration change namespace information, and the client will pull the latest configuration of the corresponding namespace accordingly * If there is no configuration change that the client cares about within 60 seconds, then the Http status code 304 will be returned to the client * The client will re-initiate the connection immediately after receiving the server-side request, going back to the first step Considering that there will be tens of thousands of clients initiating long connections to the server, on the server side we use async servlet (Spring DeferredResult) to serve Http Long Polling requests. ## 4.7 Availability considerations The following table describes the availability of Apollo under different scenarios. | Scenario | Impact | Downgrade | Reason | | ----------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ | | One of config services goes offline | No effect | | The Config service is stateless, and the client reconnects to other config services | | All config services are offline | The client cannot read the latest configuration, and Portal has no effect | When the client restarts, the local cache configuration file can be read | | | One of admin services goes offline | No effect | | The Admin service has no status, and Portal reconnects to other admin services | | All admin services are offline | The client is not affected, and the portal cannot update the configuration | | | | One of portals goes offline | No effect | | The portal domain name is bound to multiple servers through slb, and points to an available server after retrying | | All portals are offline | The client is not affected, and the portal cannot update the configuration | | | | One of data center goes offline | No effect | | Multi-data center deployment, data is fully synchronized, Meta Server/Portal domain name is automatically switched to other surviving data centers through slb | # 5、Contribute to Apollo Apollo is developed in open source mode from the beginning of development, so you are also very welcome to join in the interest and spare capacity of friends. The server-side development is in Java, based on Spring Cloud and Spring Boot framework. The client side currently provides both Java and . GitHub address: https://github.com/ctripcorp/apollo Welcome to submit a Pull Request! ================================================ FILE: docs/en/extension/portal-how-to-enable-email-service.md ================================================ When configuring the release, we hope to release the information email notification to the relevant person in charge. The actions currently supported for sending emails include: normal publishing, grayscale publishing, full publishing, and rollback. The notification objects include: personnel with namespace editing and publishing permissions, and the person in charge of the app. Since each company's mail service often has different implementations, Apollo defines some SPIs for decoupling. The key to Apollo's access to mail services is to implement these SPIs. ## 1. Implementation-1: Use the smtp mail service provided by Apollo ### 1.1 Access steps Configure the following parameters in the ApolloPortalDB.ServerConfig table or through the Administrator Tools - System Parameters page, and the modification will take effect in real time within one minute. as follows: * **email.enabled** Set to true to enable the default smtp mail service * **email.config.host** smtp service address, such as `smtp.163.com` * **email.config.user** smtp account username * **email.config.password** smtp account password * **email.supported.envs** A comma-separated list of environments that support sending emails. We don't want posting emails to become spam for users, only posting actions under certain circumstances will send emails. * **email.sender** The sender of the email, can not be configured, the default is `email.config.user`. * **apollo.portal.address** The address of the Apollo Portal. It is convenient for users to click to jump from the email to the Apollo Portal to view the detailed release information. * **email.template.framework** Email content template framework. The email content is templated and configurable to facilitate management and change of email content. * **email.template.release.module.diff** The diff module for publishing emails. * **email.template.rollback.module.diff** The diff module for rolling back emails. * **email.template.release.module.rules** Grayscale rules module for grayscale release. We provide [Sample Email Template](en/extension/portal-how-to-enable-email-service?id=_3-email-template-sample) for your convenience. ## 2. Implementation-2: Access the company's unified mail service Similar to SSO, each company also has its own mail service implementation, so we define [EmailService](https://github.com/apolloconfig/apollo/blob/master/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/EmailService.java) interface, there are two implementation classes: 1. CtripEmailService: EmailService implemented by Ctrip 2. DefaultEmailService: smtp implementation ### 2.1 Access steps 1. Provide your company's [EmailService](https://github.com/apolloconfig/apollo/blob/master/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/EmailService.java) implementation and in [EmailConfiguration](https://github.com/apolloconfig/apollo/blob/master/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/configuration/EmailConfiguration.java) registered. 2. Configure the following parameters in the ApolloPortalDB.ServerConfig table, or through the Administrator Tools - System Parameters page, and the modification will take effect in one minute. as follows: * **email.supported.envs** A comma-separated list of environments that support sending emails. We don't want posting emails to become spam for users, only posting actions under certain circumstances will send emails. * **email.sender** The sender of the email. * **apollo.portal.address** The address of the Apollo Portal. It is convenient for users to click to jump from the email to the Apollo Portal to view the detailed release information. * **email.template.framework** Email content template framework. The email content is templated and configurable to facilitate management and change of email content. * **email.template.release.module.diff** The diff module for publishing emails. * **email.template.rollback.module.diff** The diff module for rolling back emails. * **email.template.release.module.rules** Grayscale rules module for grayscale release. We provide [Sample Email Template](en/extension/portal-how-to-enable-email-service?id=_3-email-template-sample) for your convenience. Note: using different implementations at runtime is achieved through [Profiles](http://docs.spring.io/autorepo/docs/spring-boot/current/reference/html/boot-features-profiles.html), For example, if your own Email implementation is in the `custom` profile, you can specify -Dapollo_profile=github,custom in the packaging script. Among them, `github` is a required profile of Apollo, which is used for database configuration, and `custom` is a profile that you implement yourself. Also note that in [EmailConfiguration](https://github.com/apolloconfig/apollo/blob/master/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/configuration/EmailConfiguration.java) to modify the default implementation condition `@Profile({"!custom"})`. ### 2.2 Related code 1. [ConfigPublishListener](https://github.com/apolloconfig/apollo/blob/master/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/listener/ConfigPublishListener.java) to monitor the release event, call emailbuilder to build email content, and then call EmailService to send emails 2. The [emailbuilder](https://github.com/apolloconfig/apollo/tree/master/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/component/emailbuilder) package is the build email realization of content 3. [EmailService](https://github.com/apolloconfig/apollo/blob/master/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/EmailService.java) Email sending Serve 4. [EmailConfiguration](https://github.com/apolloconfig/apollo/blob/master/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/configuration/EmailConfiguration.java) Mail service registration class ## 3. Email template sample The following are the template content styles for publishing emails and rolling back emails. The email templates are in html format. When sending emails in html format, some additional processing may be required, depending on the implementation of each company's email service. To reduce character count, templates are compressed and self-formatted to improve readability. ### 3.1 email.template.framework ```html < /head>

    Post basic information

    AppId#{appId}Environment#{ env}cluster#{clusterName}Namespace#{namespaceName}
    Publisher#{operator}release time#{releaseTime}release Title#{releaseTitle}Comment#{releaseComment}
    #{diffModule}#{rulesModule}
    Click to view detailed release information

    If you have any questions about using Apollo, please check document, or reply directly to this email inquiry. ``` > Note: To use this template, you need to configure apollo.portal.address in the system parameters of the portal to point to the address of the apollo portal ### 3.2 email.template.release.module.diff ```html

    Changed Configuration Items

    #{diffContent}
    Type Key Old Value New Value
    ``` ### 3.3 email.template.rollback.module.diff ```html


    Changed Configuration Items

    #{diffContent}
    Type Key Before Rollback After Rollback

    ``` ### 3.4 email.template.release.module.rules ```html

    Grayscale Rules

    #{rulesContent}
    ``` ### 3.5 Sample Release Email ![发布邮件模板](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/email-template-release.png) ### 3.6 Sample Rollback Email ![回滚邮件模板](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/email-template-rollback.png) ================================================ FILE: docs/en/extension/portal-how-to-enable-session-store.md ================================================ Starting with version 1.9.0, apollo-portal adds session sharing support so that session sharing can be implemented in cluster deployments of apollo-portal. ## Usage ### 1. JDBC-based session sharing (default) The default configuration is JDBC-based session sharing. So clear the session sharing related configuration and configure the database connection, the configuration to be cleared is as follows `spring.session.store-type` configuration in the external configuration file (properties/yml) Environment variables inside the `SPRING_SESSION_STORE_TYPE` System Property inside `spring.session.store-type` There are several ways to set the database connection, from highest to lowest priority, as follows #### 1.1 System Property ```bash -Dspring.datasource.url=xxx -Dspring.datasource.username=xxx -Dspring.datasource.password=xxx ``` #### 1.2 Environment variables ```bash export SPRING_DATASOURCE_URL="xxx" export SPRING_DATASOURCE_USERNAME="xxx" export SPRING_DATASOURCE_PASSWORD="xxx" ``` #### 1.3 External configuration files For example `config/application-github.properties` ```properties spring.datasource.url=xxx spring.datasource.username=xxx spring.datasource.password=xxx ``` #### 1.4 Table on initializing session for non-mysql databases If you need to use other databases, you can refer to the other table creation sql provided by [spring-session](https://github.com/spring-projects/spring-session) Please select the corresponding sql script according to the database you are using - [db2.sql](https://github.com/spring-projects/spring-session/blob/faee8f1bdb8822a5653a81eba838dddf224d92d6/spring-session-jdbc/src/main/resources/org/springframework/session/jdbc/schema-db2.sql) - [derby.sql](https://github.com/spring-projects/spring-session/blob/faee8f1bdb8822a5653a81eba838dddf224d92d6/spring-session-jdbc/src/main/resources/org/springframework/session/jdbc/schema-derby.sql) - [h2.sql](https://github.com/spring-projects/spring-session/blob/faee8f1bdb8822a5653a81eba838dddf224d92d6/spring-session-jdbc/src/main/resources/org/springframework/session/jdbc/schema-h2.sql) - [hsqldb.sql](https://github.com/spring-projects/spring-session/blob/faee8f1bdb8822a5653a81eba838dddf224d92d6/spring-session-jdbc/src/main/resources/org/springframework/session/jdbc/schema-hsqldb.sql) - [mysql.sql](https://github.com/spring-projects/spring-session/blob/faee8f1bdb8822a5653a81eba838dddf224d92d6/spring-session-jdbc/src/main/resources/org/springframework/session/jdbc/schema-mysql.sql) - [oracle.sql](https://github.com/spring-projects/spring-session/blob/faee8f1bdb8822a5653a81eba838dddf224d92d6/spring-session-jdbc/src/main/resources/org/springframework/session/jdbc/schema-oracle.sql) - [postgresql.sql](https://github.com/spring-projects/spring-session/blob/faee8f1bdb8822a5653a81eba838dddf224d92d6/spring-session-jdbc/src/main/resources/org/springframework/session/jdbc/schema-postgresql.sql) - [sqlite.sql](https://github.com/spring-projects/spring-session/blob/faee8f1bdb8822a5653a81eba838dddf224d92d6/spring-session-jdbc/src/main/resources/org/springframework/session/jdbc/schema-sqlite.sql) - [sqlserver.sql](https://github.com/spring-projects/spring-session/blob/faee8f1bdb8822a5653a81eba838dddf224d92d6/spring-session-jdbc/src/main/resources/org/springframework/session/jdbc/schema-sqlserver.sql) - [sybase.sql](https://github.com/spring-projects/spring-session/blob/faee8f1bdb8822a5653a81eba838dddf224d92d6/spring-session-jdbc/src/main/resources/org/springframework/session/jdbc/schema-sybase.sql) ### 2. Redis-based session sharing There are several ways to set this up, from highest to lowest priority, as follows Note: redis also supports cluster and sentry mode, configured in the standard `Spring Data Redis` mode (configuration items starting with `spring.redis`), please study the `Spring Data Redis` related documentation or consult the `Spring Data` Group for details #### 2.1 System Property ```bash -Dspring.session.store-type=redis -Dspring.redis.host=xxx -Dspring.redis.port=xxx -Dspring.redis.username=xxx -Dspring.redis.password=xxx ``` #### 2.2 Environment variables ```bash export SPRING_SESSION_STORE_TYPE="redis" export SPRING_REDIS_HOST="xxx" export SPRING_REDIS_PORT="xxx" export SPRING_REDIS_USERNAME="xxx" export SPRING_REDIS_PASSWORD="xxx" ``` #### 2.3 External configuration files For example ``config/application-github.properties`'' ```properties spring.session.store-type=redis spring.redis.host=xxx spring.redis.port=xxx spring.redis.username=xxx spring.redis.password=xxx ``` ### 3. Do not enable session sharing There are several ways to set this, in descending order of priority, as follows #### 3.1 System Property ```bash -Dspring.session.store-type=none ``` #### 3.2 Environment Variables ```bash export SPRING_SESSION_STORE_TYPE="none" ``` #### 3.3 External configuration files For example `config/application-github.properties` ```properties spring.session.store-type=none ``` ================================================ FILE: docs/en/extension/portal-how-to-enable-webhook-notification.md ================================================ Starting with version 1.8.0, Apollo has added webhook support, which will be triggered to send you a message when your configuration is released. ## How to enable webhook > The configuration items are stored in the table named ApolloPortalDB.ServerConfig. You may also config them in `Admin Tools - System Configuration` page. The configuration changes will take effect within one minute. 1. webhook.supported.envs The environment lists to enable webhook support. Multiple environments should be separated by commas, e.g., ``` DEV, FAT, UAT, PRO ``` 2. config.release.webhook.service.url Config the url that receives HTTP post request sent by webhook for notifying. Multiple urls should be separated by commas, e.g., ``` http://www.xxx.com/webhook1,http://www.xxx.com/webhook2 ``` ## How to use 1. URL parameters parameter name | parameter annotation --- | --- env | env of the configuration to be released 2. Request body sample ```json { "appId": "", // appId "clusterName": "", // cluster "namespaceName": "", // namespace "operator": "", // modifier "releaseId": 2, // release id "releaseTitle": "", // release title "releaseComment": "", // release Comment "releaseTime": "", // release time eg:2020-01-01T00:00:00.000+0800 "configuration": [ { // all configurations to be released; also applies to gray release "firstEntity": "", // key of configuration "secondEntity": "" // value of configuration } ], "isReleaseAbandoned": false, "previousReleaseId": 1, // releaseId of latest formal release "operation": // 0-normal release 1-rollabck 2-gray release 4-full release "operationContext": { // property setting for operation "isEmergencyPublish": true/false, // emergercy release or not "rules": [ { // rules for gray release "clientAppId": "", // appId "clientIpList": [ "10.0.0.2", "10.0.0.3" ] // IP lists } ], "branchReleaseKeys": [ "", "" ] // key of Gray Release } } ``` ================================================ FILE: docs/en/extension/portal-how-to-implement-user-login-function.md ================================================ Apollo is a configuration management system and will provide authority management (Authorization), theoretically it is not responsible for the implementation of user login authentication function (Authentication). So Apollo defines some SPI's for decoupling and the key to Apollo access login is to implement these SPI's. ## Implementation 1: Simple authentication using Spring Security provided by Apollo Apollo provides a simple authentication version of Http Basic using Spring Security since 0.9.0 for this situation. Use the following steps. ### 1. Install 0.9.0 or above > If the previous version is 0.8.0, you need to import [apolloportaldb-v080-v090.sql](https://github.com/apolloconfig/apollo/blob/master/scripts/sql/profiles/mysql-default/delta/v080-v090/apolloportaldb-v080-v090.sql) Checking ApolloPortalDB, the `Users` table should already exist and have an initial record. The initial user name is apollo and the password is admin. | Id | Username | Password | Email | Enabled | | ---- | -------- | ------------------------------------------------------------ | --------------- | ------- | | 1 | apollo | $2a$10$7r20uS.BQ9uBpf3Baj3uQOZvMVvB1RN3PYoKE94gtz2.WAOuiiwXS | apollo@acme.com | 1 | ### 2. Reboot the Portal Make sure `-Dapollo_profile=github,auth` if it is started by IDE ### 3. Add users Super Administrator can add users by opening `Administrator Tools - User Management` after logging into the system. ### 4. Change user password After logging in, open `Administrator Tools - User Management` and enter the user name and password to change the user's password, we recommend changing the password of super administrator apollo at the same time. ## Implementation 2: Access to LDAP Starting from version 1.2.0, Apollo supports ldap protocol login, which can be configured as follows. > If you use helm chart deployment method, it is recommended to implement it by configuration method without modifying the image, you can refer to [Enable LDAP support](en/deployment/distributed-deployment-guide?id=_241449-enable-ldap-support) ### 1. OpenLDAP access method #### 1.1 Configure `application-ldap.yml` After unpacking `apollo-portal-x.x.x-github.zip`, create `application-ldap.yml` in the `config` directory with the following reference ([sample](https://github.com/apolloconfig/apollo/blob/master/apollo-portal/src/main/resources/application-ldap-openldap-sample.yml)) , the relevant content needs to be adjusted according to the specific situation: ```yml spring: ldap: base: "dc=example,dc=org" username: "cn=admin,dc=example,dc=org" # Configure administrator account for searching and matching users password: "password" search-filter: "(uid={0})" # user filter, use this filter to search for users when logging in urls: - "ldap://localhost:389" ldap: mapping: # Configure the ldap attribute object-class: "inetOrgPerson" # ldap user objectClass configuration login-id: "uid" # ldap user unique id, used as the login id user-display-name: "cn" # ldap user name, used as display name email: "mail" # ldap mailbox attribute ``` ##### 1.1.1 Filtering users based on memberOf With OpenLDAP [memberOf feature enabled](https://myanbin.github.io/post/enable-memberof-in-openldap.html), the filter can be configured to narrow down the users to search for. ```yml spring: ldap: base: "dc=example,dc=org" username: "cn=admin,dc=example,dc=org" # Configure admin account for searching and matching users password: "password" search-filter: "(uid={0})" # user filter, use this filter to search for users when logging in urls: - "ldap://localhost:389" ldap: mapping: # Configure the ldap attribute object-class: "inetOrgPerson" # ldap user objectClass configuration login-id: "uid" # ldap user unique id, used as the login id user-display-name: "cn" # ldap user name, used as display name email: "mail" # ldap mailbox attribute filter: # Configuration filter, currently only memberOf is supported memberOf: "cn=ServiceDEV,ou=DEV,dc=example,dc=org|cn=WebDEV,ou=DEV,dc=example,dc=org" # Only allow users with memberOf attribute of ServiceDEV and WebDEV to access ``` ##### 1.1.2 Filtering users based on Group Starting with version 1.3.0, we support filtering users based on Group, which allows you to control that only users of a specific Group can log in and use apollo. ```yml spring: ldap: base: "dc=example,dc=org" username: "cn=admin,dc=example,dc=org" # Configure admin account for searching and matching users password: "password" search-filter: "(uid={0})" # user filter, use this filter to search for users when logging in urls: - "ldap://localhost:389" ldap: mapping: # Configure the ldap attribute object-class: "inetOrgPerson" # ldap user objectClass configuration login-id: "uid" # ldap user unique id, used as login id rdnKey: "uid" # ldap rdn key user-display-name: "cn" # ldap user name, used as display name email: "mail" # ldap mailbox attribute group: # enable group search, only users of a specific group can login to apollo after enabling object-class: "posixGroup" # Configure groupClassName group-base: "ou=group" # group search base group-search: "(&(cn=dev))" # group filter group-membership: "memberUid" # group memberShip eg. member or memberUid ``` #### 1.2 Configure `startup.sh` Modify `scripts/startup.sh` to specify `spring.profiles.active` as `github,ldap`. ```bash SERVICE_NAME=apollo-portal ## Adjust log dir if necessary LOG_DIR=/opt/logs ## Adjust server port if necessary SERVER_PORT=8070 export JAVA_OPTS="$JAVA_OPTS -Dspring.profiles.active=github,ldap" ``` ### 2. Active Directory access method #### 2.1 Configure `application-ldap.yml` After unpacking `apollo-portal-x.x.x-github.zip`, create `application-ldap.yml` in the `config` directory with the following reference ([sample](https://github.com/apolloconfig/apollo/blob/master/apollo-portal/src/main/resources/application-ldap-activedirectory-sample.yml)) , the relevant content needs to be adapted to the specific case: ```yml spring: ldap: base: "dc=example,dc=com" username: "admin" # Configure administrator account for searching and matching users password: "password" search-filter: "(sAMAccountName={0})" # user filter, use this filter to search for users when logging in urls: - "ldap://1.1.1.1:389" ldap: mapping: # Configure the ldap attribute object-class: "user" # ldap user objectClass configuration login-id: "sAMAccountName" # the unique id of the ldap user, used as the login id user-display-name: "cn" # ldap user name, used as display name email: "userPrincipalName" # ldap mailbox attribute filter: # optional, configure filter, currently only support memberOf memberOf: "CN=ServiceDEV,OU=test,DC=example,DC=com|CN=WebDEV,OU=test,DC=example,DC=com" # Only allow users with memberOf attribute of ServiceDEV and WebDEV to access ``` #### 2.2 Configure `startup.sh` Modify ``scripts/startup.sh`` to specify ``spring.profiles.active`` as ``github,ldap``. ```bash SERVICE_NAME=apollo-portal ## Adjust log dir if necessary LOG_DIR=/opt/logs ## Adjust server port if necessary SERVER_PORT=8070 export JAVA_OPTS="$JAVA_OPTS -Dspring.profiles.active=github,ldap" ``` ### 3. ApacheDS access method #### 3.1 Configure `application-ldap.yml` After unpacking `apollo-portal-x.x.x-github.zip`, create `application-ldap.yml` in the `config` directory with the following reference ([sample](https://github.com/apolloconfig/apollo/blob/master/apollo-portal/src/main/resources/application-ldap-apacheds-sample.yml)) , the relevant content needs to be adjusted according to the specific situation: ```yml spring: ldap: base: "dc=example,dc=com" username: "uid=admin,ou=system" # Configure administrator account for searching and matching users password: "password" search-filter: "(uid={0})" # user filter, use this filter to search for users when logging in urls: - "ldap://localhost:10389" ldap: mapping: # Configure the ldap attribute object-class: "inetOrgPerson" # ldap user objectClass configuration login-id: "uid" # ldap user unique id, used as the login id user-display-name: "displayName" # ldap user name, used as display name email: "mail" # ldap mailbox attribute ``` ##### 3.1.1 Filtering users based on Groups Starting with version 1.3.0, we support filtering users based on Group, which allows you to control that only users of a specific Group can log in and use apollo. ```yml spring: ldap: base: "dc=example,dc=com" username: "uid=admin,ou=system" # Configure admin account for searching and matching users password: "password" search-filter: "(uid={0})" # user filter, use this filter to search for users when logging in urls: - "ldap://localhost:10389" ldap: mapping: # Configure the ldap attribute object-class: "inetOrgPerson" # ldap user objectClass configuration login-id: "uid" # ldap user unique id, used as the login id rdnKey: "cn" # ldap rdn key user-display-name: "displayName" # ldap user name, used as display name email: "mail" # ldap mailbox attribute group: # Configure ldap group, only users of specific group can login apollo after enabled object-class: "groupOfNames" # Configure groupClassName group-base: "ou=group" # group search base group-search: "(&(cn=dev))" # group filter group-membership: "member" # group memberShip eg. member or memberUid ``` #### 3.2 Configuring `startup.sh` Modify ``scripts/startup.sh`` to specify ``spring.profiles.active`` as ``github,ldap``. ```bash SERVICE_NAME=apollo-portal ## Adjust log dir if necessary LOG_DIR=/opt/logs ## Adjust server port if necessary SERVER_PORT=8070 export JAVA_OPTS="$JAVA_OPTS -Dspring.profiles.active=github,ldap" ``` ## Implementation 3: Access to OIDC Since version 1.8.0 OpenID Connect login is supported, this implementation requires that the OpenID Connect login service has been deployed Before configuration, you need to prepare: * OpenID Connect provider configuration endpoint (RFC 8414-compliant issuer-uri), which needs to be **https**, e.g. https://host:port/auth/realms/apollo/.well-known/openid-configuration * Create a client in the OpenID Connect service, the signature algorithm of the idToken must be set to **RS256**, get the client-id and the corresponding client-secret ### 1. Configure `application-oidc.yml` After unpacking `apollo-portal-x.x.x-github.zip`, create `application-oidc.yml` in the `config` directory with the following contents ([sample](https://github.com/apolloconfig/apollo/blob/master/apollo-portal/src/main/resources/application-oidc-sample.yml)) , the relevant content needs to be adapted to the specific case: #### 1.1 Minimum configuration ```yml server: # Parse reverse proxy request headers forward-headers-strategy: framework spring: security: oauth2: client: provider: # provider-name is the name of the oidc provider, any character is fine, it is required for registration configuration : # must be https, issuer-uri of oidc # For example, if your issuer-uri is https://host:port/auth/realms/apollo/.well-known/openid-configuration, then here you only need to configure https://host:port/auth/realms/ apollo, spring boot will process it with the /.well known/openid-configuration suffix issuer-uri: https://host:port/auth/realms/apollo registration: # registration-name is the name of the oidc client, any character is fine, oidc login must be configured with a registration of type authorization_code : # oidc login must be configured with a registration of authorization_code type authorization-grant-type: authorization_code client-authentication-method: client_secret_basic # client-id is the client ID configured at the oidc provider, used to log in to the provider client-id: apollo-portal # The name of the provider, which should be the same as the provider name configured above provider: # openid is the required scope for oidc login, you can add other custom scopes here scope: - openid # client-secret is the client password configured at the oidc provider, used to log in to the provider # From the security point of view, it is recommended to use environment variables, which should be named as follows: dot(.), crossbar(-) The naming rule for environment variables is: replace the dot (.) and the crossbar (-) in the key of the configuration item with an underscore (_), then change all letters to uppercase, spring boot will automatically process environment variables that match this rule # For example, spring.security.oauth2.client.registration..client-secret -> SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION__CLIENT_SECRET ( can be replaced with the name of a custom oidc client) client-secret: d43c91c0-xxxx-xxxx-xxxx-xxxxxxxxxxxx ``` #### 1.2 Extended Configuration * If the OpenID Connect login service supports client_credentials mode, you can also configure a registration of type client_credentials, which can be used by apollo-portal as a client to request other resources protected by oidc * If the OpenID Connect login service supports jwt, you can also configure ${spring.security.oauth2.resourceserver.jwt.issuer-uri} to support accessing apollo-portal via jwt ```yml server: # Parse reverse proxy request headers forward-headers-strategy: framework spring: security: oauth2: client: provider: # provider-name is the name of the oidc provider, any character is fine, it is required for registration configuration : # must be the issuer-uri of https, oidc, and jwt if it is the same as the issuer-uri of jwt, or you can set it separately issuer-uri: ${spring.security.oauth2.resourceserver.jwt.issuer-uri} registration: # registration-name is the name of the oidc client, any character is fine, oidc login must be configured with a registration of type authorization_code : # oidc login must be configured with a registration of authorization_code type authorization-grant-type: authorization_code client-authentication-method: client_secret_basic # client-id is the client ID configured at the oidc provider, used to log in to the provider client-id: apollo-portal # The name of the provider, which should be the same as the provider name configured above provider: # openid is the required scope for oidc login, you can add other custom scopes here scope: - openid # client-secret is the client password configured at the oidc provider, used to log in to the provider # From the security point of view, it is recommended to use environment variables, which should be named as follows: dot(.), crossbar(-) The naming rule for environment variables is: replace the dot (.) and the crossbar (-) in the key of the configuration item with an underscore (_), then change all letters to uppercase, spring boot will automatically process environment variables that match this rule # For example, spring.security.oauth2.client.registration..client-secret -> SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION__CLIENT_SECRET ( can be replaced with the name of a custom oidc client) client-secret: d43c91c0-xxxx-xxxx-xxxx-xxxxxxxxxxxx # registration-name-client is the name of the oidc client, any character is allowed, registration of client_credentials type is optional, can not be configured registration-name-client: # registration of client_credentials type is optional, used by apollo-portal as a client to request other oidc-protected resources, may not be configured authorization-grant-type: client_credentials client-authentication-method: client_secret_basic # client-id is the client ID configured at the oidc provider, used to log in to the provider client-id: apollo-portal # The name of the provider, which must be the same as the provider name configured above provider: # openid is the required scope for oidc login, you can add other custom scopes here scope: - openid # client-secret is the client password configured at the oidc provider, used to log in to the provider, multiple registrations can be referenced if the password is the same client-secret: ${spring.security.oauth2.client.registration.registration-name.client-secret} resourceserver: jwt: # must be issuer-uri for https, jwt # For example, if your issuer-uri is https://host:port/auth/realms/apollo/.well-known/openid-configuration, then here you only need to configure https://host:port/auth/realms/ apollo, spring boot will automatically add the /.well known/openid-configuration suffix when processing issuer-uri: https://host:port/auth/realms/apollo ``` #### 1.3 Configure user display name you can also configure a custom user display name in the `application-oidc.yml` * see https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims for standard oidc claim name, and see your OpenID Connect service manager or ISP for nonstandard claim name. * the configuration property name for oidc (interactive) user display name is `spring.security.oidc.user-display-name-claim-name`, default `preferred_username`, and fallback to `name` if the claim value of `preferred_username` is blank * the configuration property name for oidc jwt user display name is `spring.security.oidc.jwt-user-display-name-claim-name`, has no default. ##### 1.3.1 Example of user display name configure * for example, using `name` as the claim of oidc (interactive) user display name. ```yml spring: security: oidc: user-display-name-claim-name: "name" ``` * for example, using `email` as the claim of oidc (interactive) user display name. ```yml spring: security: oidc: user-display-name-claim-name: "email" ``` * There is no claim name suitable for user display name in jwt standard claim name (https://tools.ietf.org/html/rfc7519#section-4), see your OpenID Connect service manager or ISP for a nonstandard claim name for display. * for example, using `user_display_name` as the claim of oidc jwt user display name. ```yml spring: security: oidc: jwt-user-display-name-claim-name: "user_display_name" ``` * it's ok to configure oidc (interactive) user display name and oidc jwt user display name at the same time. * for example, using `name` as the claim of oidc (interactive) user display name and using `user_display_name` as the claim of oidc jwt user display name. ```yml spring: security: oidc: user-display-name-claim-name: "name" jwt-user-display-name-claim-name: "user_display_name" ``` ### 2. Configure `startup.sh` Modify ``scripts/startup.sh`` to specify ``spring.profiles.active`` as ``github,oidc``. ```bash SERVICE_NAME=apollo-portal ## Adjust log dir if necessary LOG_DIR=/opt/logs ## Adjust server port if necessary SERVER_PORT=8070 export JAVA_OPTS="$JAVA_OPTS -Dspring.profiles.active=github,oidc" ``` ### 3. Configure apollo-portal to enable https #### 3.1 Adding a reverse proxy header Using nginx as an example, add or include (recommended) the following configuration directly to the http configuration section of nginx ```nginx server { listen 80 default_server; location / { # redirect all requests on port 80 to https return 301 https://$http_host$request_uri; } } server { # For lower versions of nginx that do not support http2, configure listen 443 ssl; listen 443 ssl http2; server_name xxx; # ssl certificate, nginx needs to use a full certificate chain certificate ssl_certificate /etc/nginx/ssl/xxx.crt; ssl_certificate_key /etc/nginx/ssl/xxx.key; # ... The rest of the ssl configuration location / { proxy_pass http://apollo-portal-dev:8070; proxy_set_header x-real-ip $remote_addr; proxy_set_header x-forwarded-for $proxy_add_x_forwarded_for; # !!! Here must be $http_host, if configured as $host, it will cause the port error when jumping proxy_set_header host $http_host; proxy_set_header x-forwarded-proto $scheme; proxy_http_version 1.1; } } ``` #### 3.2 Checking the application-oidc.yml configuration The configuration item ``server.forward-headers-strategy=framework`` must exist in ``application-oidc.yml``. ```yml server: # Parse reverse proxy request headers forward-headers-strategy: framework ``` #### 3.3 Adding a whitelist of redirects for the OpenID Connect login service For security reasons, the OpenID Connect login service generally has a whitelist of redirected addresses, so you need to add the apollo-portal https address to the whitelist in order to redirect properly. ## Implementation 4: access to the company's unified login authentication system This approach assumes that the company already has a unified login authentication system, such as SSO and LDAP. The following SPI must be implemented to access the system, including UserService and UserInfoHolder. The interfaces are described as follows. * UserService (Required): User service, used to provide user search-related functions to the Portal * UserInfoHolder (Required): Get the current login user information, SSO is generally the current login user information on ThreadLocal * LogoutHandler (Optional): used to implement the logout function * SsoHeartbeatHandler (Optional): If the Portal page is not refreshed for a long time, the login information will expire. Refresh the login information through this interface You can refer to `apollo-portal` module's package of [com.ctrip.framework.apollo.portal.spi](https://github.com/apolloconfig/apollo/tree/master/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi) , under this package for four implementations. 1. defaultimpl: default implementation, with only one global apollo account 2. ctrip: ctrip implementation, access to the SSO and the implementation of the user search, query interface 3. springsecurity: spring security implementation, you can add new users, modify the user password, etc. 4. ldap: ldap implementation contributed by [@pandalin](https://github.com/pandalin) and [codepiano](https://github.com/codepiano) After implementing the relevant interfaces, the AuthConfiguration can be accessed via [com.ctrip.framework.apollo.portal.configuration](https://github.com/apolloconfig/apollo/blob/master/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/configuration/AuthConfiguration.java) to replace the default implementation at runtime. The idea of accessing SSO is as follows. 1. SSO will provide a jar package, you need to configure a filter 2. filter will intercept all requests and check if you are already logged in 3. if there is no login, then it will jump to the SSO login page 4. After successful login in the SSO login page, it will jump back to the apollo page with the authentication information. 5. 5. enter the SSO filter again, verify the authentication information, save the user's information, and write the user's credentials to a cookie or distributed session to avoid having to log in again next time 6. enter Apollo's code, Apollo's code will call UserInfoHolder.getUser to get the current logged-in user Note that the above steps 1-5 are all SSO code, not Apollo code, Apollo code only requires you to implement step 6. >Note: The runtime use of different implementations is achieved by [Profiles](http://docs.spring.io/autorepo/docs/spring-boot/current/reference/html/boot-features-profiles.html). For example, if your own sso implementation is in the `custom` profile, you can specify -Dapollo_profile=github,custom in the packaging script. profile. Also note that in [AuthConfiguration](https://github.com/apolloconfig/apollo/blob/master/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/configuration/AuthConfiguration.java) to modify the default implementation of the condition >, from `@ConditionalOnMissingProfile({"ctrip", "auth", "ldap"})` to `@ConditionalOnMissingProfile({"ctrip", "auth", "ldap", "custom"})`. ================================================ FILE: docs/en/faq/common-issues-in-deployment-and-development-phase.md ================================================ ### 1. How does windows execute build.sh? Install [Git Bash](https://git-for-windows.github.io/), then run `./build.sh` Note the first `./` ### 2. When running locally Portal keeps reporting Env is down. By default config service starts on port 8080 and admin service starts on port 8090. Please check if these two ports are occupied by other applications. If there is also an exception message: org.springframework.web.client.HttpClientErrorException: 405 Method Not Allowed, it is usually due to the local startup of `ShadowSocks`, because `ShadowSocks` will occupy port 8090 by default. port 8090 by default. Version 1.1.0 added **System Information** page, you can view the current Meta Server and admin service information of each environment through `Administrator Tools` -> `System Information` to help troubleshoot the problem. ### 3. Admin server or config server is registered with intranet IP, resulting in portal or client cannot access admin server or config server Please refer to [network policy](en/deployment/distributed-deployment-guide?id=_14-network-policy). ### 4. How to add environment by Portal Console #### 4.1 1.6.0 and above Version 1.6.0 adds the ability to customize the environment, which allows you to add an environment without modifying the code 1. add environment for protaldb, refer to [3.1 Adjusting ApolloPortalDB configuration](en/deployment/distributed-deployment-guide?id=_31-adjusting-apolloportaldb-configuration) 2. add the meta server address corresponding to the new environment for apollo-portal, refer to: [2.2.1.1.2.4 Configuring meta service information for apollo-portal](en/deployment/distributed-deployment-guide?id=_221124-configuring-apollo-portal39s-meta-service-information). apollo-client also needs to be configured properly when used in a new environment, refer to: [1.2.2 Apollo Meta Server](en/client/java-sdk-user-guide?id=_122-apollo-meta-server). >Note 1: A set of Portal can manage multiple environments, but each environment needs to deploy a set of Config Service, Admin Service and ApolloConfigDB independently, please refer to: [2.1.2 Creating ApolloConfigDB](en/deployment/distributed-deployment-guide?id=_212-creating-apolloconfigdb), [3.2 Adjusting ApolloConfigDB configuration](en/deployment/distributed-deployment-guide?id=_32-adjusting-apolloconfigdb-configuration), [2.2.1.1.2 Configuring database connection information](en/deployment/distributed-deployment-guide?id=_22112-configuring-database-connection-information) > Note 2: If you are adding an environment to Apollo Configuration Center that has been running for a while, don't forget to refer to [2.1.2.4 Importing ApolloConfigDB project data from another environment](en/deployment/distributed-deployment-guide?id=_2124-importing-apolloconfigdb-project-data-from-another-environment) to initialize the new environment #### 4.2 1.5.1 and earlier ##### 4.2.1 Adding an Apollo pre-defined environment If the environment to be added is an Apollo pre-defined environment (DEV, FAT, UAT, PRO), two steps are required. 1. protaldb add environment, refer to [3.1 Adjusting ApolloPortalDB configuration](en/deployment/distributed-deployment-guide?id=_31-adjusting-apolloportaldb-configuration) 2. add the meta server address corresponding to the new environment for apollo-portal, refer to: [2.2.1.1.2.4 Configuring meta service information for apollo-portal](en/deployment/distributed-deployment-guide?id=_221124-configuring-apollo-portal39s-meta-service-information). apollo-client also needs to be configured properly when used in a new environment, refer to: [1.2.2 Apollo Meta Server](en/client/java-sdk-user-guide?id=_122-apollo-meta-server). >Note 1: A set of Portal can manage multiple environments, but each environment needs to deploy a set of Config Service, Admin Service and ApolloConfigDB independently, please refer to: [2.1.2 Creating ApolloConfigDB](en/deployment/distributed-deployment-guide?id=_212-creating-apolloconfigdb), [3.2 Adjusting ApolloConfigDB configuration](en/deployment/distributed-deployment-guide?id=_32-adjusting-apolloconfigdb-configuration), [2.2.1.1.2 Configuring database connection information](en/deployment/distributed-deployment-guide?id=_22112-configuring-database-connection-information) > Note 2: If you are adding an environment to Apollo Configuration Center that has been running for a while, don't forget to refer to [2.1.2.4 Importing ApolloConfigDB project data from another environment](en/deployment/distributed-deployment-guide?id=_2124-importing-apolloconfigdb-project-data-from-another-environment) to initialize the new environment > Note 3: if your custom environment name is "PROD", it will be forcibly converted to "PRO". Similarly, if the environment name is "FWS", it will be forcibly converted to "FAT". ##### 4.2.2 Adding a custom environment If the environment to be added is not one of Apollo's pre-defined environments, please refer to the following steps. 1. Assume the name of the environment to be added is beta 2. Modify [com.ctrip.framework.apollo.core.enums.Env](https://github.com/apolloconfig/apollo/blob/master/apollo-core/src/main/java/com/ctrip/framework/apollo/core/enums/Env.java) class by adding the ``BETA`` enumeration to it. ```java public enum Env{ LOCAL, DEV, BETA, FWS, FAT, UAT, LPT, PRO, TOOLS, UNKNOWN; ... } ``` 3. Modify the [com.ctrip.framework.apollo.core.enums.EnvUtils](https://github.com/apolloconfig/apollo/blob/master/apollo-core/src/main/java/com/ctrip/framework/apollo/core/enums/EnvUtils.java) class to include conversion logic for the `BETA` enumeration. ```java public final class EnvUtils { public static Env transformEnv(String envName) { if (StringUtils.isBlank(envName)) { return Env.UNKNOWN; } switch (envName.trim().toUpperCase()) { ... case "BETA": return Env.BETA; ... default: return Env.UNKNOWN; } } } ``` 4. Modify [apollo-env.properties](https://github.com/apolloconfig/apollo/blob/master/apollo-portal/src/main/resources/apollo-env.properties) to add the ``beta.meta`` placeholder. ```properties local.meta=http://localhost:8080 dev.meta=${dev_meta} fat.meta=${fat_meta} beta.meta=${beta_meta} uat.meta=${uat_meta} lpt.meta=${lpt_meta} pro.meta=${pro_meta} ``` 5. Modify the [com.ctrip.framework.apollo.core.internals.LegacyMetaServerProvider](https://github.com/apolloconfig/apollo/blob/master/apollo-core/src/main/java/com/ctrip/framework/apollo/core/internals/LegacyMetaServerProvider.java) class to add logic to read the meta server address of the ``BETA`` environment. ```java public class LegacyMetaServerProvider { ... domains.put(Env.BETA, getMetaServerAddress(prop, "beta_meta", "beta.meta")); ... } ``` 6. Add `BETA` environment for protaldb, refer to [3.1 Adjusting ApolloPortalDB configuration](en/deployment/distributed-deployment-guide?id=_31-adjusting-apolloportaldb-configuration) 7. Add the meta server address corresponding to the new environment for apollo-portal, refer to: [2.2.1.1.2.4 Configuring meta service information for apollo-portal](en/deployment/distributed-deployment-guide?id=_221124-configuring-apollo-portal39s-meta-service-information). apollo-client also needs to be configured properly when used in a new environment, refer to: [1.2.2 Apollo Meta Server](en/client/java-sdk-user-guide?id=_122-apollo-meta-server). >Note 1: A set of Portal can manage multiple environments, but each environment needs to deploy a set of Config Service, Admin Service and ApolloConfigDB independently, please refer to: [2.1.2 Creating ApolloConfigDB](en/deployment/distributed-deployment-guide?id=_212-creating-apolloconfigdb), [3.2 Adjusting ApolloConfigDB configuration](en/deployment/distributed-deployment-guide?id=_32-adjusting-apolloconfigdb-configuration), [2.2.1.1.2 Configuring database connection information](en/deployment/distributed-deployment-guide?id=_22112-configuring-database-connection-information) > Note 2: If you are adding an environment to Apollo Configuration Center that has been running for a while, don't forget to refer to [2.1.2.4 Importing ApolloConfigDB project data from another environment](en/deployment/distributed-deployment-guide?id=_2124-importing-apolloconfigdb-project-data-from-another-environment) to initialize the new environment ### 5. How do I delete applications, clusters, Namespace? From version 0.11.0 Apollo Administrator has added a page to delete applications, clusters and AppNamespace, we recommend to use this page to delete them. Page entry : ![delete-app-cluster-namespace-entry](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/delete-app-cluster-namespace-entry.png) Page details : ![delete-app-cluster-namespace-detail](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/delete-app-cluster-namespace-detail.png) ### 6. How to solve the problem of getting inaccurate IP due to multiple NICs on the client side? The logic of getting client NIC has been adjusted in version 1.4.0, so you need to differentiate according to client version #### 6.1 `apollo-client` for 1.3.0 and earlier versions If there are multiple NICs and they are all normal NICs, you need to add a mapping relationship in /etc/hosts to raise the weight. Format: `ip ${hostname}` Here ${hostname} is the result of your execution of hostname on the machine. For example, if the correct IP is: 192.168.1.50, the result of the hostname execution is: jim-ubuntu-pc Then the final record mapped in the hosts file is ``` 192.168.1.50 jim-ubuntu-pc ``` #### 6.2 apollo-client for version 1.4.0 and later If you have multiple NICs and they are all normal NICs, you can change the priority by adjusting their order in the system, with the first in the order having higher priority. ### 7. Dynamically adjusting the Logging level of Spring Boot via Apollo You can refer to [spring-cloud-logger](https://github.com/ctripcorp/apollo) in the [apollo-use-cases](https://github.com/ctripcorp/apollo-use-cases/tree/master/spring-cloud-logger) project and [spring-boot-logger](https://github.com/ctripcorp/apollo-use-cases/tree/master/spring-boot-logger) code examples. ### 8. Register Config Service and Admin Service to a separate Eureka Server Apollo comes with Eureka as an internal registry implementation by default, and there is generally no need to consider deploying a separate registry for Apollo. However, some companies already have a set of Eureka, if you want to register Apollo's Config Service and Admin Service to achieve unified management, you can follow the steps below. #### 1. Configure Config Service to not start the built-in Eureka Server ##### 1.1 1.5.0 and above Configure apollo-configService with `apollo.eureka.server.enabled=false`, either through bootstrap.yml or the -D parameter. ##### 1.2 versions before 1.5.0 Modify [com.ctrip.framework.apollo.configservice.ConfigServiceApplication](https://github.com/apolloconfig/apollo/blob/master/apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/ConfigServiceApplication.java) , change `@EnableEurekaServer` to `@ EnableEurekaClient` ```java @EnableEurekaClient @EnableAspectJAutoProxy @EnableAutoConfiguration // (exclude = EurekaClientConfigBean.class) @Configuration @EnableTransactionManagement @PropertySource(value = {"classpath:configservice.properties"}) @ComponentScan(basePackageClasses = {ApolloCommonConfig.class, ApolloBizConfig.class, ConfigServiceApplication.class, ApolloMetaServiceConfig.class}) public class ConfigServiceApplication { ... } ``` #### 2. Modify `eureka.service.url` in ApolloConfigDB.ServerConfig table to point to your own Eureka address For example, if your own Eureka service address is 1.1.1.1:8761 and 2.2.2.2:8761, then set `eureka.service.url` in `ApolloConfigDB.ServerConfig` table with: ``` http://1.1.1.1:8761/eureka/,http://2.2.2.2:8761/eureka/ ``` Note that changing the Eureka address only requires changing the `eureka.service.url` in the `ApolloConfigDB.ServerConfig` table, not the meta server address. > By default, the meta service and config service are deployed in the same JVM process, so the address of the meta service is the address of the config service, so you don't need to change the meta server address when you modify the Eureka address. ### 9. Using `ConditionalOnProperty` in Spring Boot does not read the configuration The `@ConditionalOnProperty` feature is supported since version 0.10.0, see [Spring Boot integration method](en/client/java-sdk-user-guide?id=_3213-spring-boot-integration-methods-recommended) ### 10. How to achieve the client in room A reads the config service of room A nearby and the client in room B reads the config service of room B nearby in multiple rooms? Please refer to [Issue 1294](https://github.com/apolloconfig/apollo/issues/1294), in this case, because the Chinese and American server rooms are far away from each other, so the config db needs to be deployed in two places. If it is a multi-city machine room, the config service of both machine rooms can be connected to one config db. ### 11. Does apollo have support for `HEAD` request pages? AliCloud slb configuration health check only supports `HEAD` requests Each service of apollo has `/health` page, which is used by apollo to do health check and supports various request methods such as GET, POST, HEAD, etc. ### 12. How can apollo be configured to view permissions? Starting from version 1.1.0, apollo-portal adds support for view permissions, which allows you to configure an environment to allow only project members to view the private Namespace. The project members here are 1. The project's administrator 2. Have the permission to modify or publish the private Namespace in that environment The configuration is very simple, after logging in with the super administrator account, go to `Administrator Tools - System Parameters` page to add or modify `configView.memberOnly.envs` configuration items. ![configView.memberOnly.envs](https://user-images.githubusercontent.com/837658/46456519-c155e100-c7e1-11e8-969b-8f332379fa29.png) ### 13. How to put apollo in a separate tomcat to run? Some companies' O&M policies may require that they must use a standalone tomcat to run their applications and do not allow apollo to run in the default startup.sh way. The following is a brief example of how to make the apollo server run in a standalone tomcat. 1. Get the apollo code (the release version is recommended for production deployments) 2. Modify the apollo-configservice pom.xml and add `war`. 3. Configure build.sh according to the distributed deployment documentation, and then package it 4. Put the apollo-configservice war package under tomcat * cp apollo-configservice/target/apollo-configservice-xxx.war ${tomcat-dir}/webapps/ROOT.war Run tomcat's startup.sh 5. Run tomcat's startup.sh In addition, apollo has some tuning parameters that are recommended to be configured in tomcat's server.xml, which can be found in [application.properties](https://github.com/apolloconfig/apollo/blob/master/apollo-common/src/main/resources/application.properties#L12) ### 14. How to replace registry Eureka with zookeeper? Many company microservice projects are already using zookeeper, if you want to replace Eureka with zookeeper for the purpose of easy service management, you can refer to the transformation steps contributed by [@hanyidreamer](https://github.com/hanyidreamer): [registry Eureka replacement for zookeeper](https://blog.csdn.net/u014732209/article/details/89555535) ### 15. How to achieve different configurations and not affect each other when multiple people are developing locally at the same time? Refer to [#1560](https://github.com/apolloconfig/apollo/issues/1560) ### 16. How to set the relative path after Portal is mounted to nginx/slb? In general it is recommended to use the root directory directly to mount the portal, however if there are cases where you want to share nginx/slb with other applications and need to add a relative path (e.g. /apollo), then you can configure it as follows. #### 16.1 Portal for version 1.7.0 and above First add the `-D` parameter `server.servlet.context-path=/apollo` or the system environment variable `SERVER_SERVLET_CONTEXT_PATH=/apollo` for apollo-portal. Then just configure forwarding on nginx/slb, using nginx as an example. ``` location /apollo/ { proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Forwarded-Server $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_pass http://127.0.0.1:8070/apollo/; } ``` #### 16.2 Portal for version 1.6.0 and above First add the `prefix.path=/apollo` configuration parameter to the portal, the configuration is very simple, after logging in with the super administrator account, go to the `Administrator Tools - System Parameters` page and add or modify the `prefix.path` configuration item. Then you can configure forwarding on nginx/slb, using nginx as an example. ``` location /apollo/ { proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Forwarded-Server $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_pass http://127.0.0.1:8070/; } ``` ### 17. How to configure https after Portal is mounted to nginx/slb? 1. configure https access configuration on nginx/slb, taking nginx as an example: ``` server { listen 80 default_server; location / { # redirect all requests on port 80 to https return 301 https://$http_host$request_uri; } } server { # If the nginx version is lower and does not support http2, configure listen 443 ssl; listen 443 ssl http2; server_name your-domain-name; # ssl certificate, nginx needs to use a certificate with a complete certificate chain ssl_certificate /etc/nginx/ssl/xxx.crt; ssl_certificate_key /etc/nginx/ssl/xxx.key; location / { proxy_pass http://apollo-portal-address:8070; proxy_set_header x-real-ip $remote_addr; proxy_set_header x-forwarded-for $proxy_add_x_forwarded_for; #! ! ! This must be $http_host, if it is configured as $host, the port will be wrong when redirecting proxy_set_header host $http_host; proxy_set_header x-forwarded-proto $scheme; proxy_http_version 1.1; } } ``` 2. Configure apollo-portal to parse the header information from the reverse proxy Modify application-github.properties under the config directory in the apollo-portal installation package and add the following configuration: ```properties server.forward-headers-strategy=framework ``` It can also be configured through environment variables: ``` SERVER_FORWARD_HEADERS_STRATEGY=framework ``` ================================================ FILE: docs/en/faq/faq.md ================================================ ## 1. What is Apollo? Apollo (Apollo) is a reliable distributed configuration management center, born in Ctrip framework R&D department, which can centralize and manage the configuration of different environments and clusters of applications, and can push to the application side in real time after configuration modification, and has standardized permissions, process governance and other features, suitable for microservice configuration management scenarios. For more information, please refer to [Apollo Configuration Center Introduction](en/design/apollo-introduction) ## 2. What is Cluster? It is a grouping of different instances of an application, for example, typically you can divide the application instances in server room A into one cluster and the application instances in server room B into another cluster according to the data center. ## 3. What is Namespace? A grouping of different configurations under one application. Please refer to [Apollo core concept "Namespace"](en/design/apollo-core-concept-namespace) ## 4. I want to access Apollo, how do I do it? Please refer to [Apollo user guide](en/portal/apollo-user-guide) ## 5. My application needs different configurations for different server rooms, does Apollo support it? Apollo is supported. Please refer to `III. Cluster independent configuration instructions` in [Apollo User Guide](en/portal/apollo-user-guide?id=iii-cluster-independent-configuration-instructions) ## 6. I have multiple applications that need to use the same configuration, can Apollo support it? Apollo is supported. Please refer to `IV. Using the same configuration for multiple AppId` in [Apollo User Guide](en/portal/apollo-user-guide?id=iv-using-the-same-configuration-for-multiple-appid). ## 7. Does Apollo support view permission control or configuration encryption? Starting from version 1.1.0, apollo-portal adds support for view permissions, which can support configuring an environment to allow only project members to view the configuration of a private Namespace. The project members here are : 1. The project's administrator 2. Have the permission to modify or publish the private Namespace in that environment The configuration is very simple, after logging in with the super administrator account, go to `Administrator Tools - System Parameters` page to add or modify `configView.memberOnly.envs` configuration items. ![configView.memberOnly.envs](https://user-images.githubusercontent.com/837658/46456519-c155e100-c7e1-11e8-969b-8f332379fa29.png) The configuration encryption can be found in the [spring-boot-encrypt demo project](https://github.com/ctripcorp/apollo-use-cases/tree/master/spring-boot-encrypt) ## 8. If there are multiple config servers, how to configure the meta server address when packaging? There are multiple meta servers can be achieved through nginx reverse proxy, by proxy multiple meta servers with one domain name ha. ## 9. What are the advantages of Apollo over Spring Cloud Config? The beauty of Spring Cloud Config is that its configuration is stored in Git, which naturally isolates configuration changes, permissions, versioning, etc. This design makes Spring Cloud Config simple to use overall, but it also brings some inconveniences. Here's a quick summary. | Function Point | Apollo | Spring Cloud Config | Notes | | ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ | ----- | | Configuration interface | One interface to manage different environments, different clusters configuration | None, need to operate through git | | | Spring Cloud Config requires a Git webhook and an additional message queue to support realtime effect. | | | | | Versioning | Release history and rollback buttons directly on the interface | None, requires git action | | | Grayscale releases | Support | Not support | | | Authorization, audit, auditing | Support directly on the interface, and support the separation of modify and release permissions | Need to set up through the git repository, and do not support the separation of modify and release permissions | | | Instance configuration monitoring | You can easily see which clients are currently using which configurations | Not supported | | | Configuration fetching performance | Fast, with database access and cache support | Slow, requiring clone repository from git, then read from filesystem | | | Net applications, provides API support for other language applications, and also supports Spring annotation to get configuration | Support for Spring applications, provides annotation to get configuration | Apollo is a little more widely available | | ## 10. What are the advantages of Apollo compared to Disconf? Since we are not experienced users of `Disconf`, we can't give a subjective evaluation. However, an enthusiastic user in the Apollo support group [@Krast](https://github.com/krast) made an [Open Source Configuration Center Comparison Matrix](https://github.com/apolloconfig/apollo/files/983064/default.pdf), which can be consulted. ## 11. I get "OpenAppDTO" or other `OpenXxxDTO` classes not found after pulling the latest code, what should I do? The `apollo-portal` module generates the `OpenXxxDTO` classes from OpenAPI YAML definitions at build time. Run a Maven compile phase to trigger the code generation: ```bash mvn clean compile -pl apollo-portal -am ``` You can also execute the command inside the `apollo-portal` module: ```bash mvn clean compile ``` After it finishes, the OpenAPI DTOs will be regenerated under `com.ctrip.framework.apollo.openapi.model`. ================================================ FILE: docs/en/misc/apollo-benchmark.md ================================================ Many people are concerned about the performance and reliability of Apollo. The following data is collected from a single machine in Ctrip's internal production environment. The monitoring tool is [Cat](https://github.com/dianping/cat). ### I. Test machine configuration #### 1.1 Machine configuration 4C12G #### 1.2 JVM parameters ``` -Xms6144m -Xmx6144m -Xss256k -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=384m -XX:NewSize=4096m -XX:MaxNewSize=4096m -XX:SurvivorRatio=8 -XX:+UseParNewGC -XX:ParallelGCThreads=4 -XX:MaxTenuringThreshold=9 -XX:+UseConcMarkSweepGC -XX:+DisableExplicitGC -XX:+ UseCMSInitiatingOccupancyOnly -XX:+ScavengeBeforeFullGC -XX:+UseCMSCompactAtFullCollection -XX:+CMSParallelRemarkEnabled -XX: CMSFullGCsBeforeCompaction=9 -XX:CMSInitiatingOccupancyFraction=60 -XX:+CMSClassUnloadingEnabled -XX:SoftRefLRUPolicyMSPerMB=0 -XX:+ CMSPermGenSweepingEnabled -XX:CMSInitiatingPermOccupancyFraction=70 -XX:+ExplicitGCInvokesConcurrent -XX:+PrintGCDetails -XX:+ PrintGCDateStamps -XX:+PrintGCApplicationConcurrentTime -XX:+PrintHeapAtGC -XX:+HeapDumpOnOutOfMemoryError -XX:- OmitStackTraceInFastThrow -Duser.timezone=Asia/Shanghai -Dclient.encoding.override=UTF-8 -Dfile.encoding=UTF-8 -Djava.security.egd= file:/dev/./urandom ``` #### 1.3 JVM Versions 1.8.0_60 #### 1.4 Apollo version 0.9.0 #### 1.5 Number of client connections per machine (number of clients) 5600 #### 1.6 Total number of client connections (number of clients) in the cluster 10W+ ### II. Performance Metrics #### 2.1 Get configured Http interface response time `QPS`: 160 `Average response time`: 0.1ms `95 line response time`: 0.3ms `999 line response time`: 2.5ms >Note: config service has config cache enabled, for more information you can refer to [Cache Configuration in Distributed Deployment Guide](en/deployment/distributed-deployment-guide?id=_323-config-servicecacheenabled-whether-to-enable-configuration-caching) #### 2.2 Config Server GC Status `YGC`: average 2Min once, one time 300ms `OGC`: average 1H once, one time consuming 380ms #### 2.3 CPU metrics LoadAverage: 0.5 System CPU utilization: 6% Process CPU utilization: 8% ================================================ FILE: docs/en/portal/apollo-open-api-platform.md ================================================ ### I. What is the open-platform? Apollo provides a set of Http REST interfaces to enable third-party applications to manage their own configurations. Although Apollo system itself provides a Portal to manage the configuration, but in some scenarios, the application needs to manage the configuration through the program. ### II. Third-party application access to Apollo open-platform #### 2.1 Registering third-party applications The person in charge of the third-party application needs to provide some basic information of the third-party application to Apollo administrator. The basic information is as follows. * AppId, app name, department of the third-party application * The person in charge of the third-party app Apollo administrator creates the third-party application at `http://{portal_address}/open/add-consumer.html`. It is better to check whether this AppId has been created before creating it. After successful creation, a token will be generated as follows. ![Open Platform Management](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/apollo-open-manage.png) #### 2.2 View third-party apps Apollo administrator in `http://{portal_address}/open/manage.html` The HTML page allows you to view the list of third-party applications. It also provides management operations such as [View token and empower] and [delete], as shown in the following figure: ![第三方应用列表](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/apollo-open-manage-list.png) The modal box page of [View token and empower] is shown in the following figure: ![查看Token并赋权](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/apollo-open-manage-token.png) #### 2.3 Authorization for registered third-party apps Third-party applications should not be able to manipulate any Namespace configuration, so you need to bind the token to a Namespace that can be manipulated. Apollo administrators assign rights to the token in the `http://{portal_address}/open/add-consumer.html` page. After the assignment, the third-party application can manage the configuration of the authorized Namespace through the Http REST interface provided by Apollo. #### 2.4 Third-party application calls Apollo Open API ##### 2.4.1 Calling the Http REST Interface Third-party applications in any language can call Apollo's Open API. When calling the interface, you need to set attention to the following two points. * Add an Authorization field to the Http Header, with the field value of the applied token * Http Header Content-Type field needs to be set to application/json;charset=UTF-8 ##### 2.4.2 Java application calls Apollo Open API via apollo-openapi Starting from version 1.1.0, Apollo provides the [apollo-openapi](https://github.com/apolloconfig/apollo/tree/master/apollo-openapi) client, so third-party applications in the Java language can more easily invoke the Apollo Open API. First introduce the `apollo-openapi` dependency. ```xml com.ctrip.framework.apollo apollo-openapi 1.7.0 ``` Construct ``ApolloOpenApiClient`` in the program. ``` java String portalUrl = "http://localhost:8070"; // portal url String token = "e16e5cd903fd0c97a116c873b448544b9d086de9"; // token of the application ApolloOpenApiClient client = ApolloOpenApiClient.newBuilder() .withPortalUrl(portalUrl) .withToken(token) .build(); ``` You can then operate the Apollo Open API directly through the `ApolloOpenApiClient` interface, see the Rest interface documentation below for a description of the interface. ##### 2.4.3 .Net core application calls Apollo Open API Net core also provides a client for the open api, see https://github.com/ctripcorp/apollo.net/pull/77 for details ##### 2.4.4 Shell Scripts calls Apollo Open API Encapsulated bash functions, the underlying use of curl to send HTTP requests * Bash function: [openapi.sh](https://github.com/apolloconfig/apollo/blob/master/scripts/openapi/bash/openapi.sh) * Usage example: [openapi-usage-example.sh](https://github.com/apolloconfig/apollo/blob/master/scripts/openapi/bash/openapi-usage-example.sh) * All the shell scripts related to openapi are in the folder https://github.com/apolloconfig/apollo/tree/master/scripts/openapi/bash ### III. Interface documentation #### 3.1 URL path parameter description | parameter name | parameter description | | -------------- | ------------------------------------------------------------ | | env | Managed configuration environment | | appId | The managed configuration AppId | | clusterName | The name of the managed configuration cluster, normally passed in as default. If it is a special cluster, just pass the name of the corresponding cluster. | | namespaceName | the name of the managed namespace, if it is not in properties format, you need to add the suffix name, such as `sample.yml` | #### 3.2 API interface list ##### 3.2.1 Get App's environment, cluster information * **URL** : `http://{portal_address}/openapi/v1/apps/{appId}/envclusters` * **Method** : GET * **Request Params** : None * **Response Sample**: ``` json [ { "env": "FAT", "clusters":[ //cluster list "default", "FAT381" ] }, { "env": "UAT", "clusters":[ "default" ] }, { "env": "PRO", "clusters":[ "default", "SHAOY", "SHAJQ" ] } ] ``` ##### 3.2.2 Get App information * **URL** : `http://{portal_address}/openapi/v1/apps` * **Method** : GET * **Request Params** : | Parameter Name | Required | Type | Description | | -------------- | -------- | ------ | ------------------------------------------------------------ | | appIds | false | String | List of appId, separated by commas, or return all app information if empty | * **Response Sample**: ``` json [ { "name": "first_app", "appId": "100003171", "orgId": "development", "orgName": "development", "ownerName": "apollo", "ownerEmail": "test@test.com", "dataChangeCreatedBy": "apollo", "dataChangeLastModifiedBy": "apollo", "dataChangeCreatedTime": "2019-05-08T09:13:31.000+0800", "dataChangeLastModifiedTime": "2019-05-08T09:13:31.000+0800" }, { "name": "apollo-demo", "appId": "100004458", "orgId": "development", "orgName": "product-development", "ownerName": "apollo", "ownerEmail": "apollo@cmcm.com", "dataChangeCreatedBy": "apollo", "dataChangeLastModifiedBy": "apollo", "dataChangeCreatedTime": "2018-12-23T12:35:16.000+0800", "dataChangeLastModifiedTime": "2019-04-08T13:58:36.000+0800" } ] ``` ##### 3.2.3 Getting the cluster interface * **URL** : `http://{portal_address}/openapi/v1/envs/{env}/apps/{appId}/clusters/{clusterName}` * **Method** : GET * **Request Params** : None * **Response Sample**: ``` json { "name": "default", "appId": "100004458", "dataChangeCreatedBy": "apollo", "dataChangeLastModifiedBy": "apollo", "dataChangeCreatedTime": "2018-12-23T12:35:16.000+0800", "dataChangeLastModifiedTime": "2018-12-23T12:35:16.000+0800" } ``` ##### 3.2.4 Creating a Cluster Interface Clusters can be created through this interface, and calling this interface requires granting the third-party APP administrative privileges to the target APP. * **URL** : `http://{portal_address}/openapi/v1/envs/{env}/apps/{appId}/clusters` * **Method** : POST * **Request Params** : None * **Request Content** (Request Body, JSON format) | Parameter Name | Required | Type | Description | | ------------------- | -------- | ------ | ------------------------------------------------------------ | | name | true | String | The name of the Cluster | | appId | true | String | The AppId to which the Cluster belongs | | dataChangeCreatedBy | true | String | The creator of the namespace, in the format of the domain account, which is the User ID of the sso system | * **Response Sample**: ``` json { "name": "someClusterName", "appId": "100004458", "dataChangeCreatedBy": "apollo", "dataChangeLastModifiedBy": "apollo", "dataChangeCreatedTime": "2018-12-23T12:35:16.000+0800", "dataChangeLastModifiedTime": "2018-12-23T12:35:16.000+0800" } ``` ##### 3.2.5 Interface to get information about all Namespaces under a cluster * **URL** : `http://{portal_address}/openapi/v1/envs/{env}/apps/{appId}/clusters/{clusterName}/namespaces` * **Method**: GET * **Request Params**: None * **Response Sample**: ``` json [ { "appId": "100003171", "clusterName": "default", "namespaceName": "application", "comment": "default app namespace", "format": "properties", //Namespace format may take values of: properties, xml, json, yml, yaml "isPublic": false, //Whether the Namespace is public or not "items": [ // collection of all configurations under Namespace { "key": "batch", "value": "100", "dataChangeCreatedBy": "song_s", "dataChangeLastModifiedBy": "song_s", "dataChangeCreatedTime": "2016-07-21T16:03:43.000+0800", "dataChangeLastModifiedTime": "2016-07-21T16:03:43.000+0800" } ], "dataChangeCreatedBy": "song_s", "dataChangeLastModifiedBy": "song_s", "dataChangeCreatedTime": "2016-07-20T14:05:58.000+0800", "dataChangeLastModifiedTime": "2016-07-20T14:05:58.000+0800" }, { "appId": "100003171", "clusterName": "default", "namespaceName": "FX.apollo", "comment": "apollo public namespace", "format": "properties", "isPublic": true, "items": [ { "key": "request.timeout", "value": "3000", "comment": "", "dataChangeCreatedBy": "song_s", "dataChangeLastModifiedBy": "song_s", "dataChangeCreatedTime": "2016-07-20T14:08:30.000+0800", "dataChangeLastModifiedTime": "2016-08-01T13:56:25.000+0800" }, { "id": 1116, "key": "batch", "value": "3000", "comment": "", "dataChangeCreatedBy": "song_s", "dataChangeLastModifiedBy": "song_s", "dataChangeCreatedTime": "2016-07-28T15:13:42.000+0800", "dataChangeLastModifiedTime": "2016-08-01T13:51:00.000+0800" } ], "dataChangeCreatedBy": "song_s", "dataChangeLastModifiedBy": "song_s", "dataChangeCreatedTime": "2016-07-20T14:08:13.000+0800", "dataChangeLastModifiedTime": "2016-07-20T14:08:13.000+0800" } ] ``` ##### 3.2.6 Interface to get information about a Namespace * **URL** : http://{portal_address}/openapi/v1/envs/{env}/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName} * **Method** : GET * **Request Params** : None * **Response Sample** : ``` json { "appId": "100003171", "clusterName": "default", "namespaceName": "application", "comment": "default app namespace", "format": "properties", //Namespace format may take values of: properties, xml, json, yml, yaml "isPublic": false, //Whether the Namespace is public or not "items": [ // collection of all configurations under Namespace { "key": "batch", "value": "100", "dataChangeCreatedBy": "song_s", "dataChangeLastModifiedBy": "song_s", "dataChangeCreatedTime": "2016-07-21T16:03:43.000+0800", "dataChangeLastModifiedTime": "2016-07-21T16:03:43.000+0800" } ], "dataChangeCreatedBy": "song_s", "dataChangeLastModifiedBy": "song_s", "dataChangeCreatedTime": "2016-07-20T14:05:58.000+0800", "dataChangeLastModifiedTime": "2016-07-20T14:05:58.000+0800" } ``` ##### 3.2.7 Creating Namespace Namespace can be created through this interface, and calling this interface requires granting the third-party app administrative privileges to the target app. * **URL** : `http://{portal_address}/openapi/v1/apps/{appId}/appnamespaces` * **Method** : POST * **Request Params** : None * **Request Content** (Request Body, JSON format) | Parameter Name | Required | Type | Description | | ------------------- | -------- | ------- | ------------------------------------------------------------ | | name | true | String | The name of the Namespace | | appId | true | String | The AppId to which the Namespace belongs | | format | true | String | The format of the Namespace, ** which can only be of the following types: properties, xml, json, yml, yaml** | | isPublic | true | boolean | Whether the file is public | | comment | false | String | Namespace description | | dataChangeCreatedBy | true | String | The creator of the namespace, in the format of the domain account, which is the User ID of the sso system | * **Response Sample**: ``` json { "name": "FX.public-0420-11", "appId": "100003173", "format": "properties", "isPublic": true, "comment": "test", "dataChangeCreatedBy": "zhanglea", "dataChangeLastModifiedBy": "zhanglea", "dataChangeCreatedTime": "2017-04-20T18:25:49.033+0800", "dataChangeLastModifiedTime": "2017-04-20T18:25:49.033+0800" } ``` * **Explanation of returned values** :. > If properties file, name = ${appId belongs to department}. ${name passed in}, e.g. call the interface passed in name=xy-z, format=properties, the application's department is framework (FX), then name=FX.xy-z > if not the properties file name = ${appId belongs to the department}. ${name value passed in}. ${format}, for example, call the interface passed name=xy-z, format=json, the application's department is the framework (FX), then name=FX.xy-z.json ##### 3.2.8 Get the current editor of a Namespace interface Apollo has a restriction rule in the production environment (PRO): only one person can edit the configuration for each release, and the person who edited the release cannot be the editor of the release. In other words, if a user A modifies the configuration of a namespace, it can only be modified by A before the namespace is released, and no other user can modify it. At the same time, the user A cannot publish the configuration he modified, but must find another person who has the permission to do so. This interface is the interface used to get whether the current namespace is locked or not. In non-production environments (FAT, UAT), this interface always returns that no one is locked. * **URL** : http://{portal_address}/openapi/v1/envs/{env}/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/lock * **Method** : GET * **Request Params** : None * **Response Sample (unlocked)** : ``` json { "namespaceName": "application", "isLocked": false } ``` * **Response Sample (isLocked)** : ``` json { "namespaceName": "application", "isLocked": true, "lockedBy": "song_s" //lockedowner } ``` ##### 3.2.9 Reading the configuration interface * **URL** : `http://{portal_address}/openapi/v1/envs/{env}/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/items/{key}` * **Method** : GET * **Request Params** : None * **Response Sample** : ``` json { "key": "timeout", "value": "3000", "comment": "timeout", "dataChangeCreatedBy": "zhanglea", "dataChangeLastModifiedBy": "zhanglea", "dataChangeCreatedTime": "2016-08-11T12:06:41.818+0800", "dataChangeLastModifiedTime": "2016-08-11T12:06:41.818+0800" } ``` ##### 3.2.10 New configuration interface * **URL** : `http://{portal_address}/openapi/v1/envs/{env}/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/items` * **Method** : POST * **Request Params** : None * **Request Content** (Request Body, JSON format) | Parameter Name | Required | Type | Description | | ------------------- | -------- | ------ | ------------------------------------------------------------ | | key | true | String | The key of the configuration, the length cannot exceed 128 characters. Non-properties format, key is fixed to `content` | | value | true | String | The configured value, which cannot exceed 20000 characters in length, is in non-properties format. | | comment | false | String | The configured comment, the length can not exceed 256 characters | | dataChangeCreatedBy | true | String | The creator of the item, in the format of the domain account, which is the User ID of the sso system | * **Request body sample** : ``` json { "key": "timeout", "value": "3000", "comment": "timeout", "dataChangeCreatedBy": "zhanglea" } ``` * **Response Sample**: ``` json { "key": "timeout", "value": "3000", "comment": "timeout", "dataChangeCreatedBy": "zhanglea", "dataChangeLastModifiedBy": "zhanglea", "dataChangeCreatedTime": "2016-08-11T12:06:41.818+0800", "dataChangeLastModifiedTime": "2016-08-11T12:06:41.818+0800" } ``` ##### 3.2.11 Modifying the configuration interface * **URL** : `http://{portal_address}/openapi/v1/envs/{env}/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/items/{key}` * **Method** : PUT * **Request Params** : | Parameter Name | Required | Type | Description | | ----------------- | -------- | ------- | ------------------------------------------------------------ | | createIfNotExists | false | Boolean | Whether to create automatically when the configuration does not exist | * **Request Body, JSON format**: | parameter name | required | type | description | | ------------------------ | -------- | ------ | ------------------------------------------------------------ | | key | true | String | The key of the configuration, must be the same as the key in the url. For non-properties format, the key is fixed to `content` | | value | true | String | The configured value can not exceed 20000 characters, non-properties format, the value is the entire content of the file | | comment | false | String | The configured comment, the length can not exceed 256 characters | | dataChangeLastModifiedBy | true | String | The modifier of the item, in the format of the domain account, which is the User ID of the sso system | | dataChangeCreatedBy | false | String | Required when createIfNotExists is true. item's creator, in the format of the domain account, i.e. sso system's User ID | * **Request Body Sample** : ```json { "key": "timeout", "value": "3000", "comment": "timeout", "dataChangeLastModifiedBy": "zhanglea" } ``` * **Response Value** : none ##### 3.2.12 Deleting configuration interfaces * **URL** : `http://{portal_address}/openapi/v1/envs/{env}/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/items/{key}? operator={operator}` * **Method** : DELETE * **Request Params** : | Parameter Name | Required | Type | Description | | -------------- | -------- | ------ | ------------------------------------------------------------ | | key | true | String | The configured key. non-properties format, the key is fixed to `content` | | operator | true | String | Delete the operator of the configuration, domain account | * **Response Value** : None ##### 3.2.13 Publish Configuration Interface * **URL** : `http://{portal_address}/openapi/v1/envs/{env}/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/releases` * **Method** : POST * **Request Params** : None * **Request Body** : None | Parameter Name | Required | Type | Description | | -------------- | -------- | ------ | ------------------------------------------------------------ | | releaseTitle | true | String | The title of the release, which cannot exceed 64 characters in length | | releaseComment | false | String | The comment of the release, which cannot exceed 256 characters in length | | releasedBy | true | String | The publisher, domain account, and note: If `namespace.lock.switch` in `ApolloConfigDB.ServerConfig` is set to true (default is false), then the environment does not allow the publisher and editor to be the same person. . So if the editor is zhanglea, the publisher can no longer be zhanglea. | * **Request Body Example** : ```json { "releaseTitle": "2016-08-11", "releaseComment": "Modify timeout value", "releasedBy": "zhanglea" } ``` * **Response Sample** : ``` json { "appId": "test-0620-01", "clusterName": "test", "namespaceName": "application", "name": "2016-08-11", "configurations": { "timeout": "3000", }, "comment": "Modify timeout value", "dataChangeCreatedBy": "zhanglea", "dataChangeLastModifiedBy": "zhanglea", "dataChangeCreatedTime": "2016-08-11T14:03:46.232+0800", "dataChangeLastModifiedTime": "2016-08-11T14:03:46.235+0800" } ``` ##### 3.2.14 Interface to get the published configuration currently in effect for a Namespace * **URL** : `http://{portal_address}/openapi/v1/envs/{env}/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/releases/ latest` * **Method** : GET * **Request Params** : None * **Response Sample** : ``` json { "appId": "test-0620-01", "clusterName": "test", "namespaceName": "application", "name": "2016-08-11", "configurations": { "timeout": "3000", }, "comment": "Modify timeout value", "dataChangeCreatedBy": "zhanglea", "dataChangeLastModifiedBy": "zhanglea", "dataChangeCreatedTime": "2016-08-11T14:03:46.232+0800", "dataChangeLastModifiedTime": "2016-08-11T14:03:46.235+0800" } ``` ##### 3.2.15 Rollback of published configuration interface * **URL** : `http://{portal_address}/openapi/v1/envs/{env}/releases/{releaseId}/rollback` * **Method** : PUT * **Request Params** : | Parameter Name | Required | Type | Description | | -------------- | -------- | ------ | ----------------------------------------------- | | operator | true | String | Deletes the configured operator, domain account | * **Response Value** : None ##### 3.2.16 Get configuration items with pagination * **URL** : `http://{portal_address}/openapi/v1/envs/{env}/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/items` * **Method** : GET * **Version** : >= 2.1.0 * **Request Params** : | Parameter Name | Required | Type | Description | |----------------|----------|------|--------------------------------------------| | page | false | int | page number, starting from 0, default is 0 | | size | false | int | records in each page, default is 50 | * **Response Sample** : ``` json { "content": [ { "key": "timeout", "value": "3000", "comment": "timeout", "dataChangeCreatedBy": "mghio", "dataChangeLastModifiedBy": "mghio", "dataChangeCreatedTime": "2022-07-17T21:37:41.818+0800", "dataChangeLastModifiedTime": "2022-07-17T21:37:41.818+0800" }, { "key": "page.size", "value": "200", "comment": "page size", "dataChangeCreatedBy": "mghio", "dataChangeLastModifiedBy": "mghio", "dataChangeCreatedTime": "2022-07-17T21:37:41.818+0800", "dataChangeLastModifiedTime": "2022-07-17T21:37:41.818+0800" } ], "page": 0, "size": 50, "total": 2 } ``` ##### 3.2.17 Create App And grant administrative privileges App can be created through this interface, > note: When creating and allowing third-party application, **check the box to Allow app creation, otherwise an exception will be throw, HTTP status code 401** * **URL** : http://{portal_address}/openapi/v1/apps/ * **Method** : POST * **Request Params** :无 * **Request Body, JSON format**: | Parameter Name | Required | Type | Description | | ------------------- | -------- | -------- | -------------------------------------------------------- | | assignAppRoleToSelf | true | Boolean | true:granting app's administrative privileges to self | | admins | false | String[] | granting app's administrative privileges to those users | | app | true | Object | APP's information,see Request Sample followed for field | * **Request Sample** : ```json { "assignAppRoleToSelf": true, "admins": [ "user1", "user2" ], "app": { "name": "appName1234", "appId": "xxx-web", "orgId": "development", "orgName": "产品研发部", "ownerName": "user3", "ownerEmail": "user3@test.com" } } ``` * **Response Sample** : None ### IV. Error code description Under normal circumstances, the Http status code returned by the interface is 200, the following lists the non-200 error code descriptions that Apollo will return. #### 4.1 400 - Bad Request The client needs to check whether the corresponding parameters are correct according to the prompt. #### 4.2 401 - Unauthorized The token passed to the interface is illegal or expired, the client needs to check if the token was passed correctly. #### 4.3 403 - Forbidden The interface is trying to access a resource that is not authorized, for example, it is only authorized to manage Namespace under application A, but it is trying to manage the configuration under application B. #### 4.4 404 - Not Found The resource to be accessed by the interface does not exist, typically the URL or the parameters of the URL are incorrect. #### 4.5 405 - Method Not Allowed The Method of the interface access is incorrect, for example, the interface that should use POST uses GET access, etc. The client needs to check if the interface access method is correct. #### 4.6 500 - Internal Server Error Other types of errors will return 500 by default. For this type of error, if the application cannot find the cause according to the prompt, you can ask Apollo's R&D team to troubleshoot the problem together. ================================================ FILE: docs/en/portal/apollo-user-guide.md ================================================ #   # Glossary of Terms * Common application * Common applications refer to programs that run independently, such as * Web applications * Programs with main functions * Public components * Public components refer to distributed libraries, client programs that do not run on their own, such as * Java jar packages * .Net dll file # I. General Application Access Guide ## 1.1 Creating a project To use Apollo, you need to create a project as the first step. 1. Open apollo-portal homepage 2. Click "Create a project". ![create-app-entry](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/create-app-entry.png) 3. Enter the project information * Department: Select the department where the application is located * Application AppId: the unique id used to identify the application, the format is string, it needs to correspond to the app.id configured in the client app.properties * Application Name: the name of the application, used only for interface display * Application Manager: The person who selects it will be the administrator of the project by default, with permissions to manage project permissions, create clusters, create Namespace, etc. ![create-app](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/create-app.png) 4. Click Submit After successful creation, it will automatically jump to the project home page ![app-created](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/app-created.png) ## 1.2 Project permission assignment ### 1.2.1 Project administrator privileges The project administrator has the following privileges. 1. Can manage the permissions assignment of the project 2. Create clusters 3. Can create Namespace If you need someone else to be the project administrator, you can follow the steps below. 1. Click "Manage Projects" on the left side of the page * ![app-permission-entry](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/app-permission-entry.png) 2. Search for the member you want to add and click Add * ![app-permission-search-user](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/app-permission-search-user.png) * ![app-permission-user-added](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/app-permission-user-added.png) ### 1.2.2 Configuring editing and publishing permissions Configuration permissions are divided into edit and publish. * Edit permission allows users to create, modify, and delete configurations on the Apollo interface * Configuration modification only changes on Apollo interface and does not affect the actual configuration used by the application * Publish permissions allow users to publish and roll back configurations in the Apollo interface * Configurations are only actually used by the application after they have been published and rolled back * Apollo notifies the application in real time after the user has performed a publish or rollback action and the latest configuration takes effect After the project is created, there are no editing and publishing permissions assigned to the configuration by default, and the project administrator needs to authorize it. 1. Click the authorization button of the namespace application * ![namespace-permission-entry](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/namespace-permission-entry.png) 2. Assign the modify permission * ![namespace-permission-edit](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/namespace-permission-edit.png) 3. Assign publish privileges * ![namespace-publish-permission](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/namespace-publish-permission.png) ### 1.2.3 Configuring permissions for different dimensions Regarding Apollo's configuration permissions, the permissions were bound to the namespace during the initial design. Because Apollo's permission management itself is relatively flexible, it can be expanded on this basis. Design of main entity classes based on Apollo [E-R Diagram](/docs/en/design/apollo-design.md?id=_14-e-r-diagram), We can think of Namespace as the smallest unit of permissions, and App as the largest unit of permissions. In the middle are Env and Cluster, so we can manage permissions in different dimensions. | App | Env | Cluster | Namespace | Model | Impl | | --- | --- | --- | --- | --- |------| | ☑️ | | | | App → * | no | | ☑️ | | | ☑️ | App → Namespace | yes | | ☑️ | ☑️ | | | App + Env → * | no | | ☑️ | ☑️ | | ☑️ | App + Env → Namespace | yes | | ☑️ | ☑️ | ☑️ | | App + Env + Cluster → * | yes | | ☑️ | ☑️ | ☑️ | ☑️ | App + Env + Cluster → Namespace | no | Explanation of different permission models: | Model | Target | PermissionType (e.g. Modify) | TargetId | | --- | --- | --- | --- | | App → * | All namespaces of App | | | | App → Namespace | All namespaces with specified names under App | ModifyNamespace | App+Namespace | | App + Env → * | All namespaces under App's env | | | | App + Env → Namespace | All namespaces with specified names under App's env | ModifyNamespace | App+Namespace+Env | | App + Env + Cluster → * | All namespaces of the cluster in App's env | ModifyNamespaceInCluster | App+Env+ClusterName | | App + Env + Cluster → Namespace | The namespace with the specified name under the cluster in App's env | | | #### 1.2.3.1 All namespaces of App 1. Click the authorization button of the application * ![ns-permission-app-allns-entry](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/ns-permission-app-allns-entry.png) 2. Select "All environments" * ![ns-permission-app-allns-select](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/ns-permission-app-allns-select.png) 3. Assign the modify permission * ![namespace-permission-edit](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/namespace-permission-edit.png) 4. Assign publish privileges * ![namespace-publish-permission](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/namespace-publish-permission.png) #### 1.2.3.2 All namespaces of App's env 1. Click the authorization button of the application * ![ns-permission-app-allns-entry](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/ns-permission-app-allns-entry.png) 2. Select the env * ![ns-permission-app-env-ns-select](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/ns-permission-app-env-ns-select.png) 3. Assign the modify permission * ![namespace-permission-edit](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/namespace-permission-edit.png) 4. Assign publish privileges * ![namespace-publish-permission](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/namespace-publish-permission.png) #### 1.2.3.3 All namespaces of the cluster in App's env 1. Click "Manage Cluster" to enter the management cluster page * ![manage-cluster-entry](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/manage-cluster-entry.png) 2. Click the authorization button of the Cluster you want to manage * ![ns-permission-app-env-cluster-entry](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/ns-permission-app-env-cluster-entry.png) 3. Edit permissions * ![ns-permission-app-env-cluster-edit](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/ns-permission-app-env-cluster-edit.png) ## 1.3 Adding configuration items To edit the configuration, you need to have the edit permission of this Namespace. If you find that there is no Add Configuration button, you can find the project administrator to authorize it. ### 1.3.1 Adding configuration via form mode 1. Click Add Configuration * ![create-item-entry](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/create-item-entry.png) 2. Enter a configuration item * ![create-item-detail](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/create-item-detail.png) 3. Click Submit * ![item-created](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/item-created.png) ### 1.3.2 Editing via text mode Apollo not only supports table mode to add and modify configurations one by one, but also provides text mode to add and modify them in bulk. This is especially useful for migrating from an existing properties file. 1. Switch to text editing mode ![text-mode-config-overview](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/text-mode-config-overview.png) 2. Click the Modify Configuration button on the right ![text-mode-config-entry](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/text-mode-config-entry.png) 3. Enter the configuration entries and click on Submit Changes ![text-mode-config-submit](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/text-mode-config-submit.png) ## 1.4 Publishing the configuration The configuration will only really be used by the application after it has been published, so after editing the configuration, you need to publish it. If you find that there is no publish button, you can ask the project administrator to authorize it. 1. Click the "Publish button" ![publish-entry](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/hermes-portal-publish-entry.png) 2. Fill in the information about the publish and click Publish ![publish-detail](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/hermes-portal-publish-detail.png) ## 1.5 Application reads the configuration After the configuration is successfully published, the application can read the configuration through the Apollo client. Apollo currently provides a Java client. For more information, please click [Java Client Usage Documentation](en/client/java-sdk-user-guide). If the application uses other languages, you can also get the configuration by directly accessing the Http interface, for details, please refer to [other-language-client-access-guide](en/client/other-language-client-user-guide) ## 1.6 Rollback published configuration If you find any problem with the released configuration, you can roll back the configuration read by the client to the previous release by clicking the "Rollback" button. The rollback mechanism here is similar to the release system, where the rollback operation in the release system rolls back the installed packages deployed to the machine to the previous deployed version, but the code in the code repository is not rolled back, so that development can re-release the code after fixing it. The rollback in Apollo is a similar mechanism. Clicking rollback rolls back the configuration published to the client to the previous published version, which means that the configuration read by the client will be restored to the previous version, but the configuration in the edited state on the page will not be rolled back, so that the developer can re-publish after fixing the configuration. ## 1.7 Configuration queries (administrator privileges) After a configuration has been added or modified, the administrator user can make a query for the configuration item it belongs to as well as jump to modifications by going to the `Administrator Tools - Global Search for Value` page. The query here is a fuzzy search, where at least one of the key and value of the configuration item is searched to find out in which application, environment, cluster, namespace the configuration is used. - Properties format configuration can be retrieved directly from the key and value ![Configuration query-properties](../images/Configuration query-properties.png) - xml, json, yml, yaml, txt and other formats configuration, because the storage of content-value storage, so you can key = content, value = configuration item content, retrieval ![Configuration query-Non properties](../images/Configuration query-Non properties.png) # II. Public component access guide ## 2.1 Difference between public components and common applications Public components are those client code that are published for use by other applications, such as the CAT client, Hermes Producer client, etc. Although such components are developed and maintained by other teams, the runtime is within the actual business application, so they can essentially be considered part of the application. Usually, the configuration used by these components is maintained by the original development team, but since the runtime and environment of the actual application vary, we also allow the application to override some of the configuration of the public components when they are actually used. ## 2.2 Steps to access public components The access steps for public components are almost identical to those for normal applications, the only difference being that public components need to create their own unique Namespace. So, first perform the following steps in the common application access document, and then follow the steps later in this section. 1. [Create Project](en/portal/apollo-user-guide?id=_11-creating-a-project) 2. [Project administrator privileges](en/portal/apollo-user-guide?id=_121-project-administrator-privileges) ### 2.2.1 Creating Namespace If you find no Add Namespace button, you can find the project administrator to authorize it. 1. Click Add Namespace on the left side of the page * ![create-namespace](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/create-namespace.png) 2. Click "Create New Namespace". * ![create-namespace-select-type](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/create-namespace-select-type.png) 3. Enter the namespace name for the public component. Note that the namespace name is globally unique. * Apollo will add the department name at the top by default * ![create-namespace-detail](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/create-namespace-detail.png) 4. After clicking Submit, the page will automatically jump to the Associated Namespace page * First, select all the environments and clusters that need to have this Namespace, it is generally recommended to select all of them. * Second, select the namespace you just created. * Finally, click Submit Finally, click Submit * ![link-namespace-detail](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/link-namespace-detail.png) 5. After successful linking, the page will automatically jump to the Namespace permission management page 1. Assign the permission to modify the namespace - ​ ![namespace-permission-edit](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/namespace-permission-edit.png) 2. Assign permission to publish - ​ ![namespace-publish-permission](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/namespace-publish-permission.png) 6. Click "Back" to return to the project page ### 2.2.2 Adding configuration items To edit the configuration, you need to have the edit permission of this Namespace. If you find that there is no Add Configuration button, you can find the project administrator to authorize it. #### 2.2.2.1 Adding configuration via form mode 1. Click Add Configuration ![public-namespace-edit-item-entry](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/public-namespace-edit-item-entry.png) 2. Enter the configuration items ![public-namespace-edit-item](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/public-namespace-edit-item.png) 3. Click Submit ![public-namespace-item-created](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/public-namespace-item-created.png) #### 2.2.2.2 Edit by text mode This part is the same as normal application, please refer to [1.3.2 Editing via text mode](en/portal/apollo-user-guide?id=_132-editing-via-text-mode) for the detailed steps. ### 2.2.3 Publish configuration The configuration will only really be used by the application after it has been published, so after editing the configuration, it needs to be published. If you find that there is no publish button, you can ask the project administrator to authorize it. 1. Click the "Publish button" ![public-namespace-publish-items-entry](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/public-namespace-publish-items-entry.png) 2. Fill in the information about publishing and click Publish ![public-namespace-publish-items](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/public-namespace-publish-items.png) ### 2.2.4 Application reading configuration Once the configuration is successfully published, the application can read the configuration through the Apollo client. Apollo currently provides a Java client. For more information, click [Java Client Usage Documentation](en/client/java-sdk-user-guide). If the application uses other languages, you can also get the configuration by directly accessing the Http interface, for details, please refer to [other-language-client-access-guide](en/client/other-language-client-user-guide) For reading the configuration of public components, you can refer to the "Getting the Configuration of Public Namespace" section in the above document. ## 2.3 Application Override Public Component Configuration Steps As mentioned earlier, the configuration of public components is usually maintained by the original development team, but since the actual application runtime and environment vary, we also allow applications to override some of the configuration of public components when they are actually used. Here is how the application can override the configuration of the public component. For simplicity, assume that the apollo-portal application uses the hermes producer client and wants to adjust the batch send size of hermes. ### 2.3.1 Associating the public component Namespace 1. Go to the home page of the application project that uses the public component and click the Add Namespace button on the left * So, in this example, we need to go to the home page of apollo-portal. * (Adding Namespace requires project administrator privileges, if you find no Add Namespace button, you can find the project administrator to authorize it) * ![link-public-namespace-entry](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/link-public-namespace-entry.png) 2. Find the namespace of the hermes producer and select which environments and clusters to associate with it ![link-public-namespace](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/link-public-namespace.png) 3. After successful linking, the page will automatically jump to the Namespace permission management page 1. Assign modify permission ![namespace-permission-edit](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/namespace-permission-edit.png) 2. Assign permission to publish ![namespace-publish-permission](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/namespace-publish-permission.png) 4. Click "Back" to return to the project page ### 2.3.2 Overriding common component configuration 1. Click Add Configuration ![override-public-namespace-entry](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/override-public-namespace-entry.png) 2. Enter the configuration items to be overridden ![override-public-namespace-item](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/override-public-namespace-item.png) 3. Click Submit ![override-public-namespace-item-done](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/override-public-namespace-item-done.png) ### 2.3.3 Publish configuration The configuration will only really be used by the application after it is published, so after editing the configuration, it needs to be published. To publish the configuration, you need to have the publish permission of this Namespace, if you find that there is no publish button, you can ask the project administrator to authorize it. 1. Click the "Publish button" ![override-public-namespace-item-publish-entry](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/override-public-namespace-item-publish-entry.png) 2. Fill in the information about the publication and click Publish ![override-public-namespace-item-publish](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/override-public-namespace-item-publish.png) 3. After the configuration is successfully published, the value of sender.batchSize read by the hermes producer client when running inside the apollo-portal application is 1000. # III. Cluster independent configuration instructions In some special cases, the application has the need to do different configurations for different clusters, such as the application deployed in server room A connects to a different es server address than the application deployed in server room B connects to a different es server address. In this case, it can be solved by creating different clusters in Apollo. ## 3.1 Creating a cluster If you find that there is no Add Cluster button, you can ask the project administrator for authorization. 1. Click the "Add Cluster" button on the left side of the page * ![create-cluster](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/create-cluster.png) 2. Enter the cluster name, select the environment and submit * ![create-cluster-detail](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/create-cluster-detail.png) 3. Switch to the corresponding cluster, modify the configuration and release it * ![config-in-cluster-created](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/cluster-created.png) 4. With the above configuration, the application deployed in the SHAJQ server room will read the configuration under the SHAJQ cluster 5. If the application is also deployed in other server rooms, then with the above configuration, the configuration under the default cluster will be read. # IV. Using the same configuration for multiple AppId In some cases, although the application itself is not a public component, it still needs to share the same configuration among multiple AppId, such as different projects of the same product: XX-Web, XX-Service, XX-Job and so on. In this case, if you want to implement multiple AppId to use the same configuration, the basic concept is the same as the configuration of public components. Specifically, we create a namespace under one of the AppId, write the public configuration information, and then read the configuration of the namespace in each project. If an AppId needs to override the public configuration information, then associate a public namespace under that AppId and write the configuration that needs to be overridden. The specific steps can be referred to [Public Component Access Guide](en/portal/apollo-user-guide?id=ii-public-component-access-guide). # V. Grayscale publishing usage guide With the grayscale release function, you can achieve. 1. For some configurations that have a relatively large impact on the program, you can first take effect in one or more instances, and observe no problem for a period of time before releasing the configuration in full. 2. For some configuration parameters that need to be tuned, A/B testing can be achieved through the grayscale release function. You can apply different configurations on different machines and keep tuning and evaluating for a period of time to find out the better configuration before releasing the configuration in full. The following is a practical example to describe how to use the grayscale release function. ## 5.1 Introduction to the scenario The 100004458 (apollo-demo) project has two clients. 1. 10.32.21.19 2. 10.32.21.22 ![initial-instance-list](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/gray-release/initial-instance-list.png) **Grayscale targets:** * Currently there is a configuration timeout=2000, we want to grayscale release timeout=3000 for 10.32.21.22 and still timeout=2000 for 10.32.21.19. ![initial-config](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/gray-release/initial-config.png) ## 5.2 Creating greyscale First click the `create grayscale` button in the top right corner of the application namespace. ![create-gray-release](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/gray-release/create-gray-release.png) After clicking OK, the grayscale version is created successfully and the page will automatically switch to the `grayscale version` tab. ![initial-gray-release-tab](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/gray-release/initial-gray-release-tab.png) ## 5.3 Grayscale configuration Click on the `Configure main version`, the `Gray this configuration` button on the far right of the timeout configuration ![initial-gray-release-tab](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/gray-release/edit-gray-release-config.png) Fill in the popup box with the value to be grayed out: 3000 and click Submit. ![submit-gray-release-config](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/gray-release/submit-gray-release-config.png) ![gray-release-config-submitted](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/gray-release/gray-release-config-submitted.png) After the grayscale configuration, confirm the grayscale configuration and the major version configuration. Click on the `Grayscale Publish` button. ![click-gray-release](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/gray-release/click-gray-release.png) Confirm the grayscale configuration to be released by comparing the value of the major version and the published value of the grayscale version. ![gray-release-diff-items](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/gray-release/gray-release-diff-items.png) ## 5.4 Configuring grayscale rules Switch to the `Gray Rule` Tab and click the `Add Rule` button ![new-gray-release-rule](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/gray-release/new-gray-release-rule.png) In the popup box `Grayed IP` dropdown box will show the list of machines currently using the configuration by default, select the IP we want to gray. ![select-gray-release-ip](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/gray-release/select-gray-release-ip.png ) ![gray-release-ip-selected](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/gray-release/gray-release-ip-selected.png) In addition to the IP dimension, from version 2.0.0 onwards there is also support for identifying the list of instances in grayscale by label, which is suitable for scenarios where the IP is not fixed such as `Kubernetes`. Manually enter the label you want to set, and click the Add button when you're done. ![manual-input-gray-release-label](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/gray-release/manual-input-gray-release-label.png) ![manual-input-gray-release-label-2](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/gray-release/manual-input-gray-release-label2.png) ![gray-release-rule-saved](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/gray-release/gray-release-rule-saved.png) After the above rules are configured, the grayed configuration will take effect for instances with AppId `100004458`, IP `10.32.21.22` or `Label` marked as `myLabel` or `appLabel`. > For more information on how to label `Label`, you can refer to the configuration instructions in [ApolloLabel](en/client/java-sdk-user-guide?id=_1247-apollo-label). If the required IP is not found in the drop-down box, it means that the machine has not yet taken the configuration from Apollo, you can enter it by clicking Enter IP manually, and click the Add button after entering the IP. ![manual-input-gray-release-ip](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/gray-release/manual-input-gray-release-ip.png) ![manual-input-gray-release-ip-2](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/gray-release/manual-input-gray-release-ip-2.png) >Note: For the grayscale rule of public Namespace, you need to specify the appId to be grayscale first, then select the IP and Label. ## 5.5 Grayscale Release The configuration rules are in effect, but the grayscale configuration has not been published yet. Switch to the `Configuration` Tab. Check the grayscale configuration section again, and if there are no problems, click `Grayscale Publish`. ![prepare-to-do-gray-release](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/gray-release/prepare-to-do-gray-release.png) In the popup box, you can see that the value of the master version is 2000 and the value of the gray version to be released is 3000. fill in the other information and click on release. ![gray-release-confirm-dialog](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/gray-release/gray-release-confirm-dialog.png) After the release, switch to the `gray instance list` Tab and you can see that 10.32.21.22 has used the values of the gray release. ![gray-release-instance-list](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/gray-release/gray-release-instance-list.png) Switch to the `instance list` of the `master release` and you will see that only 10.32.21.19 of the master configuration is in use. ![master-branch-instance-list](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/gray-release/master-branch-instance-list.png) You can continue with configuration changes or rule changes later. Configuration changes need to click Gray Release before they take effect, and rule changes take effect in real time after the rule is clicked to complete. ## 5.6 Full Release If the grayscale configuration is tested down ideally and meets expectations, then you can operate `full release`. The effect of a full release is that 1. The grayscale configuration will be merged back to the main version, in this case, the timeout of the main version will be updated to 3000 2. The configuration of the main version will be automatically published once 3. In the full release page, you can choose whether to keep the current grayscale version, the default is not to keep. ![prepare-to-full-release](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/gray-release/prepare-to-full-release.png) ![full-release-confirm-dialog](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/gray-release/full-release-confirm-dialog.png) ![full-release-confirm-dialog-2](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/gray-release/full-release-confirm-dialog-2.png) I chose not to keep the grayscale version, so the effect after the release is that the configuration of the master version is updated and the grayscale version is deleted. Clicking on the instance list of the main version, you can see that 10.32.21.22 and 10.32.21.19 both use the latest configuration of the main version. ![master-branch-instance-list-after-full-release](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/gray-release/master-branch-instance-list-after-full-release.png) ## 5.7 Giving up grayscale If the grayscale version is not ideal or not needed anymore, you can click `Drop Grayscale`. ![abandon-gray-release](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/gray-release/abandon-gray-release.png) ## 5.8 Release History Click the `release history` button of the main release to see the release history of the current namespace's main release as well as the grayscale version. ![view-release-history](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/gray-release/view-release-history.png) ![view-release-history-detail](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/gray-release/view-release-history-detail.png) # VI、Other Configurations ## 6.1 Configure view permissions Starting from version 1.1.0, apollo-portal has added support for view permissions, which allows you to configure an environment to allow only project members to view the private Namespace configuration. The project members here are : 1. The project's administrator 2. Have the permission to modify or publish the private Namespace in that environment The configuration is very simple. After logging in with your super administrator account, go to the `Administrator Tools - System Parameters` page and add or modify the `configView.memberOnly.envs` configuration item. ![configView.memberOnly.envs](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/configure-view-permissions.png) ## 6.2 Configuring access keys Apollo has added an access key mechanism since version 1.6.0, so that only authenticated clients can access sensitive configurations. If the application has access keys enabled, the client needs to configure the keys, otherwise the configuration cannot be accessed. 1. The project administrator opens the Manage Keys page ![Admin Key Portal](https://user-images.githubusercontent.com/837658/94990081-f4d3cd80-05ab-11eb-9470-fed5ec6de92e.png) 2. Generate an access key for each environment of the project, note that it is disabled by default, and it is recommended to turn it on after the clients are all configured ![Key Configuration Page](https://user-images.githubusercontent.com/837658/94990150-788dba00-05ac-11eb-9a12-727fdb872e42.png) 3. Client-side [configure access key](en/client/java-sdk-user-guide?id=_1244-configuring-access-keys) . ## 6.3 System parameterization of global search configuration items Starting from version 2.4.0, apollo-portal adds the ability to globally search for configuration items by fuzzy retrieval of the key and value of a configuration item to find out which application, environment, cluster, or namespace the configuration item with the corresponding value is used in. In order to prevent memory overflow (OOM) problems when performing global view searches of configuration items, we introduce a system parameter `apollo.portal.search.perEnvMaxResults`, which is used to limit the number of maximum search results per environment configuration item in a single search. By default, this value is set to `200`, but administrators can adjust it to suit their actual needs. **Setting method:** 1. Log in to the Apollo Configuration Center interface with a super administrator account. 2. Just go to the `Administrator Tools - System Parameters` page and add or modify the `apollo.portal.search.perEnvMaxResults` configuration item. Please note that modifications to system parameters may affect the performance of the search function, so you should perform adequate testing and ensure that you understand exactly what the parameters do before making changes. ![System-parameterization-of-global-search-configuration-items](../images/System-parameterization-of-global-search-configuration-items.png) ## 6.4 Parameter settings for limiting the number of namespaces in the appld+cluster dimension Starting from version 2.4.0, apollo-portal provides the function of checking the upper limit of the number of namespaces that can be created under the appld+cluster dimension. This function is disabled by default and needs to be enabled by configuring the system `namespace.num.limit.enabled`. At the same time, the system parameter `namespace.num.limit` is provided to dynamically configure the upper limit of the number of Namespaces under the appld+cluster dimension. The default value is 200. Considering that some basic components such as gateways, message queues, Redis, and databases require special processing, a new system parameter `namespace.num.limit.white` is added to configure the verification whitelist, which is not affected by the upper limit of the number of Namespaces. **Setting method:** 1. Log in to the Apollo Configuration Center interface with a super administrator account. 2. Go to the `Administrator Tools - System Parameters - ConfigDB Configuration Management` page and add or modify the `namespace.num.limit.enabled` configuration item to true/false to enable/disable this function. It is disabled by default. ![item-num-limit-enabled](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/namespace-num-limit-enabled.png) 3. Go to the `Administrator Tools - System Parameters - ConfigDB Configuration Management` page to add or modify the `namespace.num.limit` configuration item to configure the upper limit of the number of namespaces under a single appld+cluster. The default value is 200 ![item-num-limit](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/namespace-num-limit.png) 4. Go to `Administrator Tools - System Parameters - ConfigDB Configuration Management` page to add or modify the `namespace.num.limit.white` configuration item to configure the whitelist for namespace quantity limit verification. Multiple AppIds are separated by English commas. ![item-num-limit](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/namespace-num-limit-white.png) ## 6.5 Limitation on the number of configuration items in a single namespace Starting from version 2.4.0, apollo-portal provides the function of limiting the number of configuration items in a single namespace. This function is disabled by default and needs to be enabled by configuring the system `item.num.limit.enabled`. At the same time, the system parameter `item.num.limit` is provided to dynamically configure the upper limit of the number of items in a single Namespace. **Setting method:** 1. Log in to the Apollo Configuration Center interface with a super administrator account 2. Go to the `Administrator Tools - System Parameters - ConfigDB Configuration Management` page and add or modify the `item.num.limit.enabled` configuration item to true/false to enable/disable this function. ![item-num-limit-enabled](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/item-num-limit-enabled.png) 3. Go to the `Admin Tools - System Parameters - ConfigDB Configuration Management` page and add or modify the `item.num.limit` configuration item to configure the upper limit of the number of items under a single Namespace. ![item-num-limit](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/item-num-limit.png) # VII. Best practices ## 7.1 Security Related As a basic service, the configuration center stores very important configuration information of the company, so security factors need to be focused on, some considerations are listed below for your reference, and you are also welcome to share your own practice cases. ### 7.1.1 Authentication It is recommended to access the company's unified authentication system, such as SSO, LDAP, etc. The access method can be found in [Portal to implement user login function](en/extension/portal-how-to-implement-user-login-function) > If you use Spring Security simple authentication provided by Apollo, you must remember to change the password of super administrator apollo ### 7.1.2 Authorization Apollo supports fine-grained permissions control, so please make sure to control the permissions according to the actual situation: 1. 1. [Project administrator privileges](en/portal/apollo-user-guide?id=_121-project-administrator-privileges) * Apollo allows all logged-in users to create projects by default. If you only allow some users to create projects, you can turn on [Create project permission control](en/deployment/distributed-deployment-guide?id=_3110-rolecreate-applicationenabled-whether-to-enable-control-over-creating-project-permissions) 2. [Configure editing and publishing privileges](en/portal/apollo-user-guide?id=_122-configuring-editing-and-publishing-permissions) * Configuration editing and publishing privileges support configuration by environment, for example, the development environment developers can complete the process of configuration editing and publishing by themselves, but the production environment publishing privileges to the test or operation and maintenance personnel * It is recommended to turn on [release audit](en/deployment/distributed-deployment-guide?id=_322-namespacelockswitch-only-one-person-can-modify-the-switch-at-a-time-for-release-review) at the same time for production environment, so as to control that only one person can modify a configuration release and another person can release it. This ensures that configuration changes are adequately checked. 3. [Configuration view permissions](en/portal/apollo-user-guide?id=_61-configure-view-permissions) * You can specify that only project members of an environment are allowed to view the configuration of a private Namespace, thus avoiding sensitive configuration leaks, such as production environments ### 7.1.3 System access In addition to user permissions, system access also needs to be considered in terms of. 1. `apollo-configservice` and `apollo-adminservice` are designed based on the intranet trusted network, so for security reasons, `apollo-configservice` and `apollo-adminservice` are prohibited from being exposed directly to the public network 2. For sensitive configurations, consider enabling [access secret key](en/portal/apollo-user-guide?id=_62-configuring-access-keys) so that only authenticated clients can access sensitive configurations 3. version 1.7.1 and above can consider enabling [access control](en/deployment/distributed-deployment-guide?id=_326-admin-servicesaccesscontrolenabled-configure-whether-apollo-adminservice-has-access-control-enabled) for `apollo-adminservice`, so that only [controlled](en/deployment/distributed-deployment-guide?id=_3112-admin-servicesaccesstokens-set-the-access-token-required-by-apollo-portal-to-access-the-apollo-adminservice-for-each-environment) `apollo-portal` can access the corresponding interface to enhance security 4. version 2.1.0 and above can consider enabling [access control](en/deployment/distributed-deployment-guide?id=_329-apolloeurekaserversecurityenabled-configure-whether-to-enable-eureka-login-authentication) for `eureka`, so that only controlled `apollo-configservice` and `apollo-adminservice` can be registered to `eureka` to enhance security ================================================ FILE: docs/en/portal/apollo-user-practices.md ================================================ Practical examples of Apollo configuration center for your reference. * [Apollo+ES source code transformation to build China Minsheng Bank's ELK logging platform configuration management center](https://mp.weixin.qq.com/s/VHugn0vgNu4m56V49geC4w) * [Apollo practice in Youzan.Inc](https://mp.weixin.qq.com/s/Ge14UeY9Gm2Hrk--E47eJQ) * [Initial Design Ideas for Microservice Version Switching](https://blog.llyweb.com/articles/2020/08/11/1597149013480.html) * [Alibaba Sentinel Push pattern Rule Push Base on Apollo-Configuration-Center](https://anilople.github.io/Sentinel) ================================================ FILE: docs/en/quick-start.md ================================================ # Prepare Wait for content... ```bash content for copy ``` ================================================ FILE: docs/index.html ================================================ Apollo
    Loading ...
    ================================================ FILE: docs/scripts/multiple-language-redirect.js ================================================ /* * Copyright 2024 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ /* * A custom docsify plugin. Let user switch to another language more convenient. * Reference https://docsify.js.org/#/write-a-plugin * * @author wxq */ /* JavaScript's function below */ function findCurrentLanguagePrefix(languagePrefixs, path) { languagesPrefixOrderByLengthDesc = languagePrefixs.sort(function (a, b) { return b.length - a.length; }) for (const languagePrefix of languagesPrefixOrderByLengthDesc) { if (path.startsWith(languagePrefix)) { return languagePrefix; } } console.error("cannot find language prefix in path", path, "through", languagePrefixs) return '/'; } function generateLanguagePrefix2Path(languagePrefixs, path) { const currentLanguagePrefix = findCurrentLanguagePrefix(languagePrefixs, path); languagePrefix2Path = {}; for (const targetLanguagePrefix of languagesPrefixOrderByLengthDesc) { languagePrefix2Path[targetLanguagePrefix] = path.replace(currentLanguagePrefix, targetLanguagePrefix); } return languagePrefix2Path; } /** * find list item in navbar by its name. */ function findTranslationsListItem(translationsListItemName) { const nav = document.querySelector("nav"); if (null == nav) { return null; } const ul = nav.querySelector("ul"); if (null == ul) { return null; } const listItems = ul.querySelectorAll("li"); if (null == listItems) { return null; } for (const listItem of listItems) { if (listItem.innerText.includes(translationsListItemName)) { return listItem; } } return null; } function walkElementInTranslationsListItem(translationsListItem, elementName, visitor) { const elements = translationsListItem.querySelectorAll(elementName); for (const element of elements) { visitor(element); } } function removeSharpPrefixInHref(href) { if (href.startsWith("#")) { // delete '#' in prefix, example: '#/zh-cn/' => '/zh-cn/' return href.substring(1); } else { return href; } } function addSharpToPrefix(path) { if (path.startsWith("#")) { return path; } else { return "#" + path; } } /** * find language which user config in '_navbar.md'. * * @returns a list of language prefix config in '_navbar.md' */ function resolveLanguagePrefixsFromListItem(translationsListItem) { languagePrefixs = [] walkElementInTranslationsListItem(translationsListItem, 'a', function (aElement) { const href = aElement.getAttribute("href"); languagePrefix = removeSharpPrefixInHref(href); languagePrefixs.push(languagePrefix); }); // a return example: ['/', '/zh-cn/', '/de-de/', '/es/', '/ru-ru/'] return languagePrefixs; } function changeLinkInTranslationsListItem(currrentPath, translationsListItem) { const languagePrefixs = resolveLanguagePrefixsFromListItem(translationsListItem); const languagePrefix2Path = generateLanguagePrefix2Path(languagePrefixs, currrentPath); walkElementInTranslationsListItem(translationsListItem, 'a', function (aElement) { const href = aElement.getAttribute("href"); const languagePrefix = removeSharpPrefixInHref(href); const newPath = languagePrefix2Path[languagePrefix]; const newHref = addSharpToPrefix(newPath); aElement.setAttribute("href", newHref); // console.log(href, "=>", newHref); }); } /** * When user click another language in navbar's Translations, * website's path will change to path which corresponding to current language. * @param {string} name item name 'Translations' in navbar */ function generateMultipleLanguagesNavbarPluginByListItemName(name) { return function (hook, vm) { const bindEventForChangeHrefInNavbar = () => { // when user's mouse down, change href in navbar document.addEventListener("mousedown", _mouseEvent => { const currrentPath = vm.route.path; // find navbar list item by hard code name const translationsListItemName = name; const translationsListItem = findTranslationsListItem(translationsListItemName); if (null == translationsListItem) { console.warn("there is no navbar or ", translationsListItemName, "in current path", currrentPath); } else { changeLinkInTranslationsListItem(currrentPath, translationsListItem); } }); }; hook.init(bindEventForChangeHrefInNavbar); }; } ================================================ FILE: docs/zh/README.md ================================================ apollo-logo # Introduction Apollo(阿波罗)是一款可靠的分布式配置管理中心,诞生于携程框架研发部,能够集中化管理应用不同环境、不同集群的配置,配置修改后能够实时推送到应用端,并且具备规范的权限、流程治理等特性,适用于微服务配置管理场景。 服务端基于Spring Boot和Spring Cloud开发,打包后可以直接运行,不需要额外安装Tomcat等应用容器。 Java客户端不依赖任何框架,能够运行于所有Java运行时环境,同时对Spring/Spring Boot环境也有较好的支持。 .Net客户端不依赖任何框架,能够运行于所有.Net运行时环境。 更多产品介绍参见[Apollo配置中心介绍](zh/design/apollo-introduction) 本地快速部署请参见[Quick Start](zh/deployment/quick-start) 演示环境(Demo): - [http://81.68.181.139](http://81.68.181.139/) - 账号/密码:apollo/admin > 如访问GitHub速度缓慢,可以访问[Gitee镜像](https://gitee.com/apolloconfig/apollo),不定期同步 # Screenshots ![配置界面](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/apollo-home-screenshot.jpg) # Features * **统一管理不同环境、不同集群的配置** * Apollo提供了一个统一界面集中式管理不同环境(environment)、不同集群(cluster)、不同命名空间(namespace)的配置。 * 同一份代码部署在不同的集群,可以有不同的配置,比如zk的地址等 * 通过命名空间(namespace)可以很方便的支持多个不同应用共享同一份配置,同时还允许应用对共享的配置进行覆盖 * 配置界面支持多语言(中文,English) * **配置修改实时生效(热发布)** * 用户在Apollo修改完配置并发布后,客户端能实时(1秒)接收到最新的配置,并通知到应用程序。 * **版本发布管理** * 所有的配置发布都有版本概念,从而可以方便的支持配置的回滚。 * **灰度发布** * 支持配置的灰度发布,比如点了发布后,只对部分应用实例生效,等观察一段时间没问题后再推给所有应用实例。 - **配置项的全局视角搜索** - 通过对配置项的key与value进行的模糊检索,找到拥有对应值的配置项在哪个应用、环境、集群、命名空间中被使用。 - 通过高亮显示、分页与跳转配置等操作,便于让管理员以及SRE角色快速、便捷地找到与更改资源的配置值。 * **权限管理、发布审核、操作审计** * 应用和配置的管理都有完善的权限管理机制,对配置的管理还分为了编辑和发布两个环节,从而减少人为的错误。 * 所有的操作都有审计日志,可以方便的追踪问题。 * **客户端配置信息监控** * 可以方便的看到配置在被哪些实例使用 * **提供Java和.Net原生客户端** * 提供了Java和.Net的原生客户端,方便应用集成 * 支持Spring Placeholder,Annotation和Spring Boot的ConfigurationProperties,方便应用使用(需要Spring 3.1.1+) * 同时提供了Http接口,非Java和.Net应用也可以方便的使用 * **提供开放平台API** * Apollo自身提供了比较完善的统一配置管理界面,支持多环境、多数据中心配置管理、权限、流程治理等特性。 * 不过Apollo出于通用性考虑,对配置的修改不会做过多限制,只要符合基本的格式就能够保存。 * 在我们的调研中发现,对于有些使用方,它们的配置可能会有比较复杂的格式,如xml, json,需要对格式做校验。 * 还有一些使用方如DAL,不仅有特定的格式,而且对输入的值也需要进行校验后方可保存,如检查数据库、用户名和密码是否匹配。 * 对于这类应用,Apollo支持应用方通过开放接口在Apollo进行配置的修改和发布,并且具备完善的授权和权限控制 * **部署简单** * 配置中心作为基础服务,可用性要求非常高,这就要求Apollo对外部依赖尽可能地少 * 目前唯一的外部依赖是MySQL,所以部署非常简单,只要安装好Java和MySQL就可以让Apollo跑起来 * Apollo还提供了打包脚本,一键就可以生成所有需要的安装包,并且支持自定义运行时参数 # Release Notes * [版本发布历史](https://github.com/apolloconfig/apollo/releases) # Presentation * [开源配置中心Apollo的设计与实现](http://www.itdks.com/dakalive/detail/3420) * [Slides](https://github.com/apolloconfig/apollo-community/blob/master/slides/design-and-implementation-of-apollo.pdf) * [配置中心,让微服务更『智能』](https://2018.qconshanghai.com/presentation/799) * [Slides](https://github.com/apolloconfig/apollo-community/blob/master/slides/configuration-center-makes-microservices-smart.pdf) # Publication * [开源配置中心Apollo的设计与实现](https://www.infoq.cn/article/open-source-configuration-center-apollo) * [配置中心,让微服务更『智能』](https://mp.weixin.qq.com/s/iDmYJre_ULEIxuliu1EbIQ) # Support
    Apollo技术支持②群
    群号:904287263(未满)
    Apollo技术支持⑤群
    群号:914839843(已满)
    Apollo技术支持④群
    群号:516773934(已满)
    Apollo技术支持③群
    群号:742035428(已满)
    Apollo技术支持①群
    群号:375526581(已满)
    tech-support-qq-2 tech-support-qq-5 tech-support-qq-4 tech-support-qq-3 tech-support-qq-1
    # License The project is licensed under the [Apache 2 license](https://github.com/apolloconfig/apollo/blob/master/LICENSE). ================================================ FILE: docs/zh/_navbar.md ================================================ - 社区 - [团队](zh/community/team.md) - [社区治理](zh/governance.md) - [贡献指南](zh/contributing.md) - [致谢](zh/community/thank-you.md) - Translations - [:uk: English](/en/) - [:cn: 中文](/zh/) ================================================ FILE: docs/zh/_sidebar.md ================================================ - [**首页**](zh/README.md) - 设计文档 - [Apollo配置中心设计](zh/design/apollo-design.md) - [Apollo配置中心介绍](zh/design/apollo-introduction.md) - [Apollo核心概念之“Namespace”](zh/design/apollo-core-concept-namespace.md) - [Apollo源码解析(全)](http://www.iocoder.cn/categories/Apollo/) - 部署文档 - [Quick Start](zh/deployment/quick-start.md) - [Docker方式部署Quick Start](zh/deployment/quick-start-docker.md) - [分布式部署指南](zh/deployment/distributed-deployment-guide.md) - [部署架构](zh/deployment/deployment-architecture.md) - 第三方工具部署 - [基于Rainbond一键安装高可用Apollo集群](zh/deployment/third-party-tool-rainbond.md) - [基于宝塔面板快速部署 Apollo](zh/deployment/third-party-tool-btpanel.md) - 管理端指南 - [Apollo使用指南](zh/portal/apollo-user-guide.md) - [Apollo开放平台接入指南](zh/portal/apollo-open-api-platform.md) - [Apollo实践案例](zh/portal/apollo-user-practices.md) - [Apollo安全相关最佳实践](zh/portal/apollo-user-guide?id=_71-%e5%ae%89%e5%85%a8%e7%9b%b8%e5%85%b3) - [Apollo使用场景和示例代码](https://github.com/ctripcorp/apollo-use-cases) - 客户端指南 - [Java 客户端使用指南](zh/client/java-sdk-user-guide.md) - [.Net 客户端使用指南](zh/client/dotnet-sdk-user-guide.md) - [Golang 客户端使用指南](zh/client/golang-sdks-user-guide.md) - [Python 客户端使用指南](zh/client/python-sdks-user-guide.md) - [NodeJS 客户端使用指南](zh/client/nodejs-sdks-user-guide.md) - [PHP 客户端使用指南](zh/client/php-sdks-user-guide.md) - [C 客户端使用指南](zh/client/c-sdks-user-guide.md) - [C++ 客户端使用指南](zh/client/cpp-sdks-user-guide.md) - [Rust 客户端使用指南](zh/client/rust-sdks-user-guide.md) - [K8S ConfigMap接入指南](zh/client/k8s-configmap-user-guide.md) - [HTTP API 接入指南](zh/client/other-language-client-user-guide.md) - 扩展开发 - [Portal实现用户登录功能](zh/extension/portal-how-to-implement-user-login-function.md) - [Portal接入邮件服务](zh/extension/portal-how-to-enable-email-service.md) - [Portal 共享 session](zh/extension/portal-how-to-enable-session-store.md) - [Portal启用webhook通知](zh/extension/portal-how-to-enable-webhook-notification.md) - 开源共建 - [Apollo开发指南](zh/contribution/apollo-development-guide.md) - Code Styles - [Eclipse Code Style](https://github.com/apolloconfig/apollo/blob/master/apollo-buildtools/style/eclipse-java-google-style.xml) - [Intellij Code Style](https://github.com/apolloconfig/apollo/blob/master/apollo-buildtools/style/intellij-java-google-style.xml) - [Apollo 版本发布操作手册](zh/contribution/apollo-release-guide.md) - [贡献指南](zh/contributing.md) - FAQ - [常见问题回答](zh/faq/faq.md) - [部署&开发遇到的常见问题](zh/faq/common-issues-in-deployment-and-development-phase.md) - 其它 - [版本历史](https://github.com/apolloconfig/apollo/releases) - [Apollo性能测试报告](zh/misc/apollo-benchmark.md) - 社区 - [团队](zh/community/team.md) - [社区治理](zh/governance.md) - [致谢](zh/community/thank-you.md) ================================================ FILE: docs/zh/client/c-sdks-user-guide.md ================================================ ### Apollo C 客户端 项目地址:[apollo-c-client](https://github.com/lzeqian/apollo) > 非常感谢[@lzeqian](https://github.com/lzeqian)提供C Apollo客户端的支持 ================================================ FILE: docs/zh/client/cpp-sdks-user-guide.md ================================================ ### Apollo C++ 客户端 项目地址:[apollo-cpp-client](https://github.com/jiazhanfeng1989/apollo-cpp-client) > 非常感谢[@jiazhanfeng](https://github.com/jiazhanfeng1989)提供C++ Apollo客户端的支持 ================================================ FILE: docs/zh/client/dotnet-sdk-user-guide.md ================================================ >注意:本文档适用对象是Apollo系统的使用者,如果你是公司内Apollo系统的开发者/维护人员,建议先参考[Apollo开发指南](zh/contribution/apollo-development-guide)。 #   # 〇、重要提示! > 以下文档是旧文档,已不支持,最新.Net接入文档请参考[Apollo.net框架集成](https://github.com/apolloconfig/apollo.net#一框架集成) # 一、准备工作 ## 1.1 环境要求 * .Net: 4.0+ ## 1.2 必选设置 Apollo客户端依赖于`AppId`,`Environment`等环境信息来工作,所以请确保阅读下面的说明并且做正确的配置: ### 1.2.1 AppId AppId是应用的身份信息,是从服务端获取配置的一个重要信息。 请确保在app.config或web.config有AppID的配置,其中内容形如: ```xml ``` > 注:app.id是用来标识应用身份的唯一id,格式为string。 ### 1.2.2 Environment Apollo支持应用在不同的环境有不同的配置,所以Environment是另一个从服务器获取配置的重要信息。 Environment通过配置文件来指定,文件位置为`C:\opt\settings\server.properties`,文件内容形如: ```properties env=DEV ``` 目前,`env`支持以下几个值(大小写不敏感): * DEV * Development environment * FAT * Feature Acceptance Test environment * UAT * User Acceptance Test environment * PRO * Production environment ### 1.2.3 服务地址 Apollo客户端针对不同的环境会从不同的服务器获取配置,所以请确保在app.config或web.config正确配置了服务器地址(Apollo.{ENV}.Meta),其中内容形如: ```xml ``` ### 1.2.4 本地缓存路径 Apollo客户端会把从服务端获取到的配置在本地文件系统缓存一份,用于在遇到服务不可用,或网络不通的时候,依然能从本地恢复配置,不影响应用正常运行。 本地缓存路径位于`C:\opt\data\{appId}\config-cache`,所以请确保`C:\opt\data\`目录存在,且应用有读写权限。 ### 1.2.5 可选设置 **Cluster**(集群) Apollo支持配置按照集群划分,也就是说对于一个appId和一个环境,对不同的集群可以有不同的配置。 如果需要使用这个功能,你可以通过以下方式来指定运行时的集群: 1. 通过App Config * 我们可以在App.config文件中设置Apollo.Cluster来指定运行时集群(注意大小写) * 例如,下面的截图配置指定了运行时的集群为SomeCluster * ![apollo-net-apollo-cluster](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/apollo-net-apollo-cluster.png) 2. 通过配置文件 * 首先确保`C:\opt\settings\server.properties`在目标机器上存在 * 在这个文件中,可以设置数据中心集群,如`idc=xxx` * 注意key为全小写 **Cluster Precedence**(集群顺序) 1. 如果`Apollo.Cluster`和`idc`同时指定: * 我们会首先尝试从`Apollo.Cluster`指定的集群加载配置 * 如果没找到任何配置,会尝试从`idc`指定的集群加载配置 * 如果还是没找到,会从默认的集群(`default`)加载 2. 如果只指定了`Apollo.Cluster`: * 我们会首先尝试从`Apollo.Cluster`指定的集群加载配置 * 如果没找到,会从默认的集群(`default`)加载 3. 如果只指定了`idc`: * 我们会首先尝试从`idc`指定的集群加载配置 * 如果没找到,会从默认的集群(`default`)加载 4. 如果`Apollo.Cluster`和`idc`都没有指定: * 我们会从默认的集群(`default`)加载配置 # 二、DLL引用 .Net客户端项目地址位于:[https://github.com/ctripcorp/apollo.net](https://github.com/ctripcorp/apollo.net)。 将项目下载到本地,切换到`Release`配置,编译Solution后会在`apollo.net\Apollo\bin\Release`中生成`Framework.Apollo.Client.dll`。 在应用中引用`Framework.Apollo.Client.dll`即可。 如果需要支持.Net Core的Apollo版本,可以参考[dotnet-core](https://github.com/ctripcorp/apollo.net/tree/dotnet-core)以及[nuget仓库](https://www.nuget.org/packages?q=Com.Ctrip.Framework.Apollo) # 三、客户端用法 ## 3.1 获取默认namespace的配置(application) ```c# Config config = ConfigService.GetAppConfig(); //config instance is singleton for each namespace and is never null string someKey = "someKeyFromDefaultNamespace"; string someDefaultValue = "someDefaultValueForTheKey"; string value = config.GetProperty(someKey, someDefaultValue); ``` 通过上述的**config.GetProperty**可以获取到someKey对应的实时最新的配置值。 另外,配置值从内存中获取,所以不需要应用自己做缓存。 ## 3.2 监听配置变化事件 监听配置变化事件只在应用真的关心配置变化,需要在配置变化时得到通知时使用,比如:数据库连接串变化后需要重建连接等。 如果只是希望每次都取到最新的配置的话,只需要按照上面的例子,调用**config.GetProperty**即可。 ```c# Config config = ConfigService.GetAppConfig(); //config instance is singleton for each namespace and is never null config.ConfigChanged += new ConfigChangeEvent(OnChanged); private void OnChanged(object sender, ConfigChangeEventArgs changeEvent) { Console.WriteLine("Changes for namespace {0}", changeEvent.Namespace); foreach (string key in changeEvent.ChangedKeys) { ConfigChange change = changeEvent.GetChange(key); Console.WriteLine("Change - key: {0}, oldValue: {1}, newValue: {2}, changeType: {3}", change.PropertyName, change.OldValue, change.NewValue, change.ChangeType); } } ``` ## 3.3 获取公共Namespace的配置 ```c# string somePublicNamespace = "CAT"; Config config = ConfigService.GetConfig(somePublicNamespace); //config instance is singleton for each namespace and is never null string someKey = "someKeyFromPublicNamespace"; string someDefaultValue = "someDefaultValueForTheKey"; string value = config.GetProperty(someKey, someDefaultValue); ``` ## 3.4 Demo apollo.net项目中有一个样例客户端的项目:`ApolloDemo`,具体信息可以参考[Apollo开发指南](zh/contribution/apollo-development-guide)中的[2.4 .Net样例客户端启动](zh/contribution/apollo-development-guide?id=_24-net样例客户端启动)部分。 >注:Apollo .Net客户端开源版目前默认会把日志直接输出到Console,大家可以自己实现Logging相关功能。 > > 详见[https://github.com/ctripcorp/apollo.net/tree/master/Apollo/Logging/Spi](https://github.com/ctripcorp/apollo.net/tree/master/Apollo/Logging/Spi) # 四、客户端设计 ![client-architecture](https://github.com/apolloconfig/apollo/raw/master/doc/images/client-architecture.png) 上图简要描述了Apollo客户端的实现原理: 1. 客户端和服务端保持了一个长连接,从而能第一时间获得配置更新的推送。(通过Http Long Polling实现) 2. 客户端还会定时从Apollo配置中心服务端拉取应用的最新配置。 * 这是一个fallback机制,为了防止推送机制失效导致配置不更新 * 客户端定时拉取会上报本地版本,所以一般情况下,对于定时拉取的操作,服务端都会返回304 - Not Modified * 定时频率默认为每5分钟拉取一次,客户端也可以通过App.config设置`Apollo.RefreshInterval`来覆盖,单位为毫秒。 3. 客户端从Apollo配置中心服务端获取到应用的最新配置后,会保存在内存中 4. 客户端会把从服务端获取到的配置在本地文件系统缓存一份 * 在遇到服务不可用,或网络不通的时候,依然能从本地恢复配置 5. 应用程序可以从Apollo客户端获取最新的配置、订阅配置更新通知 # 五、本地开发模式 Apollo客户端还支持本地开发模式,这个主要用于当开发环境无法连接Apollo服务器的时候,比如在邮轮、飞机上做相关功能开发。 在本地开发模式下,Apollo只会从本地文件读取配置信息,不会从Apollo服务器读取配置。 可以通过下面的步骤开启Apollo本地开发模式。 ## 5.1 修改环境 修改C:\opt\settings\server.properties文件,设置env为Local: ```properties env=Local ``` ## 5.2 准备本地配置文件 在本地开发模式下,Apollo客户端会从本地读取文件,所以我们需要事先准备好配置文件。 ### 5.2.1 本地配置目录 本地配置目录位于:C:\opt\data\\{_appId_}\config-cache。 appId就是应用的appId,如100004458。 请确保该目录存在,且应用程序对该目录有读权限。 **【小技巧】** 推荐的方式是先在普通模式下使用Apollo,这样Apollo会自动创建该目录并在目录下生成配置文件。 ### 5.2.2 本地配置文件 本地配置文件需要按照一定的文件名格式放置于本地配置目录下,文件名格式如下: **_{appId}+{cluster}+{namespace}.json_** * appId就是应用自己的appId,如100004458 * cluster就是应用使用的集群,一般在本地模式下没有做过配置的话,就是default * namespace就是应用使用配置namespace,一般是application ![client-local-cache](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/apollo-net-config-cache.png) 文件内容以json格式存储,比如如果有两个key,一个是request.timeout,另一个是batch,那么文件内容就是如下格式: ```json { "request.timeout":"1000", "batch":"2000" } ``` ## 5.3 修改配置 在本地开发模式下,Apollo不会实时监测文件内容是否有变化,所以如果修改了配置,需要重启应用生效。 ================================================ FILE: docs/zh/client/golang-sdks-user-guide.md ================================================ ### Apollo Go 客户端 1 项目地址:[apolloconfig/agollo](https://github.com/apolloconfig/agollo) > 非常感谢[@zouyx](https://github.com/zouyx)提供Go Apollo客户端的支持 ### Apollo Go 客户端 2 项目地址:[philchia/agollo](https://github.com/philchia/agollo) > 非常感谢[@philchia](https://github.com/philchia)提供Go Apollo客户端的支持 ### Apollo Go 客户端 3 项目地址:[shima-park/agollo](https://github.com/shima-park/agollo) > 非常感谢[@shima-park](https://github.com/shima-park)提供Go Apollo客户端的支持 ### Apollo Go 客户端 4 项目地址:[go-microservices/php_conf_agent](https://github.com/go-microservices/php_conf_agent) > 非常感谢[@GanymedeNil](https://github.com/GanymedeNil)提供Go Apollo客户端的支持 ### Apollo Go 客户端 5 项目地址:[hyperjiang/lunar](https://github.com/hyperjiang/lunar) > 非常感谢[@hyperjiang](https://github.com/hyperjiang)提供Go Apollo客户端的支持 ### Apollo Go 客户端 6 项目地址:[tagconfig/tagconfig](https://github.com/tagconfig/tagconfig) > 非常感谢[@n0trace](https://github.com/n0trace)提供Go Apollo客户端的支持 ### Apollo Go 客户端 7 项目地址:[go-chassis/go-archaius](https://github.com/go-chassis/go-archaius/tree/master/examples/apollo) > 非常感谢[@tianxiaoliang](https://github.com/tianxiaoliang) 和 [@Shonminh](https://github.com/Shonminh)提供Go Apollo客户端的支持 ### Apollo Go 客户端 8 项目地址:[xhrg-product/apollo-client-golang](https://github.com/xhrg-product/apollo-client-golang) > 非常感谢[@xhrg](https://github.com/xhrg)提供Go Apollo客户端的支持 ### Apollo Go 客户端 9 项目地址:[xnzone/apollo-go](https://github.com/xnzone/apollo-go) > 非常感谢[@xnzone](https://github.com/xnzone)提供Go Apollo客户端的支持 ================================================ FILE: docs/zh/client/java-sdk-user-guide.md ================================================ >注意:本文档适用对象是Apollo系统的使用者,如果你是公司内Apollo系统的开发者/维护人员,建议先参考[Apollo开发指南](zh/contribution/apollo-development-guide)。 #   # 一、准备工作 ## 1.1 环境要求 * Java: 1.8+ * 如需运行在 Java 1.7 运行时环境,请使用 1.x 版本的 apollo 客户端,如 1.9.1 * Guava: 22.0+ * Apollo客户端默认会引用Guava 32,如果你的项目引用了其它版本,请确保版本号大于等于22.0 >注:对于Apollo客户端,如果有需要的话,可以做少量代码修改来降级到Java 1.6,详细信息可以参考[Issue 483](https://github.com/apolloconfig/apollo/issues/483) ## 1.2 必选设置 Apollo客户端依赖于`AppId`,`Apollo Meta Server`等环境信息来工作,所以请确保阅读下面的说明并且做正确的配置: ### 1.2.1 AppId AppId是应用的身份信息,是从服务端获取配置的一个重要信息。 有以下几种方式设置,按照优先级从高到低分别为: 1. System Property Apollo 0.7.0+支持通过System Property传入app.id信息,如 ```bash -Dapp.id=YOUR-APP-ID ``` 2. 操作系统的System Environment Apollo 1.4.0+支持通过操作系统的System Environment `APP_ID`来传入app.id信息,如 ```bash APP_ID=YOUR-APP-ID ``` 3. Spring Boot application.properties Apollo 1.0.0+支持通过Spring Boot的application.properties文件配置,如 ```properties app.id=YOUR-APP-ID ``` > 该配置方式不适用于多个war包部署在同一个tomcat的使用场景 4. app.properties 确保classpath:/META-INF/app.properties文件存在,并且其中内容形如: >app.id=YOUR-APP-ID 文件位置参考如下: ![app-id-location](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/app-id-location.png) > 注:app.id是用来标识应用身份的唯一id,格式为string。 ### 1.2.2 Apollo Meta Server Apollo支持应用在不同的环境有不同的配置,所以需要在运行提供给Apollo客户端当前环境的[Apollo Meta Server](zh/design/apollo-design?id=_133-meta-server)信息。默认情况下,meta server和config service是部署在同一个JVM进程,所以meta server的地址就是config service的地址。 为了实现meta server的高可用,推荐通过SLB(Software Load Balancer)做动态负载均衡。Meta server地址也可以填入IP,如`http://1.1.1.1:8080,http://2.2.2.2:8080`,不过生产环境还是建议使用域名(走slb),因为机器扩容、缩容等都可能导致IP列表的变化。 1.0.0版本开始支持以下方式配置apollo meta server信息,按照优先级从高到低分别为: 1. 通过Java System Property `apollo.meta` * 可以通过Java的System Property `apollo.meta`来指定 * 在Java程序启动脚本中,可以指定`-Dapollo.meta=http://config-service-url` * 如果是运行jar文件,需要注意格式是`java -Dapollo.meta=http://config-service-url -jar xxx.jar` * 也可以通过程序指定,如`System.setProperty("apollo.meta", "http://config-service-url");` 2. 通过Spring Boot的配置文件 * 可以在Spring Boot的`application.properties`或`bootstrap.properties`中指定`apollo.meta=http://config-service-url` > 该配置方式不适用于多个war包部署在同一个tomcat的使用场景 3. 通过操作系统的System Environment`APOLLO_META` * 可以通过操作系统的System Environment `APOLLO_META`来指定 * 注意key为全大写,且中间是`_`分隔 4. 通过`server.properties`配置文件 * 可以在`server.properties`配置文件中指定`apollo.meta=http://config-service-url` * 对于Mac/Linux,默认文件位置为`/opt/settings/server.properties` * 对于Windows,默认文件位置为`C:\opt\settings\server.properties` 5. 通过`app.properties`配置文件 * 可以在`classpath:/META-INF/app.properties`指定`apollo.meta=http://config-service-url` 6. 通过Java system property `${env}_meta` * 如果当前[env](#_1241-environment)是`dev`,那么用户可以配置`-Ddev_meta=http://config-service-url` * 使用该配置方式,那么就必须要正确配置Environment,详见[1.2.4.1 Environment](#_1241-environment) 7. 通过操作系统的System Environment `${ENV}_META` (1.2.0版本开始支持) * 如果当前[env](#_1241-environment)是`dev`,那么用户可以配置操作系统的System Environment `DEV_META=http://config-service-url` * 注意key为全大写 * 使用该配置方式,那么就必须要正确配置Environment,详见[1.2.4.1 Environment](#_1241-environment) 8. 通过`apollo-env.properties`文件 * 用户也可以创建一个`apollo-env.properties`,放在程序的classpath下,或者放在spring boot应用的config目录下 * 使用该配置方式,那么就必须要正确配置Environment,详见[1.2.4.1 Environment](#_1241-environment) * 文件内容形如: ```properties dev.meta=http://1.1.1.1:8080 fat.meta=http://apollo.fat.xxx.com uat.meta=http://apollo.uat.xxx.com pro.meta=http://apollo.xxx.com ``` > 如果通过以上各种手段都无法获取到Meta Server地址,Apollo最终会fallback到`http://apollo.meta`作为Meta Server地址 #### 1.2.2.1 自定义Apollo Meta Server地址定位逻辑 在1.0.0版本中,Apollo提供了[MetaServerProvider SPI](https://github.com/apolloconfig/apollo-java/blob/main/apollo-core/src/main/java/com/ctrip/framework/apollo/core/spi/MetaServerProvider.java),用户可以注入自己的MetaServerProvider来自定义Meta Server地址定位逻辑。 由于我们使用典型的[Java Service Loader模式](https://docs.oracle.com/javase/7/docs/api/java/util/ServiceLoader.html),所以实现起来还是比较简单的。 有一点需要注意的是,apollo会在运行时按照顺序遍历所有的MetaServerProvider,直到某一个MetaServerProvider提供了一个非空的Meta Server地址,因此用户需要格外注意自定义MetaServerProvider的Order。规则是较小的Order具有较高的优先级,因此Order=0的MetaServerProvider会排在Order=1的MetaServerProvider的前面。 **如果你的公司有很多应用需要接入Apollo,建议封装一个jar包,然后提供自定义的Apollo Meta Server定位逻辑,从而可以让接入Apollo的应用零配置使用。比如自己写一个`xx-company-apollo-client`,该jar包依赖`apollo-client`,在该jar包中通过spi方式定义自定义的MetaServerProvider实现,然后应用直接依赖`xx-company-apollo-client`即可。** MetaServerProvider的实现可以参考[LegacyMetaServerProvider](https://github.com/apolloconfig/apollo-java/blob/main/apollo-core/src/main/java/com/ctrip/framework/apollo/core/internals/LegacyMetaServerProvider.java)和[DefaultMetaServerProvider](https://github.com/apolloconfig/apollo-java/blob/main/apollo-client/src/main/java/com/ctrip/framework/apollo/internals/DefaultMetaServerProvider.java)。 #### 1.2.2.2 跳过Apollo Meta Server服务发现 > 适用于apollo-client 0.11.0及以上版本 一般情况下都建议使用Apollo的Meta Server机制来实现Config Service的服务发现,从而可以实现Config Service的高可用。不过apollo-client也支持跳过Meta Server服务发现,主要用于以下场景: 1. Config Service部署在公有云上,注册到Meta Server的是内网地址,本地开发环境无法直接连接 * 如果通过公网 SLB 对外暴露 Config Service的话,记得要设置 IP 白名单,避免数据泄露 2. Config Service部署在docker环境中,注册到Meta Server的是docker内网地址,本地开发环境无法直接连接 3. Config Service部署在kubernetes中,希望使用kubernetes自带的服务发现能力(Service) 针对以上场景,可以通过直接指定Config Service地址的方式来跳过Meta Server服务发现,按照优先级从高到低分别为: 1. 通过Java System Property `apollo.config-service`(1.9.0+) 或者 `apollo.configService`(1.9.0之前) * 可以通过Java的System Property `apollo.config-service`(1.9.0+) 或者 `apollo.configService`(1.9.0之前)来指定 * 在Java程序启动脚本中,可以指定`-Dapollo.config-service=http://config-service-url:port` * 如果是运行jar文件,需要注意格式是`java -Dapollo.configService=http://config-service-url:port -jar xxx.jar` * 也可以通过程序指定,如`System.setProperty("apollo.config-service", "http://config-service-url:port");` 2. 通过操作系统的System Environment `APOLLO_CONFIG_SERVICE`(1.9.0+) 或者 `APOLLO_CONFIGSERVICE`(1.9.0之前) * 可以通过操作系统的System Environment `APOLLO_CONFIG_SERVICE`(1.9.0+) 或者 `APOLLO_CONFIGSERVICE`(1.9.0之前)来指定 * 注意key为全大写,且中间是`_`分隔 4. 通过`server.properties`配置文件 * 可以在`server.properties`配置文件中指定`apollo.config-service=http://config-service-url:port`(1.9.0+) 或者 `apollo.configService=http://config-service-url:port`(1.9.0之前) * 对于Mac/Linux,默认文件位置为`/opt/settings/server.properties` * 对于Windows,默认文件位置为`C:\opt\settings\server.properties` ### 1.2.3 本地缓存路径 Apollo客户端会把从服务端获取到的配置在本地文件系统缓存一份,用于在遇到服务不可用,或网络不通的时候,依然能从本地恢复配置,不影响应用正常运行。 本地缓存路径默认位于以下路径,所以请确保`/opt/data`或`C:\opt\data\`目录存在,且应用有读写权限。 * **Mac/Linux**: /opt/data/{_appId_}/config-cache * **Windows**: C:\opt\data\\{_appId_}\config-cache 本地配置文件会以下面的文件名格式放置于本地缓存路径下: **_{appId}+{cluster}+{namespace}.properties_** * appId就是应用自己的appId,如100004458 * cluster就是应用使用的集群,一般在本地模式下没有做过配置的话,就是default * namespace就是应用使用的配置namespace,一般是application ![client-local-cache](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/client-local-cache.png) 文件内容以properties格式存储,比如如果有两个key,一个是request.timeout,另一个是batch,那么文件内容就是如下格式: ```properties request.timeout=2000 batch=2000 ``` > 注:如果部署在Kubernetes环境中,您还可以启用configMap缓存来进一步提高可用性 #### 1.2.3.1 自定义缓存路径 1.0.0版本开始支持以下方式自定义缓存路径,按照优先级从高到低分别为: 1. 通过Java System Property `apollo.cache-dir`(1.9.0+) 或者 `apollo.cacheDir`(1.9.0之前) * 可以通过Java的System Property `apollo.cache-dir`(1.9.0+) 或者 `apollo.cacheDir`(1.9.0之前)来指定 * 在Java程序启动脚本中,可以指定`-Dapollo.cache-dir=/opt/data/some-cache-dir`(1.9.0+) 或者 `apollo.cacheDir=/opt/data/some-cache-dir`(1.9.0之前) * 如果是运行jar文件,需要注意格式是`java -Dapollo.cache-dir=/opt/data/some-cache-dir -jar xxx.jar`(1.9.0+) 或者 `java -Dapollo.cacheDir=/opt/data/some-cache-dir -jar xxx.jar`(1.9.0之前) * 也可以通过程序指定,如`System.setProperty("apollo.cache-dir", "/opt/data/some-cache-dir");`(1.9.0+) 或者 `System.setProperty("apollo.cacheDir", "/opt/data/some-cache-dir");`(1.9.0之前) 2. 通过Spring Boot的配置文件 * 可以在Spring Boot的`application.properties`或`bootstrap.properties`中指定`apollo.cache-dir=/opt/data/some-cache-dir`(1.9.0+) 或者 `apollo.cacheDir=/opt/data/some-cache-dir`(1.9.0之前) 3. 通过操作系统的System Environment`APOLLO_CACHE_DIR`(1.9.0+) 或者 `APOLLO_CACHEDIR`(1.9.0之前) * 可以通过操作系统的System Environment `APOLLO_CACHE_DIR`(1.9.0+) 或者 `APOLLO_CACHEDIR`(1.9.0之前)来指定 * 注意key为全大写,且中间是`_`分隔 4. 通过`server.properties`配置文件 * 可以在`server.properties`配置文件中指定`apollo.cache-dir=/opt/data/some-cache-dir`(1.9.0+) 或者 `apollo.cacheDir=/opt/data/some-cache-dir`(1.9.0之前) * 对于Mac/Linux,默认文件位置为`/opt/settings/server.properties` * 对于Windows,默认文件位置为`C:\opt\settings\server.properties` > 注:本地缓存路径也可用于容灾目录,如果应用在所有config service都挂掉的情况下需要扩容,那么也可以先把配置从已有机器上的缓存路径复制到新机器上的相同缓存路径 ### 1.2.4 可选设置 #### 1.2.4.1 Environment Environment可以通过以下3种方式的任意一个配置: 1. 通过Java System Property * 可以通过Java的System Property `env`来指定环境 * 在Java程序启动脚本中,可以指定`-Denv=YOUR-ENVIRONMENT` * 如果是运行jar文件,需要注意格式是`java -Denv=YOUR-ENVIRONMENT -jar xxx.jar` * 注意key为全小写 2. 通过操作系统的System Environment * 还可以通过操作系统的System Environment `ENV`来指定 * 注意key为全大写 3. 通过配置文件 * 最后一个推荐的方式是通过配置文件来指定`env=YOUR-ENVIRONMENT` * 对于Mac/Linux,默认文件位置为`/opt/settings/server.properties` * 对于Windows,默认文件位置为`C:\opt\settings\server.properties` 文件内容形如: ```properties env=DEV ``` 目前,`env`支持以下几个值(大小写不敏感): * DEV * Development environment * FAT * Feature Acceptance Test environment * UAT * User Acceptance Test environment * PRO * Production environment 更多环境定义,可以参考[Env.java](https://github.com/apolloconfig/apollo/blob/master/apollo-core/src/main/java/com/ctrip/framework/apollo/core/enums/Env.java) #### 1.2.4.2 Cluster(集群) Apollo支持配置按照集群划分,也就是说对于一个appId和一个环境,对不同的集群可以有不同的配置。 1.0.0版本开始支持以下方式集群,按照优先级从高到低分别为: 1. 通过Java System Property `apollo.cluster` * 可以通过Java的System Property `apollo.cluster`来指定 * 在Java程序启动脚本中,可以指定`-Dapollo.cluster=SomeCluster` * 如果是运行jar文件,需要注意格式是`java -Dapollo.cluster=SomeCluster -jar xxx.jar` * 也可以通过程序指定,如`System.setProperty("apollo.cluster", "SomeCluster");` 2. 通过Spring Boot的配置文件 * 可以在Spring Boot的`application.properties`或`bootstrap.properties`中指定`apollo.cluster=SomeCluster` 3. 通过Java System Property * 可以通过Java的System Property `idc`来指定环境 * 在Java程序启动脚本中,可以指定`-Didc=xxx` * 如果是运行jar文件,需要注意格式是`java -Didc=xxx -jar xxx.jar` * 注意key为全小写 4. 通过操作系统的System Environment * 还可以通过操作系统的System Environment `IDC`来指定 * 注意key为全大写 5. 通过`server.properties`配置文件 * 可以在`server.properties`配置文件中指定`idc=xxx` * 对于Mac/Linux,默认文件位置为`/opt/settings/server.properties` * 对于Windows,默认文件位置为`C:\opt\settings\server.properties` **Cluster Precedence**(集群顺序) 1. 如果`apollo.cluster`和`idc`同时指定: * 我们会首先尝试从`apollo.cluster`指定的集群加载配置 * 如果没找到任何配置,会尝试从`idc`指定的集群加载配置 * 如果还是没找到,会从默认的集群(`default`)加载 2. 如果只指定了`apollo.cluster`: * 我们会首先尝试从`apollo.cluster`指定的集群加载配置 * 如果没找到,会从默认的集群(`default`)加载 3. 如果只指定了`idc`: * 我们会首先尝试从`idc`指定的集群加载配置 * 如果没找到,会从默认的集群(`default`)加载 4. 如果`apollo.cluster`和`idc`都没有指定: * 我们会从默认的集群(`default`)加载配置 #### 1.2.4.3 设置内存中的配置项是否保持和页面上的顺序一致 > 适用于1.6.0及以上版本 默认情况下,apollo client内存中的配置存放在Properties中(底下是Hashtable),不会刻意保持和页面上看到的顺序一致,对绝大部分的场景是没有影响的。不过有些场景会强依赖配置项的顺序(如spring cloud zuul的路由规则),针对这种情况,可以开启OrderedProperties特性来使得内存中的配置顺序和页面上看到的一致。 配置方式按照优先级从高到低分别为: 1. 通过Java System Property `apollo.property.order.enable` * 可以通过Java的System Property `apollo.property.order.enable`来指定 * 在Java程序启动脚本中,可以指定`-Dapollo.property.order.enable=true` * 如果是运行jar文件,需要注意格式是`java -Dapollo.property.order.enable=true -jar xxx.jar` * 也可以通过程序指定,如`System.setProperty("apollo.property.order.enable", "true");` 2. 通过Spring Boot的配置文件 * 可以在Spring Boot的`application.properties`或`bootstrap.properties`中指定`apollo.property.order.enable=true` 3. 通过`app.properties`配置文件 * 可以在`classpath:/META-INF/app.properties`指定`apollo.property.order.enable=true` #### 1.2.4.4 配置访问密钥 > 适用于1.6.0及以上版本 Apollo从1.6.0版本开始增加访问密钥机制,从而只有经过身份验证的客户端才能访问敏感配置。如果应用开启了访问密钥,客户端需要配置密钥,否则无法获取配置。 配置方式按照优先级从高到低分别为: 1. 通过Java System Property `apollo.access-key.secret`(1.9.0+) 或者 `apollo.accesskey.secret`(1.9.0之前) * 可以通过Java的System Property `apollo.access-key.secret`(1.9.0+) 或者 `apollo.accesskey.secret`(1.9.0之前)来指定 * 在Java程序启动脚本中,可以指定`-Dapollo.access-key.secret=1cf998c4e2ad4704b45a98a509d15719`(1.9.0+) 或者 `-Dapollo.accesskey.secret=1cf998c4e2ad4704b45a98a509d15719`(1.9.0之前) * 如果是运行jar文件,需要注意格式是`java -Dapollo.access-key.secret=1cf998c4e2ad4704b45a98a509d15719 -jar xxx.jar`(1.9.0+) 或者 `java -Dapollo.accesskey.secret=1cf998c4e2ad4704b45a98a509d15719 -jar xxx.jar`(1.9.0之前) * 也可以通过程序指定,如`System.setProperty("apollo.access-key.secret", "1cf998c4e2ad4704b45a98a509d15719");`(1.9.0+) 或者 `System.setProperty("apollo.accesskey.secret", "1cf998c4e2ad4704b45a98a509d15719");`(1.9.0之前) 2. 通过Spring Boot的配置文件 * 可以在Spring Boot的`application.properties`或`bootstrap.properties`中指定`apollo.access-key.secret=1cf998c4e2ad4704b45a98a509d15719`(1.9.0+) 或者 `apollo.accesskey.secret=1cf998c4e2ad4704b45a98a509d15719`(1.9.0之前) 3. 通过操作系统的System Environment * 还可以通过操作系统的System Environment `APOLLO_ACCESS_KEY_SECRET`(1.9.0+) 或者 `APOLLO_ACCESSKEY_SECRET`(1.9.0之前)来指定 * 注意key为全大写 4. 通过`app.properties`配置文件 * 可以在`classpath:/META-INF/app.properties`指定`apollo.access-key.secret=1cf998c4e2ad4704b45a98a509d15719`(1.9.0+) 或者 `apollo.accesskey.secret=1cf998c4e2ad4704b45a98a509d15719`(1.9.0之前) #### 1.2.4.5 自定义server.properties路径 > 适用于1.8.0及以上版本 1.8.0版本开始支持以下方式自定义server.properties路径,按照优先级从高到低分别为: 1. 通过Java System Property `apollo.path.server.properties` * 可以通过Java的System Property `apollo.path.server.properties`来指定 * 在Java程序启动脚本中,可以指定`-Dapollo.path.server.properties=/some-dir/some-file.properties` * 如果是运行jar文件,需要注意格式是`java -Dapollo.path.server.properties=/some-dir/some-file.properties -jar xxx.jar` * 也可以通过程序指定,如`System.setProperty("apollo.path.server.properties", "/some-dir/some-file.properties");` 2. 通过操作系统的System Environment`APOLLO_PATH_SERVER_PROPERTIES` * 可以通过操作系统的System Environment `APOLLO_PATH_SERVER_PROPERTIES`来指定 * 注意key为全大写,且中间是`_`分隔 #### 1.2.4.6 开启`propertyNames`缓存,在大量配置场景下可以显著改善启动速度 > 适用于1.9.0及以上版本 在使用`@ConfigurationProperties`和存在大量配置项场景下,Spring容器的启动速度会变慢。通过开启该配置可以显著提升启动速度,当配置发生变化时缓存会自动清理,默认为`false`。详见:[issue 3800](https://github.com/apolloconfig/apollo/issues/3800) 配置方式按照优先级从高到低依次为: 1. 通过Java System Property `apollo.property.names.cache.enable` * 可以通过Java的System Property `apollo.property.names.cache.enable`来指定 * 在Java程序启动脚本中,可以指定`-Dapollo.property.names.cache.enable=true` * 如果是运行jar文件,需要注意格式是`java -Dapollo.property.names.cache.enable=true -jar xxx.jar` * 也可以通过程序指定,如`System.setProperty("apollo.property.names.cache.enable", "true");` 2. 通过系统环境变量 * 在启动程序前配置环境变量`APOLLO_PROPERTY_NAMES_CACHE_ENABLE=true`来指定 * 注意key为全大写,且中间是`_`分隔 3. 通过Spring Boot的配置文件 * 可以在Spring Boot的`application.properties`或`bootstrap.properties`中指定`apollo.property.names.cache.enable=true` 4. 通过`app.properties`配置文件 * 可以在`classpath:/META-INF/app.properties`指定`apollo.property.names.cache.enable=true` #### 1.2.4.7 ApolloLabel ApolloLabel是应用的标签信息,是从服务端获取配置的一个重要信息,用于灰度规则的配置。 有以下几种方式设置,按照优先级从高到低分别为: 1. System Property Apollo 2.0.0+支持通过System Property传入apollo.label信息,如 ```bash -Dapollo.label=YOUR-APOLLO-LABEL ``` 2. 操作系统的System Environment Apollo 2.0.0+支持通过操作系统的System Environment `APP_LABEL`来传入apollo.label信息,如 ```bash APOLLO_LABEL=YOUR-APOLLO-LABEL ``` 3. Spring Boot application.properties Apollo 2.0.0+支持通过Spring Boot的application.properties文件配置,如 ```properties apollo.label=YOUR-APOLLO-LABEL ``` > 该配置方式不适用于多个war包部署在同一个tomcat的使用场景 4. app.properties 确保classpath:/META-INF/app.properties文件存在,并且其中内容形如: >apollo.label=YOUR-APOLLO-LABEL 文件位置参考如下: ![app-id-location](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/app-id-location.png) > 注:apollo.label是用来标识应用身份的标签,格式为string。 #### 1.2.4.8 覆盖系统属性 > 适用于2.1.0及以上版本 `apollo.override-system-properties` 标识Apollo的远程属性是否应该覆盖Java的系统属性。默认为 true。 配置方式按照优先级从高到低分别为: 1. 通过Java System Property `apollo.override-system-properties` * 可以通过Java的System Property `apollo.override-system-properties`来指定 * 在Java程序启动脚本中,可以指定`-Dapollo.override-system-properties=true` * 如果是运行jar文件,需要注意格式是`java -Dapollo.override-system-properties=true -jar xxx.jar` * 也可以通过程序指定,如`System.setProperty("apollo.override-system-properties", "true");` 2. 通过Spring Boot的配置文件 * 可以在Spring Boot的`application.properties`或`bootstrap.properties`中指定`apollo.override-system-properties=true` 3. 通过`app.properties`配置文件 * 可以在`classpath:/META-INF/app.properties`指定`apollo.override-system-properties=true` #### 1.2.4.9 开启客户端监控 > 适用于2.4.0及以上版本 在开启下方配置后, 可使用 `ConfigService.getConfigMonitor()` 获取客户端监控信息,以及自动上报 ```properties #1.是否启动Monitor机制, 即ConfigMonitor是否启用,默认false apollo.client.monitor.enabled = true #2.是否将Monitor数据以Jmx形式暴露,开启后可以通过J-console等工具查看相关信息,默认为false apollo.client.monitor.jmx.enabled = true #3.Monitor存储异常信息的最大数量,默认为25,符合先进先出原则 apollo.client.monitor.exception-queue-size= 30 #4.指定导出指标数据使用的对应监控系统的Exporter类型,如引入apollo-plugin-client-prometheus则可填写prometheus进行启用, # 取决于SPI MetricsExporter的实现 apollo.client.monitor.external.type= prometheus #5.指定Exporter从Monitor中导出状态信息转为指标数据的频率,默认为10秒导出一次, apollo.client.monitor.external.export-period= 20 ``` #### 1.2.4.10 ConfigMap缓存设置 > 适用于2.4.0及以上版本 在2.4.0版本开始,客户端在Kubernetes环境下的可用性得到了加强,开启configMap缓存后,客户端会将从服务端拉取到的配置信息在configMap中缓存一份,在服务不可用,或网络不通,且本地缓存文件丢失的情况下,依然能从configMap恢复配置。以下是相关配置 `apollo.cache.kubernetes.enable`:是否开启configMap缓存机制,默认false `apollo.cache.kubernetes.namespace`:将使用的configMap所在的namespace(Kubernetes中的namespace),默认值为"default" 配置信息会以下面的对应关系放置于指定的configmap中: namespace:使用指定的值,若未指定默认为"default" configMapName: apollo-configcache-{appId} key:{cluster}___{namespace} value:内容为对应的配置信息的json格式字符串 > appId是应用自己的appId,如100004458 > cluster是应用使用的集群,一般在本地模式下没有做过配置的话,是default > namespace就是应用使用的配置namespace。 如果namespace中出现‘_’ , 将会在拼接key时被转义为‘__’ > 由于此功能为拓展功能,所以对于client-java的依赖设为了optional。需用户自行导入匹配的版本 > 由于需要对configmap进行读写操作,所以客户端所在pod必须有相应读写权限,具体配置方法可参考下文 如何授权一个Pod的Service Account具有对ConfigMap的读写权限: 1. 创建Service Account: 如果还没有Service Account,你需要创建一个。 ```yaml apiVersion: v1 kind: ServiceAccount metadata: name: my-service-account namespace: default ``` 2. 创建Role或ClusterRole: 定义一个Role或ClusterRole,授予对特定ConfigMap的读写权限。如果ConfigMap是跨多个Namespace使用的,应该使用ClusterRole。 ```yaml apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: namespace: default name: configmap-role rules: - apiGroups: [""] resources: ["configmaps"] verbs: ["get", "list", "watch", "create", "update", "delete"] ``` 3. 绑定Service Account到Role或ClusterRole: 使用RoleBinding或ClusterRoleBinding将Service Account绑定到上面创建的Role或ClusterRole。 ```yaml apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: configmap-reader-binding namespace: default subjects: - kind: ServiceAccount name: my-service-account namespace: default roleRef: kind: Role name: configmap-role apiGroup: rbac.authorization.k8s.io ``` 4. 在Pod配置中指定Service Account: 确保Pod的配置中使用了上面创建的Service Account。 ```yaml apiVersion: v1 kind: Pod metadata: name: my-pod namespace: default spec: serviceAccountName: my-service-account containers: - name: my-container image: my-image ``` 5. 应用配置: 使用kubectl命令行工具应用这些配置。 ```yaml kubectl apply -f service-account.yaml kubectl apply -f role.yaml kubectl apply -f role-binding.yaml kubectl apply -f pod.yaml ``` 这些步骤使Pod中的Service Account具有对指定ConfigMap的读写权限。 如果ConfigMap是跨Namespace的,使用ClusterRole和ClusterRoleBinding代替Role和RoleBinding,并确保在所有需要访问ConfigMap的Namespace中应用这些配置。 # 二、Maven Dependency Apollo的客户端jar包已经上传到中央仓库,应用在实际使用时只需要按照如下方式引入即可。 ```xml com.ctrip.framework.apollo apollo-client 1.7.0 ``` # 三、客户端用法 Apollo支持API方式和Spring整合方式,该怎么选择用哪一种方式? * API方式灵活,功能完备,配置值实时更新(热发布),支持所有Java环境。 * Spring方式接入简单,结合Spring有N种酷炫的玩法,如 * Placeholder方式: * 代码中直接使用,如:`@Value("${someKeyFromApollo:someDefaultValue}")` * 配置文件中使用替换placeholder,如:`spring.datasource.url: ${someKeyFromApollo:someDefaultValue}` * 直接托管spring的配置,如在apollo中直接配置`spring.datasource.url=jdbc:mysql://localhost:3306/somedb?characterEncoding=utf8` * Spring boot的[@ConfigurationProperties](http://docs.spring.io/spring-boot/docs/current/api/org/springframework/boot/context/properties/ConfigurationProperties.html)方式 * 从v0.10.0开始的版本支持placeholder在运行时自动更新,具体参见[PR #972](https://github.com/apolloconfig/apollo/pull/972)。(v0.10.0之前的版本在配置变化后不会重新注入,需要重启才会更新,如果需要配置值实时更新,可以参考后续[3.2.2 Spring Placeholder的使用](#_322-spring-placeholder的使用)的说明) * Spring方式也可以结合API方式使用,如注入Apollo的Config对象,就可以照常通过API方式获取配置了: ```java @ApolloConfig private Config config; //inject config for namespace application ``` * 更多有意思的实际使用场景和示例代码,请参考[apollo-use-cases](https://github.com/ctripcorp/apollo-use-cases) ## 3.1 API使用方式 API方式是最简单、高效使用Apollo配置的方式,不依赖Spring框架即可使用。 ### 3.1.1 获取默认namespace的配置(application) ```java Config config = ConfigService.getAppConfig(); //config instance is singleton for each namespace and is never null String someKey = "someKeyFromDefaultNamespace"; String someDefaultValue = "someDefaultValueForTheKey"; String value = config.getProperty(someKey, someDefaultValue); ``` 通过上述的**config.getProperty**可以获取到someKey对应的实时最新的配置值。 另外,配置值从内存中获取,所以不需要应用自己做缓存。 ### 3.1.2 监听配置变化事件 监听配置变化事件只在应用真的关心配置变化,需要在配置变化时得到通知时使用,比如:数据库连接串变化后需要重建连接等。 如果只是希望每次都取到最新的配置的话,只需要按照上面的例子,调用**config.getProperty**即可。 ```java Config config = ConfigService.getAppConfig(); //config instance is singleton for each namespace and is never null config.addChangeListener(new ConfigChangeListener() { @Override public void onChange(ConfigChangeEvent changeEvent) { System.out.println("Changes for namespace " + changeEvent.getNamespace()); for (String key : changeEvent.changedKeys()) { ConfigChange change = changeEvent.getChange(key); System.out.println(String.format("Found change - key: %s, oldValue: %s, newValue: %s, changeType: %s", change.getPropertyName(), change.getOldValue(), change.getNewValue(), change.getChangeType())); } } }); ``` ### 3.1.3 获取公共Namespace的配置 ```java String somePublicNamespace = "CAT"; Config config = ConfigService.getConfig(somePublicNamespace); //config instance is singleton for each namespace and is never null String someKey = "someKeyFromPublicNamespace"; String someDefaultValue = "someDefaultValueForTheKey"; String value = config.getProperty(someKey, someDefaultValue); ``` ### 3.1.4 获取非properties格式namespace的配置 #### 3.1.4.1 yaml/yml格式的namespace apollo-client 1.3.0版本开始对yaml/yml做了更好的支持,使用起来和properties格式一致。 ```java Config config = ConfigService.getConfig("application.yml"); String someKey = "someKeyFromYmlNamespace"; String someDefaultValue = "someDefaultValueForTheKey"; String value = config.getProperty(someKey, someDefaultValue); ``` #### 3.1.4.2 非yaml/yml格式的namespace 获取时需要使用`ConfigService.getConfigFile`接口并指定Format,如`ConfigFileFormat.XML`。 ```java String someNamespace = "test"; ConfigFile configFile = ConfigService.getConfigFile("test", ConfigFileFormat.XML); String content = configFile.getContent(); ``` ### 3.1.5 读取多AppId对应namespace的配置 指定对应的AppId和namespace来获取Config,再获取属性 ```java String someAppId = "Animal"; String somePublicNamespace = "CAT"; Config config = ConfigService.getConfig(someAppId, somePublicNamespace); String someKey = "someKeyFromPublicNamespace"; String someDefaultValue = "someDefaultValueForTheKey"; String value = config.getProperty(someKey, someDefaultValue); ``` ### 3.1.6 获取客户端监控指标 > 适用于2.4.0及以上版本 apollo-client在2.4.0版本里大幅增强了可观测性,提供了ConfigMonitor-API以及JMX,Prometheus的指标导出方式,相关启用配置详见 [1.2.4.9 开启客户端监控](#_1249-开启客户端监控) #### 3.1.6.1 通过ConfigMonitor获取监控数据 ```java ConfigMonitor configMonitor = ConfigService.getConfigMonitor(); //错误相关监控API ApolloClientExceptionMonitorApi exceptionMonitorApi = configMonitor.getExceptionMonitorApi(); List apolloConfigExceptionList = exceptionMonitorApi.getApolloConfigExceptionList(); //命名空间相关监控API ApolloClientNamespaceMonitorApi namespaceMonitorApi = configMonitor.getNamespaceMonitorApi(); List namespace404 = namespaceMonitorApi.getNotFoundNamespaces(); //启动参数相关监控API ApolloClientBootstrapArgsMonitorApi runningParamsMonitorApi = configMonitor.getBootstrapArgsMonitorApi(); String bootstrapNamespaces = runningParamsMonitorApi.getBootstrapNamespaces(); //线程池相关监控API ApolloClientThreadPoolMonitorApi threadPoolMonitorApi = configMonitor.getThreadPoolMonitorApi(); ApolloThreadPoolInfo remoteConfigRepositoryThreadPoolInfo = threadPoolMonitorApi.getRemoteConfigRepositoryThreadPoolInfo(); ``` #### 3.1.6.2 以JMX形式暴露状态信息 启用相关配置 ```properties apollo.client.monitor.enabled = true apollo.client.monitor.jmx.enabled = true ``` 启动应用后,开启J-console或类似工具即可查看,这里用J-console做例子 ![showing Apollo client monitoring metrics in JMX](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/apollo-client-monitor-jmx.jpg) #### 3.1.6.3 客户端导出指标上报到外部监控系统 用户可以根据需求自定义接入Prometheus等监控系统,客户端提供了SPI,详见 [7.3 指标输出到自定义监控系统](#_73-指标输出到自定义监控系统)。 *相关指标数据表格* **Namespace Metrics** 指标对应API : ApolloClientNamespaceMonitorApi | 指标名称 | 标签 | 对应Monitor-API | | --------------------------------------------------- | --------- | -------------------------------------------- | | apollo_client_namespace_usage_total | namespace | namespaceMetrics.getUsageCount() | | apollo_client_namespace_item_num | namespace | namespaceMetrics.getFirstLoadTimeSpendInMs() | | apollo_client_namespace_not_found | | namespaceMonitorApi.getNotFoundNamespaces() | | apollo_client_namespace_timeout | | namespaceMonitorApi.getTimeoutNamespaces() | | apollo_client_namespace_first_load_time_spend_in_ms | namespace | namespaceMetrics.getLatestUpdateTime | **Thread Pool Metrics** 指标对应API:ApolloClientThreadPoolMonitorApi | 指标名称 | 标签 | 对应Monitor-API | | -------------------------------------------------- | ---------------- |--------------------------------------------| | apollo_client_thread_pool_pool_size | thread_pool_name | threadPoolInfo.getPoolSize() | | apollo_client_thread_pool_maximum_pool_size | thread_pool_name | threadPoolInfo.getMaximumPoolSize() | | apollo_client_thread_pool_largest_pool_size | thread_pool_name | threadPoolInfo.getLargestPoolSize() | | apollo_client_thread_pool_completed_task_count | thread_pool_name | threadPoolInfo.getCompletedTaskCount() | | apollo_client_thread_pool_queue_remaining_capacity | thread_pool_name | threadPoolInfo.getQueueRemainingCapacity() | | apollo_client_thread_pool_total_task_count | thread_pool_name | threadPoolInfo.getTotalTaskCount() | | apollo_client_thread_pool_active_task_count | thread_pool_name | threadPoolInfo.getActiveTaskCount() | | apollo_client_thread_pool_core_pool_size | thread_pool_name | threadPoolInfo.getCorePoolSize() | | apollo_client_thread_pool_queue_size | thread_pool_name | threadPoolInfo.getQueueSize() | **Exception Metrics** 指标对应API:ApolloClientExceptionMonitorApi | 指标名称 | 标签 | | --------------------------------- | -------------------------------------------------- | | apollo_client_exception_num_total | exceptionMonitorApi.getExceptionCountFromStartup() | ## 3.2 Spring整合方式 ### 3.2.1 配置 Apollo也支持和Spring整合(Spring 3.1.1+),只需要做一些简单的配置就可以了。 Apollo目前既支持比较传统的`基于XML`的配置,也支持目前比较流行的`基于Java(推荐)`的配置。 如果是Spring Boot环境,建议参照[3.2.1.3 Spring Boot集成方式(推荐)](#_3213-spring-boot集成方式(推荐))配置。 需要注意的是,如果之前有使用`org.springframework.beans.factory.config.PropertyPlaceholderConfigurer`的,请替换成`org.springframework.context.support.PropertySourcesPlaceholderConfigurer`。Spring 3.1以后就不建议使用PropertyPlaceholderConfigurer了,要改用PropertySourcesPlaceholderConfigurer。 如果之前有使用``,请注意xml中引入的`spring-context.xsd`版本需要是3.1以上(一般只要没有指定版本会自动升级的),建议使用不带版本号的形式引入,如:`http://www.springframework.org/schema/context/spring-context.xsd` > 注1:yaml/yml格式的namespace从1.3.0版本开始支持和Spring整合,注入时需要填写带后缀的完整名字,比如application.yml > 注2:非properties、非yaml/yml格式(如xml,json等)的namespace暂不支持和Spring整合。 #### 3.2.1.1 基于XML的配置 >注:需要把apollo相关的xml namespace加到配置文件头上,不然会报xml语法错误。 1.注入默认namespace的配置到Spring中 ```xml ``` 2.注入多个namespace的配置到Spring中 ```xml ``` 3.注入多个namespace,并且指定顺序 Spring的配置是有顺序的,如果多个property source都有同一个key,那么最终是顺序在前的配置生效。 如果不指定order,那么默认是最低优先级。 ```xml ``` #### 3.2.1.2 基于Java的配置(推荐) 相对于基于XML的配置,基于Java的配置是目前比较流行的方式。 注意`@EnableApolloConfig`要和`@Configuration`一起使用,不然不会生效。 1.注入默认namespace的配置到Spring中 ```java //这个是最简单的配置形式,一般应用用这种形式就可以了,用来指示Apollo注入application namespace的配置到Spring环境中 @Configuration @EnableApolloConfig public class AppConfig { @Bean public TestJavaConfigBean javaConfigBean() { return new TestJavaConfigBean(); } } ``` 2.注入多个namespace的配置到Spring中 ```java @Configuration @EnableApolloConfig public class SomeAppConfig { @Bean public TestJavaConfigBean javaConfigBean() { return new TestJavaConfigBean(); } } //这个是稍微复杂一些的配置形式,指示Apollo注入FX.apollo和application.yml namespace的配置到Spring环境中 @Configuration @EnableApolloConfig({"FX.apollo", "application.yml"}) public class AnotherAppConfig {} ``` 3.注入多个namespace,并且指定顺序 ```java //这个是最复杂的配置形式,指示Apollo注入FX.apollo和application.yml namespace的配置到Spring环境中,并且顺序在application前面 @Configuration @EnableApolloConfig(order = 2) public class SomeAppConfig { @Bean public TestJavaConfigBean javaConfigBean() { return new TestJavaConfigBean(); } } @Configuration @EnableApolloConfig(value = {"FX.apollo", "application.yml"}, order = 1) public class AnotherAppConfig {} ``` 4.多appId的支持(新增于2.4.0版本) ```java // 新增支持了多appId和对应namespace的加载,注意使用多appId的情况下,key相同的情况,只会取优先加载appId的那一个key @Configuration @EnableApolloConfig(value = {"FX.apollo", "application.yml"}, multipleConfigs = {@MultipleConfig(appid = "ORDER_SERVICE", namespaces = {"ORDER.apollo"})} ) public class SomeAppConfig {} ``` #### 3.2.1.3 Spring Boot集成方式(推荐) Spring Boot除了支持上述两种集成方式以外,还支持通过application.properties/bootstrap.properties来配置,该方式能使配置在更早的阶段注入,比如使用`@ConditionalOnProperty`的场景或者是有一些spring-boot-starter在启动阶段就需要读取配置做一些事情(如[dubbo-spring-boot-project](https://github.com/apache/incubator-dubbo-spring-boot-project)),所以对于Spring Boot环境建议通过以下方式来接入Apollo(需要0.10.0及以上版本)。 使用方式很简单,只需要在application.properties/bootstrap.properties中按照如下样例配置即可。 1. 注入默认`application` namespace的配置示例 ```properties # will inject 'application' namespace in bootstrap phase apollo.bootstrap.enabled = true ``` 2. 注入非默认`application` namespace或多个namespace的配置示例 ```properties apollo.bootstrap.enabled = true # will inject 'application', 'FX.apollo' and 'application.yml' namespaces in bootstrap phase apollo.bootstrap.namespaces = application,FX.apollo,application.yml ``` 3. 将Apollo配置加载提到初始化日志系统之前(1.2.0+) 从1.2.0版本开始,如果希望把日志相关的配置(如`logging.level.root=info`或`logback-spring.xml`中的参数)也放在Apollo管理,那么可以额外配置`apollo.bootstrap.eagerLoad.enabled=true`来使Apollo的加载顺序放到日志系统加载之前,更多信息可以参考[PR 1614](https://github.com/apolloconfig/apollo/pull/1614)。参考配置示例如下: ```properties # will inject 'application' namespace in bootstrap phase apollo.bootstrap.enabled = true # put apollo initialization before logging system initialization apollo.bootstrap.eagerLoad.enabled=true ``` #### 3.2.1.4 Spring Boot Config Data Loader (Spring Boot 2.4+, Apollo Client 1.9.0+ 推荐) 对于 Spring Boot 2.4 以上版本还支持通过 Config Data Loader 模式来加载配置 ##### 3.2.1.4.1 添加 maven 依赖 apollo-client-config-data 已经依赖了 apollo-client, 所以只需要添加这一个依赖即可, 无需再添加 apollo-client 的依赖 ```xml com.ctrip.framework.apollo apollo-client-config-data 1.9.0 ``` ##### 3.2.1.4.2 参照前述的方式配置 `app.id`, `env`, `apollo.meta`(或者 `apollo.config-service`), `apollo.cluster` ##### 3.2.1.4.3 配置 `application.properties` 或 `application.yml` 使用默认的 namespace `application` ```properties # old way # apollo.bootstrap.enabled=true # 不配置 apollo.bootstrap.namespaces # new way spring.config.import=apollo:// ``` 或者 ```properties # old way # apollo.bootstrap.enabled=true # apollo.bootstrap.namespaces=application # new way spring.config.import=apollo://application ``` 使用自定义 namespace ```properties # old way # apollo.bootstrap.enabled=true # apollo.bootstrap.namespaces=your-namespace # new way spring.config.import=apollo://your-namespace ``` 使用多个 namespaces 注: `spring.config.import` 是从后往前加载配置的, 而 `apollo.bootstrap.namespaces` 是从前往后加载的, 刚好相反。为了保证和原有逻辑一致, 请颠倒 namespaces 的顺序 ```properties # old way # apollo.bootstrap.enabled=true # apollo.bootstrap.namespaces=namespace1,namespace2,namespace3 # new way spring.config.import=apollo://namespace3, apollo://namespace2, apollo://namespace1 ``` #### 3.2.1.5 Spring Boot Config Data Loader (Spring Boot 2.4+, Apollo Client 1.9.0+ 推荐) + webClient 扩展 对于 Spring Boot 2.4 以上版本还支持通过 Config Data Loader 模式来加载配置 Apollo 的 Config Data Loader 还提供了基于 webClient 的 http 客户端来替换原有的 http 客户端, 从而方便的对 http 客户端进行扩展 ##### 3.2.1.5.1 添加 maven 依赖 webClient 可以基于多种实现 (reactor netty httpclient, jetty reactive httpclient, apache httpclient5), 所需添加的依赖如下 ###### reactor netty httpclient ```xml com.ctrip.framework.apollo apollo-client-config-data 1.9.0 org.springframework spring-webflux io.projectreactor.netty reactor-netty-http ``` ###### jetty reactive httpclient ```xml com.ctrip.framework.apollo apollo-client-config-data 1.9.0 org.springframework spring-webflux org.eclipse.jetty jetty-reactive-httpclient ``` ###### apache httpclient5 spring boot 没有指定 apache httpclient5 的版本, 所以这里需要手动指定一下版本 ```xml com.ctrip.framework.apollo apollo-client-config-data 1.9.0 org.springframework spring-webflux org.apache.httpcomponents.client5 httpclient5 5.1 org.apache.httpcomponents.core5 httpcore5-reactive 5.1 ``` ##### 3.2.1.5.2 参照前述的方式配置 `app.id`, `env`, `apollo.meta`(或者 `apollo.config-service`), `apollo.cluster` ##### 3.2.1.5.3 配置 `application.properties` 或 `application.yml` 这里以默认 namespace 为例, namespace 的配置详见 3.2.1.4.3 ```properties spring.config.import=apollo://application apollo.client.extension.enabled=true ``` ##### 3.2.1.5.4 提供 spi 的实现 提供接口 `com.ctrip.framework.apollo.config.data.extension.webclient.customizer.spi.ApolloClientWebClientCustomizerFactory` 的 spi 实现 在配置了 `apollo.client.extension.enabled=true` 之后, Apollo 的 Config Data Loader 会尝试去加载该 spi 的实现类来定制 webClient ### 3.2.2 Spring Placeholder的使用 Spring应用通常会使用Placeholder来注入配置,使用的格式形如${someKey:someDefaultValue},如${timeout:100}。冒号前面的是key,冒号后面的是默认值。 建议在实际使用时尽量给出默认值,以免由于key没有定义导致运行时错误。 从v0.10.0开始的版本支持placeholder在运行时自动更新,具体参见[PR #972](https://github.com/apolloconfig/apollo/pull/972)。 如果需要关闭placeholder在运行时自动更新功能,可以通过以下两种方式关闭: 1. 通过设置System Property `apollo.autoUpdateInjectedSpringProperties`,如启动时传入`-Dapollo.autoUpdateInjectedSpringProperties=false` 2. 通过设置META-INF/app.properties中的`apollo.autoUpdateInjectedSpringProperties`属性,如 ```properties app.id=SampleApp apollo.autoUpdateInjectedSpringProperties=false ``` #### 3.2.2.1 XML使用方式 假设我有一个TestXmlBean,它有两个配置项需要注入: ```java public class TestXmlBean { private int timeout; private int batch; public void setTimeout(int timeout) { this.timeout = timeout; } public void setBatch(int batch) { this.batch = batch; } public int getTimeout() { return timeout; } public int getBatch() { return batch; } } ``` 那么,我在XML中会使用如下方式来定义(假设应用默认的application namespace中有timeout和batch的配置项): ```xml ``` #### 3.2.2.2 Java Config使用方式 假设我有一个TestJavaConfigBean,通过Java Config的方式还可以使用@Value的方式注入: ```java public class TestJavaConfigBean { @Value("${timeout:100}") private int timeout; private int batch; @Value("${batch:200}") public void setBatch(int batch) { this.batch = batch; } public int getTimeout() { return timeout; } public int getBatch() { return batch; } } ``` 在Configuration类中按照下面的方式使用(假设应用默认的application namespace中有`timeout`和`batch`的配置项): ```java @Configuration @EnableApolloConfig public class AppConfig { @Bean public TestJavaConfigBean javaConfigBean() { return new TestJavaConfigBean(); } } ``` #### 3.2.2.3 ConfigurationProperties使用方式 Spring Boot提供了[@ConfigurationProperties](http://docs.spring.io/spring-boot/docs/current/api/org/springframework/boot/context/properties/ConfigurationProperties.html)把配置注入到bean对象中。 Apollo也支持这种方式,下面的例子会把`redis.cache.expireSeconds`和`redis.cache.commandTimeout`分别注入到SampleRedisConfig的`expireSeconds`和`commandTimeout`字段中。 ```java @ConfigurationProperties(prefix = "redis.cache") public class SampleRedisConfig { private int expireSeconds; private int commandTimeout; public void setExpireSeconds(int expireSeconds) { this.expireSeconds = expireSeconds; } public void setCommandTimeout(int commandTimeout) { this.commandTimeout = commandTimeout; } } ``` 在Configuration类中按照下面的方式使用(假设应用默认的application namespace中有`redis.cache.expireSeconds`和`redis.cache.commandTimeout`的配置项): ```java @Configuration @EnableApolloConfig public class AppConfig { @Bean public SampleRedisConfig sampleRedisConfig() { return new SampleRedisConfig(); } } ``` 需要注意的是,`@ConfigurationProperties`如果需要在Apollo配置变化时自动更新注入的值,需要配合使用[EnvironmentChangeEvent](https://cloud.spring.io/spring-cloud-static/spring-cloud.html#_environment_changes)或[RefreshScope](https://cloud.spring.io/spring-cloud-static/spring-cloud.html#_refresh_scope)。相关代码实现,可以参考apollo-use-cases项目中的[ZuulPropertiesRefresher.java](https://github.com/ctripcorp/apollo-use-cases/blob/master/spring-cloud-zuul/src/main/java/com/ctrip/framework/apollo/use/cases/spring/cloud/zuul/ZuulPropertiesRefresher.java#L48)和apollo-demo项目中的[SampleRedisConfig.java](https://github.com/apolloconfig/apollo-demo-java/blob/main/spring-boot-demo/src/main/java/com/apolloconfig/apollo/demo/springboot/config/SampleRedisConfig.java)以及[SpringBootApolloRefreshConfig.java](https://github.com/apolloconfig/apollo-demo-java/blob/main/spring-boot-demo/src/main/java/com/apolloconfig/apollo/demo/springboot/refresh/SpringBootApolloRefreshConfig.java) ### 3.2.3 Spring Annotation支持 Apollo同时还增加了几个新的Annotation来简化在Spring环境中的使用。 1. @ApolloConfig * 用来自动注入Config对象 2. @ApolloConfigChangeListener * 用来自动注册ConfigChangeListener 3. @ApolloJsonValue * 用来把配置的json字符串自动注入为对象 使用样例如下: ```java public class TestApolloAnnotationBean { @ApolloConfig private Config config; //inject config for namespace application @ApolloConfig("application") private Config anotherConfig; //inject config for namespace application @ApolloConfig("FX.apollo") private Config yetAnotherConfig; //inject config for namespace FX.apollo @ApolloConfig("application.yml") private Config ymlConfig; //inject config for namespace application.yml /** * ApolloJsonValue annotated on fields example, the default value is specified as empty list - [] *
    * jsonBeanProperty=[{"someString":"hello","someInt":100},{"someString":"world!","someInt":200}] */ @ApolloJsonValue("${jsonBeanProperty:[]}") private List anotherJsonBeans; @Value("${batch:100}") private int batch; //config change listener for namespace application @ApolloConfigChangeListener private void someOnChange(ConfigChangeEvent changeEvent) { //update injected value of batch if it is changed in Apollo if (changeEvent.isChanged("batch")) { batch = config.getIntProperty("batch", 100); } } //config change listener for namespace application @ApolloConfigChangeListener("application") private void anotherOnChange(ConfigChangeEvent changeEvent) { //do something } //config change listener for namespaces application, FX.apollo and application.yml @ApolloConfigChangeListener({"application", "FX.apollo", "application.yml"}) private void yetAnotherOnChange(ConfigChangeEvent changeEvent) { //do something } //example of getting config from Apollo directly //this will always return the latest value of timeout public int getTimeout() { return config.getIntProperty("timeout", 200); } //example of getting config from injected value //the program needs to update the injected value when batch is changed in Apollo using @ApolloConfigChangeListener shown above public int getBatch() { return this.batch; } private static class JsonBean{ private String someString; private int someInt; } } ``` 在Configuration类中按照下面的方式使用: ```java @Configuration @EnableApolloConfig public class AppConfig { @Bean public TestApolloAnnotationBean testApolloAnnotationBean() { return new TestApolloAnnotationBean(); } } ``` ### 3.2.4 已有配置迁移 很多情况下,应用可能已经有不少配置了,比如Spring Boot的应用,就会有bootstrap.properties/yml, application.properties/yml等配置。 在应用接入Apollo之后,这些配置是可以非常方便的迁移到Apollo的,具体步骤如下: 1. 在Apollo为应用新建项目 2. 在应用中配置好META-INF/app.properties 3. 建议把原先配置先转为properties格式,然后通过Apollo提供的文本编辑模式全部粘帖到应用的application namespace,发布配置 * 如果原来格式是yml,可以使用[YamlPropertiesFactoryBean.getObject](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/beans/factory/config/YamlPropertiesFactoryBean.html#getObject--)转成properties格式 4. 如果原来是yml,想继续使用yml来编辑配置,那么可以创建私有的application.yml namespace,把原来的配置全部粘贴进去,发布配置 * 需要apollo-client是1.3.0及以上版本 5. 把原先的配置文件如bootstrap.properties/yml, application.properties/yml从项目中删除 * 如果需要保留本地配置文件,需要注意部分配置如`server.port`必须确保本地文件已经删除该配置项 如: ```properties spring.application.name = reservation-service server.port = 8080 logging.level = ERROR eureka.client.service-url.defaultZone = http://127.0.0.1:8761/eureka/ eureka.client.healthcheck.enabled = true eureka.client.register-with-eureka = true eureka.client.fetch-registry = true eureka.client.eureka-service-url-poll-interval-seconds = 60 eureka.instance.prefer-ip-address = true ``` ![text-mode-spring-boot-config-sample](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/text-mode-spring-boot-config-sample.png) ## 3.3 Demo 项目中有一个样例客户端的项目:`apollo-demo`,具体信息可以参考[Apollo开发指南](zh/contribution/apollo-development-guide)中的[2.3 Java样例客户端启动](zh/contribution/apollo-development-guide?id=_23-java样例客户端启动)部分。 更多使用案例Demo可以参考[Apollo使用场景和示例代码](https://github.com/ctripcorp/apollo-use-cases)。 # 四、客户端设计 ![client-architecture](https://github.com/apolloconfig/apollo/raw/master/doc/images/client-architecture.png) 上图简要描述了Apollo客户端的实现原理: 1. 客户端和服务端保持了一个长连接,从而能第一时间获得配置更新的推送。(通过Http Long Polling实现) 2. 客户端还会定时从Apollo配置中心服务端拉取应用的最新配置。 * 这是一个fallback机制,为了防止推送机制失效导致配置不更新 * 客户端定时拉取会上报本地版本,所以一般情况下,对于定时拉取的操作,服务端都会返回304 - Not Modified * 定时频率默认为每5分钟拉取一次,客户端也可以通过在运行时指定System Property: `apollo.refreshInterval`来覆盖,单位为分钟。 3. 客户端从Apollo配置中心服务端获取到应用的最新配置后,会保存在内存中 4. 客户端会把从服务端获取到的配置在本地文件系统缓存一份 * 在遇到服务不可用,或网络不通的时候,依然能从本地恢复配置 5. 应用程序可以从Apollo客户端获取最新的配置、订阅配置更新通知 # 五、本地开发模式 Apollo客户端还支持本地开发模式,这个主要用于当开发环境无法连接Apollo服务器的时候,比如在邮轮、飞机上做相关功能开发。 在本地开发模式下,Apollo只会从本地文件读取配置信息,不会从Apollo服务器读取配置。 可以通过下面的步骤开启Apollo本地开发模式。 ## 5.1 修改环境 修改/opt/settings/server.properties(Mac/Linux)或C:\opt\settings\server.properties(Windows)文件,设置env为Local: ```properties env=Local ``` 更多配置环境的方式请参考[1.2.4.1 Environment](#_1241-environment) ## 5.2 准备本地配置文件 在本地开发模式下,Apollo客户端会从本地读取文件,所以我们需要事先准备好配置文件。 ### 5.2.1 本地配置目录 本地配置目录位于: * **Mac/Linux**: /opt/data/{_appId_}/config-cache * **Windows**: C:\opt\data\\{_appId_}\config-cache appId就是应用的appId,如100004458。 请确保该目录存在,且应用程序对该目录有读权限。 **【小技巧】** 推荐的方式是先在普通模式下使用Apollo,这样Apollo会自动创建该目录并在目录下生成配置文件。 ### 5.2.2 本地配置文件 本地配置文件需要按照一定的文件名格式放置于本地配置目录下,文件名格式如下: **_{appId}+{cluster}+{namespace}.properties_** * appId就是应用自己的appId,如100004458 * cluster就是应用使用的集群,一般在本地模式下没有做过配置的话,就是default * namespace就是应用使用的配置namespace,一般是application ![client-local-cache](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/client-local-cache.png) 文件内容以properties格式存储,比如如果有两个key,一个是request.timeout,另一个是batch,那么文件内容就是如下格式: ```properties request.timeout=2000 batch=2000 ``` ## 5.3 修改配置 在本地开发模式下,Apollo不会实时监测文件内容是否有变化,所以如果修改了配置,需要重启应用生效。 # 六、测试模式 1.1.0版本开始增加了`apollo-mockserver`,从而可以很好地支持单元测试时需要mock配置的场景,使用方法如下: ## 6.1 引入pom依赖 ```xml com.ctrip.framework.apollo apollo-mockserver 1.7.0 ``` ## 6.2 在test的resources下放置mock的数据 文件名格式约定为`mockdata-{namespace}.properties` ![image](https://user-images.githubusercontent.com/17842829/44515526-5e0e6480-a6f5-11e8-9c3c-4ff2ec737c8d.png) ## 6.3 写测试类 更多使用demo可以参考[ApolloMockServerApiTest.java](https://github.com/apolloconfig/apollo-java/blob/main/apollo-mockserver/src/test/java/com/ctrip/framework/apollo/mockserver/ApolloMockServerApiTest.java)和[ApolloMockServerSpringIntegrationTest.java](https://github.com/apolloconfig/apollo-java/blob/main/apollo-mockserver/src/test/java/com/ctrip/framework/apollo/mockserver/ApolloMockServerSpringIntegrationTest.java)。 ```java @RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(classes = TestConfiguration.class) public class SpringIntegrationTest { // 启动apollo的mockserver @ClassRule public static EmbeddedApollo embeddedApollo = new EmbeddedApollo(); @Test @DirtiesContext // 这个注解很有必要,因为配置注入会弄脏应用上下文 public void testPropertyInject(){ assertEquals("value1", testBean.key1); assertEquals("value2", testBean.key2); } @Test @DirtiesContext public void testListenerTriggeredByAdd() throws InterruptedException, ExecutionException, TimeoutException { String otherNamespace = "othernamespace"; embeddedApollo.addOrModifyPropery(otherNamespace,"someKey","someValue"); ConfigChangeEvent changeEvent = testBean.futureData.get(5000, TimeUnit.MILLISECONDS); assertEquals(otherNamespace, changeEvent.getNamespace()); assertEquals("someValue", changeEvent.getChange("someKey").getNewValue()); } @EnableApolloConfig("application") @Configuration static class TestConfiguration{ @Bean public TestBean testBean(){ return new TestBean(); } } static class TestBean{ @Value("${key1:default}") String key1; @Value("${key2:default}") String key2; SettableFuture futureData = SettableFuture.create(); @ApolloConfigChangeListener("othernamespace") private void onChange(ConfigChangeEvent changeEvent) { futureData.set(changeEvent); } } } ``` # 七、apollo-client定制 ## 7.1 ConfigService负载均衡算法 > from version 2.1.0 为了满足用户使用apollo-client时,对ConfigService负载均衡算法的不同需求, 我们在2.1.0版本中提供了**spi**。 interface是`com.ctrip.framework.apollo.spi.ConfigServiceLoadBalancerClient`。 输入是meta server返回的多个ConfigService,输出是1个ConfigService。 默认服务提供是`com.ctrip.framework.apollo.spi.RandomConfigServiceLoadBalancerClient`,使用random策略,也就是随机从多个ConfigService中选择1个ConfigService。 ## 7.2 指标输出到Prometheus > 适用于2.4.0及以上版本 可支持导出指标到Prometheus,或者基于SPI编写不同的实现来接入不同的监控系统。 引入客户端插件 ```xml com.ctrip.framework.apollo apollo-plugin-client-prometheus 2.4.0 ``` 调整配置 ```properties apollo.client.monitor.enabled=true apollo.client.monitor.external.type=prometheus ``` 可以通过ConfigMonitor拿到ExporterData(格式取决于你配置的监控系统,这里是支持prometheus格式) 由于Prometheus通过拉取形式获取指标,用户需要自行暴露端点,实现一个类似如下的controller 示例代码 ```java @RestController @ResponseBody public class TestController { @GetMapping("/metrics") public String metrics() { ConfigMonitor configMonitor = ConfigService.getConfigMonitor(); return configMonitor.getExporterData(); } } ``` 启动应用后让Prometheus监听该接口,打印请求日志即可发现如下类似格式信息 ``` # TYPE apollo_client_thread_pool_active_task_count gauge # HELP apollo_client_thread_pool_active_task_count apollo gauge metrics apollo_client_thread_pool_active_task_count{thread_pool_name="RemoteConfigRepository"} 0.0 apollo_client_thread_pool_active_task_count{thread_pool_name="AbstractApolloClientMetricsExporter"} 1.0 apollo_client_thread_pool_active_task_count{thread_pool_name="AbstractConfigFile"} 0.0 apollo_client_thread_pool_active_task_count{thread_pool_name="AbstractConfig"} 0.0 # TYPE apollo_client_namespace_timeout gauge # HELP apollo_client_namespace_timeout apollo gauge metrics apollo_client_namespace_timeout 0.0 # TYPE apollo_client_thread_pool_pool_size gauge # HELP apollo_client_thread_pool_pool_size apollo gauge metrics apollo_client_thread_pool_pool_size{thread_pool_name="RemoteConfigRepository"} 1.0 apollo_client_thread_pool_pool_size{thread_pool_name="AbstractApolloClientMetricsExporter"} 1.0 apollo_client_thread_pool_pool_size{thread_pool_name="AbstractConfigFile"} 0.0 apollo_client_thread_pool_pool_size{thread_pool_name="AbstractConfig"} 0.0 # TYPE apollo_client_thread_pool_queue_remaining_capacity gauge # HELP apollo_client_thread_pool_queue_remaining_capacity apollo gauge metrics apollo_client_thread_pool_queue_remaining_capacity{thread_pool_name="RemoteConfigRepository"} 2.147483647E9 apollo_client_thread_pool_queue_remaining_capacity{thread_pool_name="AbstractApolloClientMetricsExporter"} 2.147483647E9 apollo_client_thread_pool_queue_remaining_capacity{thread_pool_name="AbstractConfigFile"} 0.0 apollo_client_thread_pool_queue_remaining_capacity{thread_pool_name="AbstractConfig"} 0.0 # TYPE apollo_client_exception_num counter # HELP apollo_client_exception_num apollo counter metrics apollo_client_exception_num_total 1404.0 apollo_client_exception_num_created 1.729435502796E9 # TYPE apollo_client_thread_pool_largest_pool_size gauge # HELP apollo_client_thread_pool_largest_pool_size apollo gauge metrics apollo_client_thread_pool_largest_pool_size{thread_pool_name="RemoteConfigRepository"} 1.0 apollo_client_thread_pool_largest_pool_size{thread_pool_name="AbstractApolloClientMetricsExporter"} 1.0 apollo_client_thread_pool_largest_pool_size{thread_pool_name="AbstractConfigFile"} 0.0 apollo_client_thread_pool_largest_pool_size{thread_pool_name="AbstractConfig"} 0.0 # TYPE apollo_client_thread_pool_queue_size gauge # HELP apollo_client_thread_pool_queue_size apollo gauge metrics apollo_client_thread_pool_queue_size{thread_pool_name="RemoteConfigRepository"} 352.0 apollo_client_thread_pool_queue_size{thread_pool_name="AbstractApolloClientMetricsExporter"} 0.0 apollo_client_thread_pool_queue_size{thread_pool_name="AbstractConfigFile"} 0.0 apollo_client_thread_pool_queue_size{thread_pool_name="AbstractConfig"} 0.0 # TYPE apollo_client_namespace_usage counter # HELP apollo_client_namespace_usage apollo counter metrics apollo_client_namespace_usage_total{namespace="application"} 11.0 apollo_client_namespace_usage_created{namespace="application"} 1.729435502791E9 # TYPE apollo_client_thread_pool_core_pool_size gauge # HELP apollo_client_thread_pool_core_pool_size apollo gauge metrics apollo_client_thread_pool_core_pool_size{thread_pool_name="RemoteConfigRepository"} 1.0 apollo_client_thread_pool_core_pool_size{thread_pool_name="AbstractApolloClientMetricsExporter"} 1.0 apollo_client_thread_pool_core_pool_size{thread_pool_name="AbstractConfigFile"} 0.0 apollo_client_thread_pool_core_pool_size{thread_pool_name="AbstractConfig"} 0.0 # TYPE apollo_client_namespace_not_found gauge # HELP apollo_client_namespace_not_found apollo gauge metrics apollo_client_namespace_not_found 351.0 # TYPE apollo_client_thread_pool_total_task_count gauge # HELP apollo_client_thread_pool_total_task_count apollo gauge metrics apollo_client_thread_pool_total_task_count{thread_pool_name="RemoteConfigRepository"} 353.0 apollo_client_thread_pool_total_task_count{thread_pool_name="AbstractApolloClientMetricsExporter"} 4.0 apollo_client_thread_pool_total_task_count{thread_pool_name="AbstractConfigFile"} 0.0 apollo_client_thread_pool_total_task_count{thread_pool_name="AbstractConfig"} 0.0 # TYPE apollo_client_namespace_first_load_time_spend_in_ms gauge # HELP apollo_client_namespace_first_load_time_spend_in_ms apollo gauge metrics apollo_client_namespace_first_load_time_spend_in_ms{namespace="application"} 108.0 # TYPE apollo_client_thread_pool_maximum_pool_size gauge # HELP apollo_client_thread_pool_maximum_pool_size apollo gauge metrics apollo_client_thread_pool_maximum_pool_size{thread_pool_name="RemoteConfigRepository"} 2.147483647E9 apollo_client_thread_pool_maximum_pool_size{thread_pool_name="AbstractApolloClientMetricsExporter"} 2.147483647E9 apollo_client_thread_pool_maximum_pool_size{thread_pool_name="AbstractConfigFile"} 2.147483647E9 apollo_client_thread_pool_maximum_pool_size{thread_pool_name="AbstractConfig"} 2.147483647E9 # TYPE apollo_client_namespace_item_num gauge # HELP apollo_client_namespace_item_num apollo gauge metrics apollo_client_namespace_item_num{namespace="application"} 9.0 # TYPE apollo_client_thread_pool_completed_task_count gauge # HELP apollo_client_thread_pool_completed_task_count apollo gauge metrics apollo_client_thread_pool_completed_task_count{thread_pool_name="RemoteConfigRepository"} 1.0 apollo_client_thread_pool_completed_task_count{thread_pool_name="AbstractApolloClientMetricsExporter"} 3.0 apollo_client_thread_pool_completed_task_count{thread_pool_name="AbstractConfigFile"} 0.0 apollo_client_thread_pool_completed_task_count{thread_pool_name="AbstractConfig"} 0.0 # EOF ``` 同时查看Prometheus控制台也能看到如下信息 ![Prometheus console showing Apollo client metrics](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/apollo-client-monitor-prometheus.png) ## 7.3 指标输出到自定义监控系统 > 适用于2.4.0及以上版本 用户需要自行编写 MetricsExporter,继承 AbstractApolloClientMetricsExporter,实现以下方法: - doInit(初始化方法) - isSupport(external-type 配置调用方法) - registerOrUpdateCounterSample(注册更新 Counter 指标方法) - registerOrUpdateGaugeSample(注册更新 Gauge 指标方法) - response(导出所需类型指标数据方法) 并配置相关SPI文件 MetricsExporter加载流程图 ```mermaid sequenceDiagram participant Factory as DefaultMetricsExporterFactory participant Exporter as MetricsExporter participant SPI as SPI Loader %% 步骤 1: Factory 从 SPI 加载所有 Exporters Factory->>SPI: loadAllOrdered(MetricsExporter) SPI-->>Factory: List %% 步骤 2: Factory 查找支持的 Exporter Factory->>Exporter: isSupport(externalSystemType) Exporter-->>Factory: true / false alt Exporter Found %% 步骤 3: Factory 初始化 Exporter Factory->>Exporter: init(listeners, exportPeriod) Factory-->>Client: Exporter Instance else No Exporter Found %% 步骤 4: Factory 返回 null Factory-->>Client: null end ``` ### 7.3.1 SkyWalking案例 通过配置开启 ```properties apollo.client.monitor.enabled=true #exporter内定义 apollo.client.monitor.external.type=skywalking ``` 创建SkyWalkingMetricsExporter类,继承AbstractApolloClientMetricsExporter 继承后大致代码如下 注意: 样例演示,切勿直接到生产直接使用,请根据公司内具体情况来实现 ```java public class SkyWalkingMetricsExporter extends AbstractApolloClientMetricsExporter { private static final String SKYWALKING = "skywalking"; protected SkywalkingMeterRegistry registry; //用户设计时,需考虑存储指标的数据结构是否有内存占用过多问题 protected Map counterMap; private Map gaugeMap; private Map> gaugeValues; @Override public void doInit() { registry = new SkywalkingMeterRegistry(); counterMap = new ConcurrentHashMap<>(); gaugeValues = new ConcurrentHashMap<>(); gaugeMap = new ConcurrentHashMap<>(); } @Override public boolean isSupport(String form) { return SKYWALKING.equals(form); } @Override public void registerOrUpdateCounterSample(String name, Map tags, double incrValue) { String key = name + tags.toString(); Counter counter = counterMap.get(key); if (counter == null) { counter = createCounter(name, tags); counterMap.put(key, counter); } counter.increment(incrValue); } private Counter createCounter(String name, Map tags) { return Counter.builder(name) .tags(tags.entrySet().stream() .map(entry -> Tag.of(entry.getKey(), entry.getValue())) .collect(Collectors.toList())) .register(registry); } @Override public void registerOrUpdateGaugeSample(String name, Map tags, double value) { String key = name + tags.toString(); Gauge gauge = gaugeMap.get(key); if (gauge == null) { createGauge(name, tags, value); } else { gaugeValues.get(key).set(value); } } public void createGauge(String name, Map tags, double value) { String key = name + tags.toString(); AtomicReference valueHolder = gaugeValues.computeIfAbsent(key, k -> new AtomicReference<>(value)); gaugeMap.computeIfAbsent(key, k -> Gauge.builder(name, valueHolder::get) .tags(tags.entrySet().stream() .map(entry -> Tag.of(entry.getKey(), entry.getValue())) .collect(Collectors.toList())) .register(registry)); } @Override public String response() { // 返回需要的响应内容 return "该方法在skyWalking的推送模式中不需要实现"; } } ``` doInit方法是供用户在初始化时自行做扩展的,会在AbstractApoolloClientMetircsExporter里的init方法被调用 ```java @Override public void init(List collectors, long collectPeriod) { // code doInit(); // code } ``` 引入micrometer ```xml org.apache.skywalking apm-toolkit-micrometer-1.10 ``` 根据Micrometer的机制初始化SkywalkingMeterRegistry, 以及一些map用于存储指标数据 ```java private static final String SKYWALKING = "skywalking"; private SkywalkingMeterRegistry registry; //用户设计时,需考虑存储指标的数据结构是否有内存占用过多问题 private Map counterMap; private Map gaugeMap; private Map> gaugeValues; @Override public void doInit() { registry = new SkywalkingMeterRegistry(); counterMap = new ConcurrentHashMap<>(); gaugeValues = new ConcurrentHashMap<>(); gaugeMap = new ConcurrentHashMap<>(); } ``` isSupport方法将会在DefaultApolloClientMetricsExporterFactory通过SPI读取MetricsExporter时被调用做判断,用于实现在有多个SPI实现时可以准确启用用户所配置的那一个Exporter 比如配置时候你希望启用skyWalking,你规定的apollo.client.monitor.external.type配置值为skyWalking,那这里就实现如下方法 ```java @Override public boolean isSupport(String form) { return SKYWALKING.equals(form); } ``` registerOrUpdateCounterSample,registerOrUpdateGaugeSample即是用来注册Counter,Gauge类型指标的方法,只需要根据传来的参数正常注册以及更新数据即可 ```java @Override public void registerOrUpdateCounterSample(String name, Map tags, double incrValue) { String key = name + tags.toString(); Counter counter = counterMap.get(key); if (counter == null) { counter = createCounter(name, tags); counterMap.put(key, counter); } counter.increment(incrValue); } private Counter createCounter(String name, Map tags) { return Counter.builder(name) .tags(tags.entrySet().stream() .map(entry -> Tag.of(entry.getKey(), entry.getValue())) .collect(Collectors.toList())) .register(registry); } @Override public void registerOrUpdateGaugeSample(String name, Map tags, double value) { String key = name + tags.toString(); Gauge gauge = gaugeMap.get(key); if (gauge == null) { createGauge(name, tags, value); } else { gaugeValues.get(key).set(value); } } public void createGauge(String name, Map tags, double value) { String key = name + tags.toString(); AtomicReference valueHolder = gaugeValues.computeIfAbsent(key, k -> new AtomicReference<>(value)); gaugeMap.computeIfAbsent(key, k -> Gauge.builder(name, valueHolder::get) .tags(tags.entrySet().stream() .map(entry -> Tag.of(entry.getKey(), entry.getValue())) .collect(Collectors.toList())) .register(registry)); } ``` response是用于方便指标获取模式为拉取的监控系统,如Prometheus,但是SkyWalking用推送更常见,这里就不需要实现,用户自行配置SkyWalking即可 ```java @Override public String response() { // 返回需要的响应内容 return "该方法在skyWalking的推送模式中不需要实现"; } ``` 最后在项目目录下resources/META-INF/services编写对应的spi文件,告诉框架来加载这个类 文件名为com.ctrip.framework.apollo.monitor.internal.exporter.ApolloClientMetricsExporter ```text your.package.SkyWalkingMetricsExporter ``` 至此,已经将Client的指标数据接入SkyWalking。 ### 7.3.2 Prometheus案例 [PrometheusApolloClientMetricsExporter.java](https://github.com/apolloconfig/apollo-java/blob/main/apollo-plugin/apollo-plugin-client-prometheus/src/main/java/com/ctrip/framework/apollo/monitor/internal/exporter/impl/PrometheusApolloClientMetricsExporter.java) ================================================ FILE: docs/zh/client/k8s-configmap-user-guide.md ================================================ ### Apollo K8S ConfigMap接入 自动同步 Apollo 配置到 K8S ConfigMap 中 项目地址:[apollo-configmap](https://github.com/adamswanglin/apollo-configmap) ================================================ FILE: docs/zh/client/nodejs-sdks-user-guide.md ================================================ ### Apollo NodeJS 客户端 1 项目地址:[node-apollo](https://github.com/Quinton/node-apollo) > 非常感谢[@Quinton](https://github.com/Quinton)提供NodeJS Apollo客户端的支持 ### Apollo NodeJS 客户端 2 项目地址:[ctrip-apollo](https://github.com/kaelzhang/ctrip-apollo) > 非常感谢[@kaelzhang](https://github.com/kaelzhang)提供NodeJS Apollo客户端的支持 ### Apollo NodeJS 客户端 3 项目地址:[node-apollo-client](https://github.com/shinux/node-apollo-client) > 非常感谢[@shinux](https://github.com/shinux)提供NodeJS Apollo客户端的支持 ### Apollo NodeJS 客户端 4 项目地址:[ctrip-apollo-client](https://github.com/lvgithub/ctrip-apollo-client) > 非常感谢[@lvgithub](https://github.com/lvgithub)提供NodeJS Apollo客户端的支持 ### Apollo NodeJS 客户端 5 项目地址:[apollo-node](https://github.com/lengyuxuan/apollo-node) > 非常感谢[@lengyuxuan](https://github.com/lengyuxuan)提供NodeJS Apollo客户端的支持 ### Apollo NodeJS 客户端 6 项目地址:[egg-apollo-client](https://github.com/xuezier/egg-apollo-client) > 非常感谢[@xuezier](https://github.com/xuezier)提供NodeJS Apollo客户端的支持 ### Apollo NodeJS 客户端 7 项目地址:[apollo-node-client](https://github.com/zhangxh1023/apollo-node-client) > 非常感谢[@zhangxh1023](https://github.com/zhangxh1023)提供NodeJS Apollo客户端的支持 ### Apollo NodeJS 客户端 8 项目地址:[@vodyani/apollo-client](https://github.com/vodyani/apollo-client) > 非常感谢[@ChoGathK](https://github.com/ChoGathK)提供NodeJS Apollo客户端的支持 ### Apollo NodeJS(WebAssembly) 客户端 9 一个同时支持 Rust 以及 WASM 的 apollo 客户端 项目地址:[apollo-rust-client](https://github.com/qqiao/apollo-rust-client) > 非常感谢[@qqiao](https://github.com/qqiao)提供NodeJS Apollo客户端的支持 ================================================ FILE: docs/zh/client/other-language-client-user-guide.md ================================================ 目前Apollo团队由于人力所限,只提供了Java和.Net的客户端,对于其它语言的应用,可以通过本文的介绍来直接通过Http接口获取配置。 另外,如果有团队/个人有兴趣的话,也欢迎帮助我们来实现其它语言的客户端,具体细节可以联系@nobodyiam和@lepdou。 >注:目前已有热心用户贡献了Go、Python、NodeJS、PHP、C++的客户端,更多信息可以参考"客户端指南" ## 1.1 应用接入Apollo 首先需要在Apollo中接入你的应用,具体步骤可以参考[应用接入文档](zh/portal/apollo-user-guide?id=一、普通应用接入指南)。 ## 1.2 通过带缓存的Http接口从Apollo读取配置 该接口会从缓存中获取配置,适合频率较高的配置拉取请求,如简单的每30秒轮询一次配置。 由于缓存最多会有一秒的延时,所以如果需要配合配置推送通知实现实时更新配置的话,请参考[1.3 通过不带缓存的Http接口从Apollo读取配置](#_13-%E9%80%9A%E8%BF%87%E4%B8%8D%E5%B8%A6%E7%BC%93%E5%AD%98%E7%9A%84http%E6%8E%A5%E5%8F%A3%E4%BB%8Eapollo%E8%AF%BB%E5%8F%96%E9%85%8D%E7%BD%AE)。 ### 1.2.1 Http接口说明 **URL**: {config_server_url}/configfiles/json/{appId}/{clusterName}/{namespaceName}?ip={clientIp} **Method**: GET **参数说明**: | 参数名 | 是否必须 | 参数值 | 备注 | |-------------------|----------|----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | config_server_url | 是 | Apollo配置服务的地址 | | | appId | 是 | 应用的appId | | | clusterName | 是 | 集群名 | 一般情况下传入 default 即可。 如果希望配置按集群划分,可以参考[集群独立配置说明](zh/portal/apollo-user-guide?id=三、集群独立配置说明)做相关配置,然后在这里填入对应的集群名。 | | namespaceName | 是 | Namespace的名字 | 如果没有新建过Namespace的话,传入application即可。 如果创建了Namespace,并且需要使用该Namespace的配置,则传入对应的Namespace名字。**需要注意的是对于properties类型的namespace,只需要传入namespace的名字即可,如application。对于其它类型的namespace,需要传入namespace的名字加上后缀名,如datasources.json** | | ip | 否 | 应用部署的机器ip | 这个参数是可选的,用来实现灰度发布。 如果不想传这个参数,请注意URL中从?号开始的query parameters整个都不要出现。 | ### 1.2.2 Http接口返回格式 该Http接口返回的是JSON格式、UTF-8编码,包含了对应namespace中所有的配置项。 若是properties类型的namespace,返回内容Sample如下: ```json { "portal.elastic.document.type":"biz", "portal.elastic.cluster.name":"hermes-es-fws" } ``` 若不是properties类型的namespace,返回内容Sample如下(content是namespace的内容): ```json { "content": "{\"portal.elastic.document.type\":\"biz\",\"portal.elastic.cluster.name\":\"hermes-es-fws\"}" } ``` > 通过`{config_server_url}/configfiles/raw/{appId}/{clusterName}/{namespaceName}?ip={clientIp}`可以获取到原始的配置内容,不会进行转义。 > 通过`{config_server_url}/configfiles/{appId}/{clusterName}/{namespaceName}?ip={clientIp}`可以获取到properties形式的配置 ### 1.2.3 测试 由于是Http接口,所以在URL组装OK之后,直接通过浏览器、或者相关的http接口测试工具访问即可。 ## 1.3 通过不带缓存的Http接口从Apollo读取配置 该接口会直接从数据库中获取配置,可以配合配置推送通知实现实时更新配置。 ### 1.3.1 Http接口说明 **URL**: {config_server_url}/configs/{appId}/{clusterName}/{namespaceName}?releaseKey={releaseKey}&messages={messages}&label={label}&ip={clientIp} **Method**: GET **参数说明**: | 参数名 | 是否必须 | 参数值 | 备注 | |-------------------|---------|---------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | config_server_url | 是 | Apollo配置服务的地址 | | | appId | 是 | 应用的appId | | | clusterName | 是 | 集群名 | 一般情况下传入 default 即可。 如果希望配置按集群划分,可以参考[集群独立配置说明](zh/portal/apollo-user-guide?id=三、集群独立配置说明)做相关配置,然后在这里填入对应的集群名。 | | namespaceName | 是 | Namespace的名字 | 如果没有新建过Namespace的话,传入application即可。 如果创建了Namespace,并且需要使用该Namespace的配置,则传入对应的Namespace名字。**需要注意的是对于properties类型的namespace,只需要传入namespace的名字即可,如application。对于其它类型的namespace,需要传入namespace的名字加上后缀名,如datasources.json** | | releaseKey | 否 | 上一次的releaseKey | 将上一次返回对象中的releaseKey传入即可,用来给服务端比较版本,如果版本比下来没有变化,则服务端直接返回304以节省流量和运算 | | messages | 否 | 最新的 notificationId | 用于给服务端即时更新内存缓存,如果传递了 releaseKey,而不传递 messages参数,在服务端多实例、且开启内存缓存时、有概率会获取不到最新的配置。这个参数是json结构的字符串 {"details":{"key":notificationId}},需要将 `appId`、`clusterName`、`namespaceName`使用 `+` 号拼接为 key,假设现在 `appId=app`、`clusterName=default`、`namespaceName=test`、`notificationId=11`,则 messages 参数为 {"details":{"app+default+test":11}},使用 messages 参数时,需要进行 URL编码。 | | label | 否 | 灰度配置的标签 | 这个参数是可选的,用于灰度发布的标签规则匹配。 | | ip | 否 | 应用部署的机器ip | 这个参数是可选的,用于灰度发布的 ip 规则匹配。 | ### 1.3.2 Http接口返回格式 该Http接口返回的是JSON格式、UTF-8编码。 如果配置没有变化(传入的releaseKey和服务端的相等),则返回HttpStatus 304,response body为空。 如果配置有变化,则会返回HttpStatus 200,response body为对应namespace的meta信息以及其中所有的配置项。 返回内容Sample如下: ```json { "appId": "100004458", "cluster": "default", "namespaceName": "application", "configurations": { "portal.elastic.document.type":"biz", "portal.elastic.cluster.name":"hermes-es-fws" }, "releaseKey": "20170430092936-dee2d58e74515ff3" } ``` ### 1.3.3 测试 由于是Http接口,所以在URL组装OK之后,直接通过浏览器、或者相关的http接口测试工具访问即可。 ## 1.4 应用感知配置更新 Apollo提供了基于Http long polling的配置更新推送通知,第三方客户端可以看自己实际的需求决定是否需要使用这个功能。 如果对配置更新时间不是那么敏感的话,可以通过定时刷新来感知配置更新,刷新频率可以视应用自身情况来定,建议在30秒以上。 如果需要做到实时感知配置更新(1秒)的话,可以参考下面的文档实现配置更新推送的功能。 ### 1.4.1 配置更新推送实现思路 这里建议大家可以参考Apollo的Java实现:[RemoteConfigLongPollService.java](https://github.com/apolloconfig/apollo-java/blob/main/apollo-client/src/main/java/com/ctrip/framework/apollo/internals/RemoteConfigLongPollService.java),代码量200多行,总体上还是比较简单的。 #### 1.4.1.1 初始化 首先需要确定哪些namespace需要配置更新推送,Apollo的实现方式是程序第一次获取某个namespace的配置时就会来注册一下,我们就知道有哪些namespace需要配置更新推送了。 初始化后的结果就是得到一个notifications的Map,内容是namespaceName -> notificationId(初始值为-1)。 运行过程中如果发现有新的namespace需要配置更新推送,直接塞到notifications这个Map里面即可。 #### 1.4.1.2 请求服务 有了notifications这个Map之后,就可以请求服务了。这里先描述一下请求服务的逻辑,具体的URL参数和说明请参见后面的接口说明。 1. 请求远端服务,带上自己的应用信息以及notifications信息 2. 服务端针对传过来的每一个namespace和对应的notificationId,检查notificationId是否是最新的 3. 如果都是最新的,则保持住请求60秒,如果60秒内没有配置变化,则返回HttpStatus 304。如果60秒内有配置变化,则返回对应namespace的最新notificationId, HttpStatus 200。 4. 如果传过来的notifications信息中发现有notificationId比服务端老,则直接返回对应namespace的最新notificationId, HttpStatus 200。 5. 客户端拿到服务端返回后,判断返回的HttpStatus 6. 如果返回的HttpStatus是304,说明配置没有变化,重新执行第1步 7. 如果返回的HttpStatus是200,说明配置有变化,针对变化的namespace重新去服务端拉取配置,参见[1.3 通过不带缓存的Http接口从Apollo读取配置](#_13-%E9%80%9A%E8%BF%87%E4%B8%8D%E5%B8%A6%E7%BC%93%E5%AD%98%E7%9A%84http%E6%8E%A5%E5%8F%A3%E4%BB%8Eapollo%E8%AF%BB%E5%8F%96%E9%85%8D%E7%BD%AE)。同时更新notifications map中的notificationId。重新执行第1步。 ### 1.4.2 Http接口说明 **URL**: {config_server_url}/notifications/v2?appId={appId}&cluster={clusterName}¬ifications={notifications} **Method**: GET **参数说明**: | 参数名 | 是否必须 | 参数值 | 备注 | |-------------------|----------|----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | config_server_url | 是 | Apollo配置服务的地址 | | | appId | 是 | 应用的appId | | | clusterName | 是 | 集群名 | 一般情况下传入 default 即可。 如果希望配置按集群划分,可以参考[集群独立配置说明](zh/portal/apollo-user-guide?id=三、集群独立配置说明)做相关配置,然后在这里填入对应的集群名。 | | notifications | 是 | notifications信息 | 传入本地的notifications信息,注意这里需要以array形式转为json传入,如:[{"namespaceName": "application", "notificationId": 100}, {"namespaceName": "FX.apollo", "notificationId": 200}]。**需要注意的是对于properties类型的namespace,只需要传入namespace的名字即可,如application。对于其它类型的namespace,需要传入namespace的名字加上后缀名,如datasources.json** | > 注1:由于服务端会hold住请求60秒,所以请确保客户端访问服务端的超时时间要大于60秒。 > 注2:别忘了对参数进行[url encode](https://en.wikipedia.org/wiki/Percent-encoding) ### 1.4.3 Http接口返回格式 该Http接口返回的是JSON格式、UTF-8编码,包含了有变化的namespace和最新的notificationId。 返回内容Sample如下: ```json [ { "namespaceName": "application", "notificationId": 101 } ] ``` ### 1.4.4 测试 由于是Http接口,所以在URL组装OK之后,直接通过浏览器、或者相关的http接口测试工具访问即可。 ## 1.5 配置访问密钥 Apollo从1.6.0版本开始增加访问密钥机制,从而只有经过身份验证的客户端才能访问敏感配置。如果应用开启了访问密钥,客户端发出请求时需要增加签名,否则无法获取配置。 需要设置的Header信息: | Header | Value | 备注 | |---------------|------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------| | Authorization | Apollo ${appId}:${signature} | appId: 应用的appId,signature:使用访问密钥对当前时间毫秒值以及所访问的URL里的的path和query部分加签后的值,具体实现可以参考[Signature.signature](https://github.com/apolloconfig/apollo/blob/aa184a2e11d6e7e3f519d860d69f3cf30ccfcf9c/apollo-core/src/main/java/com/ctrip/framework/apollo/core/signature/Signature.java#L22) | | Timestamp | 从`1970-1-1 00:00:00 UTC+0`到现在所经过的毫秒数 | 可以参考[System.currentTimeMillis](https://docs.oracle.com/javase/7/docs/api/java/lang/System.html#currentTimeMillis()) | ## 1.6 错误码说明 正常情况下,接口返回的Http状态码是200,下面列举了Apollo会返回的非200错误码说明。 ### 1.6.1 400 - Bad Request 客户端传入参数的错误,如必选参数没有传入等,客户端需要根据提示信息检查对应的参数是否正确。 ### 1.6.2 401 - Unauthorized 客户端未授权,如服务端配置了访问密钥,客户端未配置或配置错误。 ### 1.6.3 404 - Not Found 接口要访问的资源不存在,一般是URL或URL的参数错误,或者是对应的namespace还没有发布过配置。 ### 1.6.4 405 - Method Not Allowed 接口访问的Method不正确,比如应该使用GET的接口使用了POST访问等,客户端需要检查接口访问方式是否正确。 ### 1.6.5 500 - Internal Server Error 其它类型的错误默认都会返回500,对这类错误如果应用无法根据提示信息找到原因的话,可以尝试查看服务端日志来排查问题。 ================================================ FILE: docs/zh/client/php-sdks-user-guide.md ================================================ ### Apollo PHP 客户端 1 项目地址:[apollo-php-client](https://github.com/multilinguals/apollo-php-client) > 非常感谢[@t04041143](https://github.com/t04041143)提供PHP Apollo客户端的支持 ### Apollo PHP 客户端 2 项目地址:[apollo-sdk-config](https://github.com/fengzhibin/apollo-sdk-config) 项目地址:[apollo-sdk-clientd](https://github.com/fengzhibin/apollo-sdk-clientd) > 非常感谢[@fengzhibin](https://github.com/fengzhibin)提供PHP Apollo客户端的支持 ================================================ FILE: docs/zh/client/python-sdks-user-guide.md ================================================ ### Apollo Python 客户端 1 项目地址:[pyapollo](https://github.com/filamoon/pyapollo) > 非常感谢[@filamoon](https://github.com/filamoon)提供Python Apollo客户端的支持 ### Apollo Python 客户端 2 项目地址:[BruceWW-pyapollo](https://github.com/BruceWW/pyapollo) > 非常感谢[@BruceWW](https://github.com/BruceWW)提供Python Apollo客户端的支持 ### Apollo Python 客户端 3 项目地址:[xhrg-product/apollo-client-python](https://github.com/xhrg-product/apollo-client-python) > 非常感谢[@xhrg-product](https://github.com/xhrg-product)提供Python Apollo客户端的支持 ### Apollo Python 客户端 4 项目地址:[OuterCloud/pyapollo](https://github.com/OuterCloud/pyapollo.git) > 非常感谢[@OuterCloud](https://github.com/OuterCloud)提供Python Apollo客户端的支持 ================================================ FILE: docs/zh/client/rust-sdks-user-guide.md ================================================ ### Apollo Rust 客户端 项目地址:[apollo-rust-sdk](https://github.com/liushv0/apollo-rust-sdk) > 非常感谢[@liushv0](https://github.com/liushv0)提供Rust Apollo客户端的支持 ### Apollo Rust 客户端 2 一个同时支持 Rust 以及 WASM 的 apollo 客户端 项目地址:[apollo-rust-client](https://github.com/qqiao/apollo-rust-client) > 非常感谢[@qqiao](https://github.com/qqiao)提供Rust Apollo客户端的支持 ### Apollo Rust 客户端 3 项目地址:[apollo-client](https://github.com/jmjoy/apollo-client) > 非常感谢[@jmjoy](https://github.com/jmjoy)提供Rust Apollo客户端的支持 ================================================ FILE: docs/zh/community/team.md ================================================ # Apollo 团队 Apollo 团队由 Member 和 Contributor 组成。Member 可以直接访问 Apollo 项目的源代码并基于代码库积极演进。Contributor 通过向 Member 提交补丁和建议来改善项目,项目的贡献者数量是没有限制的。无论是进行小规模的清理,提交新的功能或其它形式的贡献,都将受到极大的赞赏。 有关社区治理模型的更多信息,请参考[GOVERNANCE.md](https://github.com/apolloconfig/apollo/blob/master/GOVERNANCE.md)。 ## Member Member 包括 PMC 成员和 Committer,该列表按字母顺序排列。 ### Project Management Committee(PMC) | GitHub ID | Name | Organization | | ----------- | ------------- | ------------ | | Anilople | Xiaoquan Wang | Some Bank | | hezhangjian | ZhangJian He | Huawei | | JaredTan95 | Jared Tan | DaoCloud | | kezhenxu94 | Zhenxu Ke | Tetrate | | klboke | Kailing Chen | TapTap | | lepdou | Le Zhang | Tencent | | nobodyiam | Jason Song | Ant Group | | zouyx | Joe Zou | Shein | ### Committer | GitHub ID | Name | Organization | |-------------|---------------|--------------| | Accelerater | Zhuohao Li | Daocloud | | Anilople | Xiaoquan Wang | Some Bank | | hezhangjian | ZhangJian He | Huawei | | JaredTan95 | Jared Tan | DaoCloud | | kezhenxu94 | Zhenxu Ke | Tetrate | | klboke | Kailing Chen | TapTap | | lepdou | Le Zhang | Tencent | | nisiyong | Stephen Ni | Qihoo 360 | | nobodyiam | Jason Song | Ant Group | | pengweiqhca | Wei Peng | Tuhu | | vdisk-group | Lvqiu Ye | Hundsun | | zouyx | Joe Zou | Shein | | spaceluke | Zhile Wei | ByteDance | ## Contributor ### Apollo 主仓库 ### apollo.net ## **如何成为提交者** 请参考 [How to become a Committer](https://github.com/apolloconfig/apollo/blob/master/GOVERNANCE.md#how-to-become-a-committer). ================================================ FILE: docs/zh/community/thank-you.md ================================================ # 致谢
    ctrip Apollo 项目诞生于携程框架研发部,在初始开发过程中得到了各方的鼓励和支持,感谢携程为 Apollo 社区做出的贡献。
    jetbrains intellij-idea Apollo 团队使用 [IntelliJ IDEA](https://www.jetbrains.com/idea/) 开发开源项目,非常感谢 [JetBrains](https://www.jetbrains.com/) 赞助许可证。
    docsify Apollo 团队使用 [docsify](https://docsify.js.org/) 生成文档站点。
    jprofiler Apollo 团队使用 [JProfiler](https://www.ej-technologies.com/products/jprofiler/overview.html) 定位开源项目中性能问题,非常感谢 [EJ-Technologies](https://www.ej-technologies.com/) 赞助许可证。
    ================================================ FILE: docs/zh/contribution/apollo-development-guide.md ================================================ 本文档介绍了如何在本地使用IDE编译、运行Apollo,从而可以帮助大家了解Apollo的内在运行机制,同时也为自定义开发做好准备。 #   # 一、准备工作 ## 1.1 本地运行时环境 Apollo本地开发需要以下组件: 1. Java: 17+ 2. MySQL: 5.6.5+ (如果使用 H2 内存数据库/H2 文件数据库,则无需 MySQL) 3. IDE: 没有特殊要求 其中MySQL需要创建Apollo数据库并导入基础数据。 具体步骤请参考[分布式部署指南](zh/deployment/distributed-deployment-guide)中的以下部分: 1. [一、准备工作](zh/deployment/distributed-deployment-guide#一、准备工作) 2. [2.1 创建数据库](zh/deployment/distributed-deployment-guide#_21-创建数据库) ## 1.2 Apollo总体设计 具体请参考[Apollo配置中心设计](zh/design/apollo-design) ## 1.3 OpenAPI 代码生成 `apollo-portal` 模块通过 OpenAPI 的 YAML 描述文件在编译阶段生成 `OpenXxxDTO` 类(例如 `OpenAppDTO`)。 首次拉取代码或发现在 IDE 中提示这些 DTO 缺失时,请在仓库根目录或 `apollo-portal` 模块目录执行一次 Maven 编译流程,以触发本地代码生成: ```bash mvn clean compile -pl apollo-portal -am ``` 或者直接在 `apollo-portal` 目录执行: ```bash mvn clean compile ``` 命令完成后,`com.ctrip.framework.apollo.openapi.model` 包下的 `OpenXxxDTO` 类会重新生成。 # 二、本地启动 ## 2.1 Apollo Assembly 我们在本地开发时,一般会在IDE中启动`apollo-assembly`。 下面以Intellij Community 2016.2版本为例来说明如何在本地启动`apollo-assembly`。 ![ApolloApplication-Overview](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/local-development/ApolloApplication-Overview.png) ### 2.1.1 新建运行配置 ![NewConfiguration-Application](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/local-development/NewConfiguration-Application.png) ### 2.1.2 Main class配置 `com.ctrip.framework.apollo.assembly.ApolloApplication` > 注:如果希望独立启动`apollo-portal`、`apollo-configservice`和`apollo-adminservice`,可以把Main Class分别换成 > `com.ctrip.framework.apollo.portal.PortalApplication` > `com.ctrip.framework.apollo.configservice.ConfigServiceApplication` > `com.ctrip.framework.apollo.adminservice.AdminServiceApplication` ### 2.1.3 VM options配置 ![ApolloApplication-VM-Options](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/local-development/ApolloApplication-VM-Options.png) ``` -Dapollo_profile=github,auth ``` >注1:这里指定了apollo_profile是`github`和`auth`,其中`github`是Apollo必须的一个profile,用于数据库的配置,`auth`是从0.9.0新增的,用来支持使用apollo提供的Spring Security简单认证,更多信息可以参考[Portal-实现用户登录功能](zh/development/portal-how-to-implement-user-login-function) > >注2:如果需要使用 mysql 数据库,添加`spring.config-datasource.*` 和 `spring.portal-datasource.*` 相关配置, > your-mysql-server:3306 需要替换为实际的 mysql 服务器地址和端口, > ApolloConfigDB 和 ApolloPortalDB 需要替换为实际的数据库名称, > apollo-username 和 apollo-password 需要替换为实际的用户名和密码 ![ApolloApplication-Mysql-VM-Options](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/local-development/ApolloApplication-Mysql-VM-Options.png) ``` -Dspring.config-datasource.url=jdbc:mysql://your-mysql-server:3306/ApolloConfigDB?useUnicode=true&characterEncoding=UTF8 -Dspring.config-datasource.username=apollo-username -Dspring.config-datasource.password=apollo-password -Dspring.portal-datasource.url=jdbc:mysql://your-mysql-server:3306/ApolloPortalDB?useUnicode=true&characterEncoding=UTF8 -Dspring.portal-datasource.username=apollo-username -Dspring.portal-datasource.password=apollo-password ``` mysql 数据库初始化脚本见 本项目 scripts/sql/profiles/mysql-default 目录下的文件 [apolloconfigdb.sql](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/scripts/sql/profiles/mysql-default/apolloconfigdb.sql) [apolloportaldb.sql](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/scripts/sql/profiles/mysql-default/apolloportaldb.sql) >注3:程序默认日志输出为/opt/logs/apollo-assembly.log,如果需要修改日志文件路径,可以增加`logging.file.name`参数,如下: > >-Dlogging.file.name=/your-path/apollo-assembly.log ### 2.1.4 运行 对新建的运行配置点击Run或Debug皆可。 ![ApolloApplication-Run](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/local-development/ApolloApplication-Run.png) 启动完后,打开[http://localhost:8080](http://localhost:8080)可以看到`apollo-configservice`和`apollo-adminservice`都已经启动完成并注册到Eureka。 ![ConfigAdminApplication-Eureka](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/local-development/ConfigAdminApplication-Eureka.png) > 注:除了在Eureka确认服务状态外,还可以通过健康检查接口确认服务健康状况: > > apollo-adminservice: [http://localhost:8090/health](http://localhost:8090/health) > apollo-configservice: [http://localhost:8080/health](http://localhost:8080/health) > > 如果服务健康,返回内容中的status.code应当为`UP`: > > { > "status": { > "code": "UP", > ... > }, > ... > } 启动完后,打开[http://localhost:8070](http://localhost:8070)就可以看到Apollo配置中心界面了。 ![PortalApplication-Home](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/local-development/PortalApplication-Home.png) >注:如果启用了`auth` profile的话,默认的用户名是apollo,密码是admin ### 2.1.5 Demo应用接入 为了更好的开发和调试,一般我们都会自己创建一个demo项目给自己使用。 可以参考[一、普通应用接入指南](zh/portal/apollo-user-guide#一、普通应用接入指南)创建自己的demo项目。 ## 2.2 Java样例客户端启动 仓库中有一个样例客户端的项目:[apollo-demo-java](https://github.com/apolloconfig/apollo-demo-java),下面以Intellij为例来说明如何在本地启动。 ### 2.2.1 配置项目AppId 在`2.2.5 Demo应用接入`中创建Demo项目时,系统会要求填入一个全局唯一的AppId,我们需要把这个AppId配置到`apollo-demo`项目的app.properties文件中:`apollo-demo-java/api-demo/src/main/resources/META-INF/app.properties`。 ![apollo-demo-app-properties](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/local-development/apollo-demo-app-properties.jpg) 如我们自己的demo项目使用的AppId是100004458,那么文件内容就是: app.id=100004458 >注:AppId是应用的唯一身份标识,Apollo客户端使用这个标识来获取应用自己的私有Namespace配置。 > 对于公共Namespace的配置,没有AppId也可以获取到配置,但是就失去了应用覆盖公共Namespace配置的能力。 > 更多配置AppId的方式可以参考[1.2.1 AppId](zh/client/java-sdk-user-guide#_121-appid) ### 2.2.2 新建运行配置 ![NewConfiguration-Application](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/local-development/NewConfiguration-Application.png) ### 2.2.3 Main class配置 `com.apolloconfig.apollo.demo.api.SimpleApolloConfigDemo` ### 2.2.4 VM options配置 ![apollo-demo-vm-options](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/local-development/apollo-demo-vm-options.jpg) -Dapollo.meta=http://localhost:8080 > 注:这里当前环境的meta server地址为`http://localhost:8080`,也就是`apollo-configservice`的地址。 > 更多配置Apollo Meta Server的方式可以参考[1.2.2 Apollo Meta Server](zh/client/java-sdk-user-guide#_122-apollo-meta-server) ### 2.2.5 概览 ![apollo-demo-overview](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/local-development/apollo-demo-overview.jpg) ### 2.2.6 运行 对新建的运行配置点击Run或Debug皆可。 ![apollo-demo-run](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/local-development/apollo-demo-run.png) 启动完后,忽略前面的调试信息,可以看到如下提示: Apollo Config Demo. Please input key to get the value. Input quit to exit. > 输入你之前在Portal上配置的值,如我们的Demo项目中配置了`timeout`,会看到如下信息: > timeout > [SimpleApolloConfigDemo] Loading key : timeout with value: 100 > 客户端日志级别默认是`DEBUG`,如果需要调整,可以通过修改`apollo-demo/src/main/resources/log4j2.xml`中的level配置 > ```xml > > > > ``` ## 2.3 .Net样例客户端启动 [apollo.net](https://github.com/ctripcorp/apollo.net)项目中有一个样例客户端的项目:`ApolloDemo`,下面就以VS 2010为例来说明如何在本地启动。 ### 2.3.1 配置项目AppId 在`2.2.5 Demo应用接入`中创建Demo项目时,系统会要求填入一个全局唯一的AppId,我们需要把这个AppId配置到`ApolloDemo`项目的APP.config文件中:`apollo.net\ApolloDemo\App.config`。 ![apollo-demo-app-config](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/apollo-net-app-config.png) 如我们自己的demo项目使用的AppId是100004458,那么文件内容就是: ```xml ``` >注:AppId是应用的唯一身份标识,Apollo客户端使用这个标识来获取应用自己的私有Namespace配置。 > 对于公共Namespace的配置,没有AppId也可以获取到配置,但是就失去了应用覆盖公共Namespace配置的能力。 ### 2.3.2 配置服务地址 Apollo客户端针对不同的环境会从不同的服务器获取配置,所以我们需要在app.config或web.config配置服务器地址(Apollo.{ENV}.Meta)。假设DEV环境的配置服务(apollo-configservice)地址是11.22.33.44,那么我们就做如下配置: ![apollo-net-server-url-config](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/apollo-net-server-url-config.png) ### 2.3.3 运行 运行`ApolloConfigDemo.cs`即可。 启动完后,忽略前面的调试信息,可以看到如下提示: Apollo Config Demo. Please input key to get the value. Input quit to exit. > 输入你之前在Portal上配置的值,如我们的Demo项目中配置了`timeout`,会看到如下信息: > timeout > Loading key: timeout with value: 100 >注:Apollo .Net客户端开源版目前默认会把日志直接输出到Console,大家可以自己实现Logging相关功能。 > > 详见[https://github.com/ctripcorp/apollo.net/tree/master/Apollo/Logging/Spi](https://github.com/ctripcorp/apollo.net/tree/master/Apollo/Logging/Spi) # 三、开发 ## 模块依赖图 ![模块依赖图](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/module-dependency.png) ## 3.1 Portal 实现用户登录功能 请参考[Portal 实现用户登录功能](zh/extension/portal-how-to-implement-user-login-function) ## 3.2 Portal 接入邮件服务 请参考[Portal 接入邮件服务](zh/extension/portal-how-to-enable-email-service) ## 3.3 Portal 集群部署时共享 session 请参考[Portal 共享 session](zh/extension/portal-how-to-enable-session-store) ================================================ FILE: docs/zh/contribution/apollo-release-guide.md ================================================ # Apollo 发布指南(Skill 自动化版) 本文档面向在 Codex / Claude Code 中通过自然语言触发 Skill 的发布方式。 目标是:减少手工步骤、统一发布质量,并在关键外部动作前保留人工确认。 ## 1. 适用范围与推荐顺序 Apollo 发布流程由 3 个 Skill 覆盖: 1. `apollo-java-release`:发布 `apolloconfig/apollo-java` 2. `apollo-release`:发布 `apolloconfig/apollo` 3. `apollo-helm-chart-release`:发布 `apolloconfig/apollo-helm-chart` 推荐顺序: 1. 先发布 `apollo-java-release`(不依赖其它发布流程) 2. 再发布 `apollo-release`(通常依赖本次 Java SDK 版本) 3. 最后发布 `apollo-helm-chart-release` > 若本次发布不涉及某个仓库,可跳过对应子流程。 ## 2. 发布前准备 ### 2.1 权限与工具 - GitHub 账号具备对应仓库的 PR、Release、Workflow、Discussion、Milestone 权限 - 本地可用命令:`git`、`gh`、`python3`、`jq` - Helm Chart 发布额外需要:`helm` ### 2.2 仓库与分支状态 - 在每个目标仓库执行前,确保工作区干净(无未提交变更) - 确认基线分支最新(`apollo` 默认 `master`,其余仓库按各自默认分支) ### 2.3 建议提前准备的发布输入 - 发布版本号(如 `2.8.0`) - 下一开发版本(如 `2.9.0-SNAPSHOT`) - Highlights 对应 PR 列表(如 `PR_ID_1,PR_ID_2,PR_ID_3`) ### 2.4 安装发布 Skill(Codex / Claude Code) 在使用本文档后续流程前,请先安装这 3 个 Skill: - `apollo-java-release` - `apollo-release` - `apollo-helm-chart-release` 推荐方式(自然语言): 1. 在 Codex 会话中使用 `skill-installer` 2. 安装来源指定为完整 GitHub 地址:`https://github.com/apolloconfig/apollo-skills` 3. 安装上述 3 个 Skill 示例表达(自然语言): - “请用 `skill-installer` 从 `https://github.com/apolloconfig/apollo-skills` 安装 `apollo-java-release`、`apollo-release`、`apollo-helm-chart-release`” 如需手动安装,也可以将这 3 个 Skill 目录放到本地 Skill 目录(通常为 `$CODEX_HOME/skills` 或 `~/.codex/skills`)后重启客户端。 ## 3. 发布 Apollo Java(apollo-java-release) ### 3.1 触发方式(自然语言) 在 `apollo-java` 仓库会话中,直接发起类似请求: - “使用 `apollo-java-release` 发布 `X.Y.Z`,下一个版本 `A.B.C-SNAPSHOT`,highlights 用这些 PR:`...`” ### 3.2 Skill 会自动完成 - 版本变更 PR(`revision` 从 SNAPSHOT 到正式版本) - pre-release 创建 - 触发 `release.yml`,等待 Sonatype Central 发布完成 - 发布公告讨论(Announcements) - 发布后回切到下一 SNAPSHOT,并创建 post-release PR - pre-release 转正式 release ### 3.3 checkpoint 交互方式 - Skill 在关键外部动作前会自动暂停,并由系统提示是否继续 - 你只需在对话中选择继续,或要求先修改文案/参数 ## 4. 发布 Apollo Server(apollo-release) ### 4.1 触发方式(自然语言) 在 `apollo` 仓库会话中,直接发起类似请求: - “使用 `apollo-release` 发布 `X.Y.Z`,下一个版本 `A.B.C-SNAPSHOT`,highlights PR 是 `...`” ### 4.2 Skill 会自动完成 - 版本 bump PR(`pom.xml` 的 `revision`) - 从 `CHANGES.md` 生成 Release Notes 与公告草稿 - 创建 pre-release(`vX.Y.Z`) - 触发包构建与 checksum 上传(GitHub Action) - 触发 Docker 发布 workflow - pre-release 转正式 release - 发布 Announcements 讨论 - 发布后回切 `next-snapshot`、归档 `CHANGES.md`、里程碑维护、创建 post-release PR ### 4.3 checkpoint 交互方式 与 `apollo-java-release` 一致: - 系统会在关键步骤提示你确认是否继续 - 可在暂停点要求先调整 highlights、release note 或其它参数 ## 5. 发布 Helm Chart(apollo-helm-chart-release) ### 5.1 触发方式(自然语言) 在 `apollo-helm-chart` 仓库会话中,直接发起类似请求: - “使用 `apollo-helm-chart-release` 发布当前 chart 版本变更” ### 5.2 Skill 会自动完成 - chart 版本变更检测与一致性校验 - `helm lint`、`helm package`、`helm repo index` - 变更白名单校验(防止误提交) - 生成分支与 commit 草案 - 在 push / PR 前停下来等待你确认(默认不自动发布) ## 6. 发布后统一验收 建议至少检查: 1. Apollo Release 页面包含 3 个 zip + 3 个 sha1 2. Docker 镜像 tag 可用 3. Maven Central 上 apollo-java 对应版本可检索 4. Helm 仓库 `docs/index.yaml` 已包含新 chart 版本 5. 关键路径冒烟验证通过(配置发布、灰度发布、客户端拉取、Portal 核心操作) ## 7. 常见操作(Skill 使用视角) ### 7.1 中断后继续 直接在对话中要求继续即可,例如: - “继续刚才的发布流程” - “从下一个 checkpoint 继续执行” Skill 会根据状态文件自动恢复,不会重复已完成步骤。 ### 7.2 先演练再正式发布 可先要求 dry-run,例如: - “先用 dry-run 跑一遍 `apollo-release`,我先检查流程” 确认输出后,再要求正式执行。 ### 7.3 调整 Highlights / 文案 在 pre-release 创建前可直接提出调整,例如: - “把 highlights 改成这些 PR:`...`,然后重新生成 release notes” 确认无误后再继续下一步。 ================================================ FILE: docs/zh/deployment/deployment-architecture.md ================================================ #   # 一、介绍 根据不同的场景,apolloconfig部署的架构会有很多种,这里不讨论细节,仅从部署架构的宏观角度,来介绍各种部署的方案 ## 1.1 flowchart 用flowchart来表达部署方式,这里先介绍一些基本的概念 ### 1.1.1 依赖关系 依赖关系用 ```mermaid graph LR 1 --> 2 ``` 表示1依赖2,也就是2必须存在,1才可以正常工作,例如 ```mermaid flowchart LR 应用 --> MySQL ``` 表示应用需要使用MySQL才可以正常工作 依赖关系可能会比较复杂,以及存在多层级的依赖,例如 ```mermaid flowchart LR 服务A --> 注册中心 服务A --> 服务B --> MySQL 服务A --> Redis ``` 服务A需要注册中心,服务B,Redis 并且服务B需要MySQL ### 1.1.2 包含关系 包含关系用 ```mermaid graph subgraph a b end ``` 表示a包含b,也就是b是a的一部分,包含关系可能会出现嵌套的情况,例如 ```mermaid flowchart LR subgraph Linux-Server subgraph JVM1 Thread1.1 Thread1.2 end subgraph JVM2 Thread2.1 end MySQL Redis end ``` 表示在一台Linux服务器上,运行着MySQL,Redis,2个JVM,JVM里分别又存在Thread # 二、单机 单机部署的场景通常是新手学习,或者公司内部对性能要求不高的测试环境,不适用于生产环境 ## 2.1 单机,单环境 All In One 这是最简单,部署起来最方便的单机部署方式 需要: * 1台Linux服务器:有JRE * 2个database:1个PortalDB和ConfigDB 如下图,所有模块部署在同一台Linux机器上,总共有3个JVM进程 ```mermaid flowchart LR m[Meta Server] e[Eureka] c[Config Service] a[Admin Service] p[Portal] configdb[(ConfigDB)] portaldb[(PortalDB)] subgraph Linux Server subgraph JVM8080 m e c end subgraph JVM8090 a end subgraph JVM8070 p end end c --> configdb a --> configdb p --> portaldb ``` JVM8080:对外暴露的网络端口是8080,里面有Meta Server,Eureka,Config Service,其中Config Service又使用了ConfigDB JVM8090:对外暴露的网络端口是8090,里面有Admin Service,并且Admin Service使用了ConfigDB JVM8070:对外暴露的网络端口是8070,里面有Portal,并且Portal使用了PortalDB 如果加入模块之间的依赖,flowchart会变成 ```mermaid flowchart LR m[Meta Server] e[Eureka] c[Config Service] a[Admin Service] p[Portal] configdb[(ConfigDB)] portaldb[(PortalDB)] subgraph Linux Server subgraph JVM8080 m e c end subgraph JVM8090 a end subgraph JVM8070 p end end c --> configdb a --> configdb p --> portaldb m --> e c --> e a --> e p --> m p --> a ``` Config Service和Admin Service会把自己注册到Eureka上 Portal通过Meta Server服务发现Admin Service 为了flowchart看起来更加简洁,可以只表示进程之间的依赖关系 ```mermaid flowchart LR m[Meta Server] e[Eureka] c[Config Service] a[Admin Service] p[Portal] configdb[(ConfigDB)] portaldb[(PortalDB)] subgraph Linux Server subgraph JVM8080 m e c end subgraph JVM8090 a end subgraph JVM8070 p end end JVM8080 --> configdb JVM8090 --> configdb JVM8070 --> portaldb JVM8090 --> JVM8080 JVM8070 --> JVM8090 ``` 进程JVM8070依赖进程JVM8090和PortalDB 进程JVM8090依赖进程JVM8080和ConfigDB 进程JVM8080依赖ConfigDB ## 2.2 单机,单环境 分开部署 ### 2.2.1 单机,单环境 分开部署 3台Linux服务器 3个JVM进程也可以分散到3台Linux机器上 需要: * 3台Linux服务器:分别部署3个进程 * 2个database ```mermaid flowchart LR m[Meta Server] e[Eureka] c[Config Service] a[Admin Service] p[Portal] configdb[(ConfigDB)] portaldb[(PortalDB)] subgraph Linux Server 1 subgraph JVM8080 m e c end end subgraph Linux Server 2 subgraph JVM8090 a end end subgraph Linux Server 3 subgraph JVM8070 p end end JVM8080 --> configdb JVM8090 --> configdb JVM8070 --> portaldb JVM8090 --> JVM8080 JVM8070 --> JVM8090 ``` ### 2.2.2 单机,单环境 分开部署 2台Linux服务器 不过通常我们会把Config Service和Admin Service部署在一台Linux服务器上 需要: * 2台Linux服务器:1台部署Portal,另一台部署Config Service和Admin Service * 2个database ```mermaid flowchart LR m[Meta Server] e[Eureka] c[Config Service] a[Admin Service] p[Portal] configdb[(ConfigDB)] portaldb[(PortalDB)] subgraph Linux Server 1 subgraph JVM8080 m e c end subgraph JVM8090 a end end subgraph Linux Server 2 subgraph JVM8070 p end end JVM8080 --> configdb JVM8090 --> configdb JVM8070 --> portaldb JVM8090 --> JVM8080 JVM8070 --> JVM8090 ``` 后续为了flowchart更简洁,将JVM8080里的内容进行简化,只显示Config Service,里面的Meta Server和Eureka不再显示 ```mermaid flowchart LR subgraph JVM8080 m[Meta Server] e[Eureka] c[Config Service] end subgraph new-JVM8080[JVM8080] new-c[Config Service] end JVM8080 --> |simplify| new-JVM8080 ``` 所以部署架构可以简化表示成 ```mermaid flowchart LR c[Config Service] a[Admin Service] p[Portal] configdb[(ConfigDB)] portaldb[(PortalDB)] subgraph Linux Server 1 subgraph JVM8080 c end subgraph JVM8090 a end end subgraph Linux Server 2 subgraph JVM8070 p end end JVM8080 --> configdb JVM8090 --> configdb JVM8070 --> portaldb JVM8090 --> JVM8080 JVM8070 --> JVM8090 ``` ## 2.3 单机,双环境 单个环境基本没法满足实际的应用场景,例如公司里有SIT测试环境和UAT测试环境,此时需要部署2个环境提供配置服务 很容易想到的部署架构如下,把单机,单环境的部署架构重复2次即可 需要: * 2台Linux服务器 * 4个database ```mermaid flowchart LR subgraph SIT c1[SIT Config Service] a1[SIT Admin Service] p1[SIT Portal] configdb1[(SIT ConfigDB)] portaldb1[(SIT PortalDB)] subgraph SIT Linux Server subgraph sit-jvm-8080[SIT JVM8080] c1 end subgraph sit-jvm-8090[SIT JVM8090] a1 end subgraph sit-jvm-8070[SIT JVM8070] p1 end end sit-jvm-8080 --> configdb1 sit-jvm-8090 --> configdb1 sit-jvm-8070 --> portaldb1 sit-jvm-8090 --> sit-jvm-8080 sit-jvm-8070 --> sit-jvm-8090 end subgraph UAT c2[UAT Config Service] a2[UAT Admin Service] p2[UAT Portal] configdb2[(UAT ConfigDB)] portaldb2[(UAT PortalDB)] subgraph UAT Linux Server subgraph uat-jvm-8080[UAT JVM8080] c2 end subgraph uat-jvm-8090[UAT JVM8090] a2 end subgraph uat-jvm-8070[UAT JVM8070] p2 end end uat-jvm-8080 --> configdb2 uat-jvm-8090 --> configdb2 uat-jvm-8070 --> portaldb2 uat-jvm-8090 --> uat-jvm-8080 uat-jvm-8070 --> uat-jvm-8090 end ``` 但是这种方案,会存在2个Portal界面,没法1个界面管理2个环境,使用体验不是很好,Portal实际上可以只部署1套,推荐的部署架构如下 * 3台Linux服务器: * Portal Linux Server单独部署Portal * SIT Linux Server部署SIT的Config Service和Admin Service * UAT Linux Server部署UAT的Config Service和Admin Service * 3个database:1个PortalDB + 1个SIT的ConfigDB + 1个UAT的ConfigDB ```mermaid flowchart LR p[Portal] portaldb[PortalDB] p --> portaldb subgraph Portal Linux Server subgraph JVM8070 p end end subgraph SIT c1[SIT Config Service] a1[SIT Admin Service] configdb1[(SIT ConfigDB)] subgraph SIT Linux Server subgraph sit-jvm-8080[SIT JVM8080] c1 end subgraph sit-jvm-8090[SIT JVM8090] a1 end end sit-jvm-8080 --> configdb1 sit-jvm-8090 --> configdb1 sit-jvm-8090 --> sit-jvm-8080 end subgraph UAT c2[UAT Config Service] a2[UAT Admin Service] configdb2[(UAT ConfigDB)] subgraph UAT Linux Server subgraph uat-jvm-8080[UAT JVM8080] c2 end subgraph uat-jvm-8090[UAT JVM8090] a2 end end uat-jvm-8080 --> configdb2 uat-jvm-8090 --> configdb2 uat-jvm-8090 --> uat-jvm-8080 end JVM8070 --> sit-jvm-8090 JVM8070 --> uat-jvm-8090 ``` ## 2.4 单机,三个环境 假设现在需要满足SIT、UAT、PP这3个环境的使用场景, 在之前双环境的基础之上,再多加1台PP环境的Linux服务和ConfigDB即可,Portal通过修改配置的方式,来管理这3个环境 ```mermaid flowchart LR p[Portal] portaldb[PortalDB] p --> portaldb subgraph Portal Linux Server subgraph JVM8070 p end end subgraph SIT c1[SIT Config Service] a1[SIT Admin Service] configdb1[(SIT ConfigDB)] subgraph SIT Linux Server subgraph sit-jvm-8080[SIT JVM8080] c1 end subgraph sit-jvm-8090[SIT JVM8090] a1 end end sit-jvm-8080 --> configdb1 sit-jvm-8090 --> configdb1 sit-jvm-8090 --> sit-jvm-8080 end subgraph UAT c2[UAT Config Service] a2[UAT Admin Service] configdb2[(UAT ConfigDB)] subgraph UAT Linux Server subgraph uat-jvm-8080[UAT JVM8080] c2 end subgraph uat-jvm-8090[UAT JVM8090] a2 end end uat-jvm-8080 --> configdb2 uat-jvm-8090 --> configdb2 uat-jvm-8090 --> uat-jvm-8080 end subgraph PP c3[PP Config Service] a3[PP Admin Service] configdb3[(PP ConfigDB)] subgraph PP Linux Server subgraph pp-jvm-8080[PP JVM8080] c3 end subgraph pp-jvm-8090[PP JVM8090] a3 end end pp-jvm-8080 --> configdb3 pp-jvm-8090 --> configdb3 pp-jvm-8090 --> pp-jvm-8080 end JVM8070 --> sit-jvm-8090 JVM8070 --> uat-jvm-8090 JVM8070 --> pp-jvm-8090 ``` ## 2.5 单机,多个环境 原理同上,每个环境1台Linux服务器+1个ConfigDB 然后Portal添加新环境的信息即可 # 三、高可用 1个环境只有1个Config Service进程,无法满足高可用,为了避免单点宕机后影响系统的可用性,需要多实例部署,也就是部署多个Java进程在不同的Linux服务器上 ## 3.1 最简高可用,单环境 回到常见的非高可用部署方式, ```mermaid flowchart LR c[Config Service] a[Admin Service] p[Portal] configdb[(ConfigDB)] portaldb[(PortalDB)] subgraph Linux Server 1 subgraph JVM8080 c end subgraph JVM8090 a end end subgraph Linux Server 2 subgraph JVM8070 p end end JVM8080 --> configdb JVM8090 --> configdb JVM8070 --> portaldb JVM8090 --> JVM8080 JVM8070 --> JVM8090 ``` 当Linux Server 1宕机时,client就只能读取本地磁盘上的config-cache了,如果需要防止单台Linux宕机导致Config Service不可用,可以尝试再新增1台Linux机器 需要 * 3台Linux服务器:1台部署Portal,另外2台分别部署Config Service和Admin Service * 2个database ```mermaid flowchart LR c-1[Config Service] c-2[Config Service] a-1[Admin Service] a-2[Admin Service] p[Portal] configdb[(ConfigDB)] portaldb[(PortalDB)] JVM8080-1[JVM8080] JVM8080-2[JVM8080] JVM8090-1[JVM8090] JVM8090-2[JVM8090] subgraph Linux Server 1.1 subgraph JVM8080-1[JVM8080] c-1 end subgraph JVM8090-1[JVM8090] a-1 end end subgraph Linux Server 1.2 subgraph JVM8080-2[JVM8080] c-2 end subgraph JVM8090-2[JVM8090] a-2 end end subgraph Linux Server 2 subgraph JVM8070 p end end JVM8080-1 --> configdb JVM8090-1 --> configdb JVM8080-2 --> configdb JVM8090-2 --> configdb JVM8070 --> portaldb JVM8090-1 --> JVM8080-1 JVM8090-2 --> JVM8080-2 JVM8070 --> JVM8090-1 JVM8070 --> JVM8090-2 ``` 这种部署方式下,Linux Server 1.1 或者 Linux Server 1.2宕机,系统仍旧可用, ## 3.2 高可用,单环境 在上述的基础上,如果client的数量有很多(例如上万个Java进程),可以横向扩展Config Service,引入Linux Server 1.3, Linux Server 1.4, ... Admin Service由于只有Portal访问,在数量上可以比Config Service少很多 具体如何评定Config Service的数量,请参考 [Apollo性能测试报告](zh/misc/apollo-benchmark.md) ## 3.3 高可用,双环境 如[2.3 单机,双环境](#_23-单机,双环境)种,如果想让SIT和UAT都变成高可用,只需要分别在环境中再添加机器即可,如下图,每个环境中各有2台Linux Server,如果有性能上需求,可以再在每个环境中,使用更多的机器来部署Config Service即可 ```mermaid flowchart LR p[Portal] portaldb[(PortalDB)] p --> portaldb subgraph Portal Linux Server subgraph JVM8070 p end end subgraph SIT sit-c1[SIT Config Service] sit-a1[SIT Admin Service] sit-c2[SIT Config Service] sit-a2[SIT Admin Service] sit-configdb[(SIT ConfigDB)] subgraph SIT Linux Server 2.1 subgraph sit-c1-jvm-8080[SIT JVM8080] sit-c1 end subgraph sit-c1-jvm-8090[SIT JVM8090] sit-a1 end end subgraph SIT Linux Server 2.2 subgraph sit-c2-jvm-8080[SIT JVM8080] sit-c2 end subgraph sit-c2-jvm-8090[SIT JVM8090] sit-a2 end end sit-c1-jvm-8080 --> sit-configdb sit-c1-jvm-8090 --> sit-configdb sit-c2-jvm-8080 --> sit-configdb sit-c2-jvm-8090 --> sit-configdb sit-c1-jvm-8090 --> sit-c1-jvm-8080 sit-c2-jvm-8090 --> sit-c2-jvm-8080 end subgraph UAT uat-c1[UAT Config Service] uat-a1[UAT Admin Service] uat-c2[UAT Config Service] uat-a2[UAT Admin Service] uat-configdb[(UAT ConfigDB)] subgraph UAT Linux Server 2.1 subgraph uat-c1-jvm-8080[UAT JVM8080] uat-c1 end subgraph uat-c1-jvm-8090[UAT JVM8090] uat-a1 end end subgraph UAT Linux Server 2.2 subgraph uat-c2-jvm-8080[UAT JVM8080] uat-c2 end subgraph uat-c2-jvm-8090[UAT JVM8090] uat-a2 end end uat-c1-jvm-8080 --> uat-configdb uat-c1-jvm-8090 --> uat-configdb uat-c2-jvm-8080 --> uat-configdb uat-c2-jvm-8090 --> uat-configdb uat-c1-jvm-8090 --> uat-c1-jvm-8080 uat-c2-jvm-8090 --> uat-c2-jvm-8080 end JVM8070 --> sit-c1-jvm-8090 JVM8070 --> sit-c2-jvm-8090 JVM8070 --> uat-c1-jvm-8090 JVM8070 --> uat-c2-jvm-8090 ``` ## 3.4 高可用,多个环境 在上述的基础上,如果要添加一个环境,例如BETA环境,需要新增2台及以上的Linux服务器+1个ConfigDB Portal添加新环境的信息,指向BETA环境的apollo.meta ## 3.5 高可用,单环境,单机房 实际生产环境中,很多公司和测试环境进行了隔离,所以生产环境属于单环境,只有一个PRO环境 在只有1个机房时,参考 [3.2 高可用,单环境](#_32-高可用,单环境) ## 3.6 高可用,单环境,双机房 如果有2个机房,通常机房之间存在网络隔离,如果是同城机房,idc1和idc2,可以采用如下的部署方式 ```mermaid flowchart LR idc1-p[idc1 Portal] idc2-p[idc2 Portal] portaldb[(PortalDB)] idc1-p --> portaldb idc2-p --> portaldb configdb[(ConfigDB)] idc1-c1-jvm-8080 --> configdb idc1-c1-jvm-8090 --> configdb idc1-c2-jvm-8080 --> configdb idc1-c2-jvm-8090 --> configdb idc2-c1-jvm-8080 --> configdb idc2-c1-jvm-8090 --> configdb idc2-c2-jvm-8080 --> configdb idc2-c2-jvm-8090 --> configdb subgraph idc1 subgraph idc1 Portal Linux Server subgraph idc1-JVM8070 idc1-p end end idc1-c1[idc1 Config Service] idc1-a1[idc1 Admin Service] idc1-c2[idc1 Config Service] idc1-a2[idc1 Admin Service] subgraph idc1 Linux Server 2.1 subgraph idc1-c1-jvm-8080[idc1 JVM8080] idc1-c1 end subgraph idc1-c1-jvm-8090[idc1 JVM8090] idc1-a1 end end subgraph idc1 Linux Server 2.2 subgraph idc1-c2-jvm-8080[idc1 JVM8080] idc1-c2 end subgraph idc1-c2-jvm-8090[idc1 JVM8090] idc1-a2 end end idc1-c1-jvm-8090 --> idc1-c1-jvm-8080 idc1-c2-jvm-8090 --> idc1-c2-jvm-8080 end subgraph idc2 subgraph idc2 Portal Linux Server subgraph idc2-JVM8070 idc2-p end end idc2-c1[idc2 Config Service] idc2-a1[idc2 Admin Service] idc2-c2[idc2 Config Service] idc2-a2[idc2 Admin Service] subgraph idc2 Linux Server 2.1 subgraph idc2-c1-jvm-8080[idc2 JVM8080] idc2-c1 end subgraph idc2-c1-jvm-8090[idc2 JVM8090] idc2-a1 end end subgraph idc2 Linux Server 2.2 subgraph idc2-c2-jvm-8080[idc2 JVM8080] idc2-c2 end subgraph idc2-c2-jvm-8090[idc2 JVM8090] idc2-a2 end end idc2-c1-jvm-8090 --> idc2-c1-jvm-8080 idc2-c2-jvm-8090 --> idc2-c2-jvm-8080 end idc1-JVM8070 --> idc1-c1-jvm-8090 idc1-JVM8070 --> idc1-c2-jvm-8090 idc2-JVM8070 --> idc2-c1-jvm-8090 idc2-JVM8070 --> idc2-c2-jvm-8090 ``` 每个机房有自己的一套Portal, Config Service, Admin Service 对于ConfigDB,在同城双机房下,连接的ConfigDB是同一个,不存在2个不同的ConfigDB,对于PortalDB也是如此,需要连接同一个 ConfigDB和PortalDB在图中没有放入idc1或者idc2,需要自行选用合适的MySQL架构以及部署方式 # 四、部署图 ## 4.1 ctrip 以ctrip为例,我们的部署策略如下: ![Deployment](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/apollo-deployment.png) * Portal部署在生产环境的机房,通过它来直接管理FAT、UAT、PRO等环境的配置 * Meta Server、Config Service和Admin Service在每个环境都单独部署,使用独立的数据库 * Meta Server、Config Service和Admin Service在生产环境部署在两个机房,实现双活 * Meta Server和Config Service部署在同一个JVM进程内,Admin Service部署在同一台服务器的另一个JVM进程内 ## 4.2 样例部署图 [@lyliyongblue](https://github.com/lyliyongblue) 贡献的样例部署图(建议右键新窗口打开看大图): ![Deployment](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/lyliyongblue-apollo-deployment.png) ================================================ FILE: docs/zh/deployment/distributed-deployment-guide.md ================================================ 本文档介绍了如何按照分布式部署的方式编译、打包、部署Apollo配置中心,从而可以在开发、测试、生产等环境分别部署运行。 > 如果只是需要在本地快速部署试用Apollo的话,可以参考[Quick Start](zh/deployment/quick-start) #   # 一、准备工作 ## 1.1 运行时环境 ### 1.1.1 OS 服务端基于Spring Boot,启动脚本理论上支持所有Linux发行版,建议[CentOS 7](https://www.centos.org/)。 ### 1.1.2 Java * Apollo服务端:17+ * Apollo客户端:1.8+ * 如需运行在 Java 1.7 运行时环境,请使用 1.x 版本的 apollo 客户端,如 1.9.1 在配置好后,可以通过如下命令检查: ```sh java -version ``` 样例输出: ```sh java version "17.0.14" Java(TM) SE Runtime Environment (build 17.0.14+7) Java HotSpot(TM) 64-Bit Server VM (build 17.0.14+7, mixed mode) ``` ## 1.2 MySQL * 版本要求:5.6.5+ Apollo的表结构对`timestamp`使用了多个default声明,所以需要5.6.5以上版本。 连接上MySQL后,可以通过如下命令检查: ```sql SHOW VARIABLES WHERE Variable_name = 'version'; ``` | Variable_name | Value | |---------------|--------| | version | 5.7.11 | > 注1:MySQL版本可以降级到5.5,详见[mysql 依赖降级讨论](https://github.com/apolloconfig/apollo/issues/481)。 > 注2:如果希望使用Oracle的话,可以参考[vanpersl](https://github.com/vanpersl)在Apollo 0.8.0基础上开发的[Oracle适配代码](https://github.com/apolloconfig/apollo/compare/v0.8.0...vanpersl:db-oracle),Oracle版本为10.2.0.1.0。 > 注3:如果希望使用Postgres的话,可以参考[oaksharks](https://github.com/oaksharks)在Apollo 0.9.1基础上开发的[Pg适配代码](https://github.com/oaksharks/apollo/compare/ac10768ee2e11c488523ca0e845984f6f71499ac...oaksharks:pg),Postgres的版本为9.3.20,也可以参考[xiao0yy](https://github.com/xiao0yy)在Apollo 0.10.2基础上开发的[Pg适配代码](https://github.com/apolloconfig/apollo/issues/1293),Postgres的版本为9.5。 ## 1.3 环境 分布式部署需要事先确定部署的环境以及部署方式。 Apollo目前支持以下环境: * DEV * 开发环境 * FAT * 测试环境,相当于alpha环境(功能测试) * UAT * 集成环境,相当于beta环境(回归测试) * PRO * 生产环境 > 如果希望添加自定义的环境名称,具体步骤可以参考[Portal如何增加环境](zh/faq/common-issues-in-deployment-and-development-phase?id=_4-portal如何增加环境?) > 请注意,如果自定义的环境名称为 PROD,会被强制转换为 PRO。FWS 会被强制转换为 FAT。 可以参考 [部署架构](zh/deployment/deployment-architecture.md) ## 1.4 网络策略 分布式部署的时候,`apollo-configservice`和`apollo-adminservice`需要把自己的IP和端口注册到Meta Server(apollo-configservice本身)。 Apollo客户端和Portal会从Meta Server获取服务的地址(IP+端口),然后通过服务地址直接访问。 需要注意的是,`apollo-configservice`和`apollo-adminservice`是基于内网可信网络设计的,所以出于安全考虑,**请不要将`apollo-configservice`和`apollo-adminservice`直接暴露在公网**。 所以如果实际部署的机器有多块网卡(如docker),或者存在某些网卡的IP是Apollo客户端和Portal无法访问的(如网络安全限制),那么我们就需要在`apollo-configservice`和`apollo-adminservice`中做相关配置来解决连通性问题。 ### 1.4.1 忽略某些网卡 可以分别修改`apollo-configservice`和`apollo-adminservice`的startup.sh,通过JVM System Property传入-D参数,也可以通过OS Environment Variable传入,下面的例子会把`docker0`和`veth`开头的网卡在注册到Eureka时忽略掉。 JVM System Property示例: ```properties -Dspring.cloud.inetutils.ignoredInterfaces[0]=docker0 -Dspring.cloud.inetutils.ignoredInterfaces[1]=veth.* ``` OS Environment Variable示例: ```properties SPRING_CLOUD_INETUTILS_IGNORED_INTERFACES[0]=docker0 SPRING_CLOUD_INETUTILS_IGNORED_INTERFACES[1]=veth.* ``` ### 1.4.2 指定要注册的IP 可以分别修改`apollo-configservice`和`apollo-adminservice`的startup.sh,通过JVM System Property传入-D参数,也可以通过OS Environment Variable传入,下面的例子会指定注册的IP为`1.2.3.4`。 JVM System Property示例: ```properties -Deureka.instance.ip-address=1.2.3.4 ``` OS Environment Variable示例: ```properties EUREKA_INSTANCE_IP_ADDRESS=1.2.3.4 ``` ### 1.4.3 指定要注册的URL 可以分别修改`apollo-configservice`和`apollo-adminservice`的startup.sh,通过JVM System Property传入-D参数,也可以通过OS Environment Variable传入,下面的例子会指定注册的URL为`http://1.2.3.4:8080`。 > 注:apollo-configservice和apollo-adminservice默认注册端口分别为8080、8090 JVM System Property示例: ```properties # apollo-configservice -Deureka.instance.homePageUrl=http://1.2.3.4:8080 -Deureka.instance.preferIpAddress=false # apollo-adminservice -Deureka.instance.homePageUrl=http://1.2.3.4:8090 -Deureka.instance.preferIpAddress=false ``` OS Environment Variable示例: ```properties # apollo-configservice EUREKA_INSTANCE_HOME_PAGE_URL=http://1.2.3.4:8080 EUREKA_INSTANCE_PREFER_IP_ADDRESS=false # apollo-adminservice EUREKA_INSTANCE_HOME_PAGE_URL=http://1.2.3.4:8090 EUREKA_INSTANCE_PREFER_IP_ADDRESS=false ``` ### 1.4.4 直接指定apollo-configservice地址 如果Apollo部署在公有云上,本地开发环境无法连接,但又需要做开发测试的话,客户端可以升级到0.11.0版本及以上,然后配置[跳过Apollo Meta Server服务发现](zh/client/java-sdk-user-guide#_1222-跳过apollo-meta-server服务发现) ### 1.4.5 打通网络 在一些公司(例如金融行业的公司),存在很多防火墙和网络隔离,需要打通网络(让ip1可以访问ip2的某个端口) #### 1.4.5.1 打通客户端到配置中心的网络 对于使用配置中心的客户端, client需要访问所有(或者相同机房内的)Meta Server和Config Service(默认都是8080端口),请不要打通Client到Admin Service的网络 ```mermaid flowchart LR subgraph servers[IP1:8080, IP2:8080, ..., IPn:8080] m[Meta Sever] c[Config Service] end client --> servers ``` 如果某个应用需要使用openapi,还需要访问Portal(默认是8070端口) ```mermaid flowchart LR subgraph servers[IP:8070] Portal end openapi-client --> servers ``` #### 1.4.5.2 打通配置中心内部的网络 对于配置中心自己内部,由于各个服务之间需要互相访问,所以也要保证网络上的连通 ```mermaid flowchart LR subgraph config-service-servers[All Config Service's IP:8080] m[Meta Server] c[Config Service] end subgraph admin-service-servers[All Admin Service's IP:8090] a[Admin Service] end subgraph portal-servers[IP:8070] p[Portal] end configdb[(ConfigDB)] portaldb[(PortalDB)] a --> config-service-servers a --> configdb c --> configdb p --> config-service-servers p --> admin-service-servers p --> portaldb ``` # 二、部署步骤 部署步骤总体还是比较简单的,Apollo的唯一依赖是数据库,所以需要首先把数据库准备好,然后根据实际情况,选择不同的部署方式: > [@lingjiaju](https://github.com/lingjiaju)录制了一系列Apollo快速上手视频,如果看文档觉得略繁琐的话,不妨可以先看一下他的[视频教程](https://pan.baidu.com/s/1blv87EOZS77NWT8Amkijkw#list/path=%2F)。 > 如果部署过程中遇到了问题,可以参考[部署&开发遇到的常见问题](zh/faq/common-issues-in-deployment-and-development-phase),一般都能找到答案。 ## 2.1 创建数据库 Apollo服务端共需要两个数据库:`ApolloPortalDB`和`ApolloConfigDB`,我们把数据库、表的创建和样例数据都分别准备了sql文件,只需要导入数据库即可。 需要注意的是ApolloPortalDB只需要在生产环境部署一个即可,而ApolloConfigDB需要在每个环境部署一套,如fat、uat和pro分别部署3套ApolloConfigDB。 > 注意:如果你本地已经创建过Apollo数据库,请注意备份数据。我们准备的sql文件会清空Apollo相关的表。 ### 2.1.1 创建ApolloPortalDB #### 2.1.1.1 手动导入SQL创建 通过各种MySQL客户端导入[apolloportaldb.sql](https://github.com/apolloconfig/apollo/blob/master/scripts/sql/profiles/mysql-default/apolloportaldb.sql)即可。 以MySQL原生客户端为例: ```sql source /your_local_path/scripts/sql/profiles/mysql-default/apolloportaldb.sql ``` #### 2.1.1.2 验证 导入成功后,可以通过执行以下sql语句来验证: ```sql select `Id`, `Key`, `Value`, `Comment` from `ApolloPortalDB`.`ServerConfig` limit 1; ``` | Id | Key | Value | Comment | |----|--------------------|-------|------------------| | 1 | apollo.portal.envs | dev | 可支持的环境列表 | > 注:ApolloPortalDB只需要在生产环境部署一个即可 ### 2.1.2 创建ApolloConfigDB #### 2.1.2.1 手动导入SQL 通过各种MySQL客户端导入[apolloconfigdb.sql](https://github.com/apolloconfig/apollo/blob/master/scripts/sql/profiles/mysql-default/apolloconfigdb.sql)即可。 以MySQL原生客户端为例: ```sql source /your_local_path/scripts/sql/profiles/mysql-default/apolloconfigdb.sql ``` #### 2.1.2.2 验证 导入成功后,可以通过执行以下sql语句来验证: ```sql select `Id`, `Key`, `Value`, `Comment` from `ApolloConfigDB`.`ServerConfig` limit 1; ``` | Id | Key | Value | Comment | |----|--------------------|-------------------------------|---------------| | 1 | eureka.service.url | http://127.0.0.1:8080/eureka/ | Eureka服务Url | > 注:ApolloConfigDB需要在每个环境部署一套,如fat、uat和pro分别部署3套ApolloConfigDB #### 2.1.2.4 从别的环境导入ApolloConfigDB的项目数据 如果是全新部署的Apollo配置中心,请忽略此步。 如果不是全新部署的Apollo配置中心,比如已经使用了一段时间,这时在Apollo配置中心已经创建了不少项目以及namespace等,那么在新环境中的ApolloConfigDB中需要从其它正常运行的环境中导入必要的项目数据。 主要涉及ApolloConfigDB的下面4张表,下面同时附上需要导入的数据查询语句: 1. App * 导入全部的App * 如:insert into `新环境的ApolloConfigDB`.`App` select * from `其它环境的ApolloConfigDB`.`App` where `IsDeleted` = 0; 2. AppNamespace * 导入全部的AppNamespace * 如:insert into `新环境的ApolloConfigDB`.`AppNamespace` select * from `其它环境的ApolloConfigDB`.`AppNamespace` where `IsDeleted` = 0; 3. Cluster * 导入默认的default集群 * 如:insert into `新环境的ApolloConfigDB`.`Cluster` select * from `其它环境的ApolloConfigDB`.`Cluster` where `Name` = 'default' and `IsDeleted` = 0; 4. Namespace * 导入默认的default集群中的namespace * 如:insert into `新环境的ApolloConfigDB`.`Namespace` select * from `其它环境的ApolloConfigDB`.`Namespace` where `ClusterName` = 'default' and `IsDeleted` = 0; 同时也别忘了通知用户在新的环境给自己的项目设置正确的配置信息,尤其是一些影响面比较大的公共namespace配置。 > 如果是为正在运行的环境迁移数据,建议迁移完重启一下config service,因为config service中有appnamespace的缓存数据 ### 2.1.3 调整服务端配置 Apollo自身的一些配置是放在数据库里面的,所以需要针对实际情况做一些调整,具体参数说明请参考[三、服务端配置说明](#三、服务端配置说明)。 大部分配置可以先使用默认值,不过 [apollo.portal.envs](#_311-apolloportalenvs-可支持的环境列表) 和 [eureka.service.url](#_321-eurekaserviceurl-eureka服务url) 请务必配置正确后再进行下面的部署步骤。 ## 2.2 虚拟机/物理机部署 ### 2.2.1 获取安装包 可以通过两种方式获取安装包: 1. 直接下载安装包 * 从[GitHub Release](https://github.com/apolloconfig/apollo/releases)页面下载预先打好的安装包 * 如果对Apollo的代码没有定制需求,建议使用这种方式,可以省去本地打包的过程 2. 通过源码构建 * 从[GitHub Release](https://github.com/apolloconfig/apollo/releases)页面下载Source code包或直接clone[源码](https://github.com/ctripcorp/apollo)后在本地构建 * 如果需要对Apollo的做定制开发,需要使用这种方式 #### 2.2.1.1 直接下载安装包 ##### 2.2.1.1.1 获取apollo-configservice、apollo-adminservice、apollo-portal安装包 从[GitHub Release](https://github.com/apolloconfig/apollo/releases)页面下载最新版本的`apollo-configservice-x.x.x-github.zip`、`apollo-adminservice-x.x.x-github.zip`和`apollo-portal-x.x.x-github.zip`即可。 ##### 2.2.1.1.2 配置数据库连接信息 Apollo服务端需要知道如何连接到你前面创建的数据库,数据库连接串信息位于上一步下载的压缩包中的`config/application-github.properties`中。 ###### 2.2.1.1.2.1 配置apollo-configservice的数据库连接信息 1. 解压`apollo-configservice-x.x.x-github.zip` 2. 用程序员专用编辑器(如vim,notepad++,sublime等)打开`config`目录下的`application-github.properties`文件 3. 填写正确的ApolloConfigDB数据库连接串信息,注意用户名和密码后面不要有空格! 4. 修改完的效果如下: ```properties # DataSource spring.datasource.url = jdbc:mysql://localhost:3306/ApolloConfigDB?useSSL=false&characterEncoding=utf8 spring.datasource.username = someuser spring.datasource.password = somepwd ``` > 注:由于ApolloConfigDB在每个环境都有部署,所以对不同的环境config-service需要配置对应环境的数据库参数 ###### 2.2.1.1.2.2 配置apollo-adminservice的数据库连接信息 1. 解压`apollo-adminservice-x.x.x-github.zip` 2. 用程序员专用编辑器(如vim,notepad++,sublime等)打开`config`目录下的`application-github.properties`文件 3. 填写正确的ApolloConfigDB数据库连接串信息,注意用户名和密码后面不要有空格! 4. 修改完的效果如下: ```properties # DataSource spring.datasource.url = jdbc:mysql://localhost:3306/ApolloConfigDB?useSSL=false&characterEncoding=utf8 spring.datasource.username = someuser spring.datasource.password = somepwd ``` > 注:由于ApolloConfigDB在每个环境都有部署,所以对不同的环境admin-service需要配置对应环境的数据库参数 ###### 2.2.1.1.2.3 配置apollo-portal的数据库连接信息 1. 解压`apollo-portal-x.x.x-github.zip` 2. 用程序员专用编辑器(如vim,notepad++,sublime等)打开`config`目录下的`application-github.properties`文件 3. 填写正确的ApolloPortalDB数据库连接串信息,注意用户名和密码后面不要有空格! 4. 修改完的效果如下: ```properties # DataSource spring.datasource.url = jdbc:mysql://localhost:3306/ApolloPortalDB?useSSL=false&characterEncoding=utf8 spring.datasource.username = someuser spring.datasource.password = somepwd ``` ###### 2.2.1.1.2.4 配置apollo-portal的meta service信息 Apollo Portal需要在不同的环境访问不同的meta service(apollo-configservice)地址,所以我们需要在配置中提供这些信息。默认情况下,meta service和config service是部署在同一个JVM进程,所以meta service的地址就是config service的地址。 > 对于1.6.0及以上版本,可以通过ApolloPortalDB.ServerConfig中的配置项来配置Meta Service地址,详见[apollo.portal.meta.servers - 各环境Meta Service列表](#_312-apolloportalmetaservers-各环境meta-service列表) 使用程序员专用编辑器(如vim,notepad++,sublime等)打开`apollo-portal-x.x.x-github.zip`中`config`目录下的`apollo-env.properties`文件。 假设DEV的apollo-configservice未绑定域名,地址是1.1.1.1:8080,FAT的apollo-configservice绑定了域名apollo.fat.xxx.com,UAT的apollo-configservice绑定了域名apollo.uat.xxx.com,PRO的apollo-configservice绑定了域名apollo.xxx.com,那么可以如下修改各环境meta service服务地址,格式为`${env}.meta=http://${config-service-url:port}`,如果某个环境不需要,也可以直接删除对应的配置项(如lpt.meta): ```sh dev.meta=http://1.1.1.1:8080 fat.meta=http://apollo.fat.xxx.com uat.meta=http://apollo.uat.xxx.com pro.meta=http://apollo.xxx.com ``` 除了通过`apollo-env.properties`方式配置meta service以外,apollo也支持在运行时指定meta service(优先级比`apollo-env.properties`高): 1. 通过Java System Property `${env}_meta` * 可以通过Java的System Property `${env}_meta`来指定 * 如`java -Ddev_meta=http://config-service-url -jar xxx.jar` * 也可以通过程序指定,如`System.setProperty("dev_meta", "http://config-service-url");` 2. 通过操作系统的System Environment`${ENV}_META` * 如`DEV_META=http://config-service-url` * 注意key为全大写,且中间是`_`分隔 >注1: 为了实现meta service的高可用,推荐通过SLB(Software Load Balancer)做动态负载均衡 >注2: meta service地址也可以填入IP,0.11.0版本之前只支持填入一个IP。从0.11.0版本开始支持填入以逗号分隔的多个地址([PR #1214](https://github.com/apolloconfig/apollo/pull/1214)),如`http://1.1.1.1:8080,http://2.2.2.2:8080`,不过生产环境还是建议使用域名(走slb),因为机器扩容、缩容等都可能导致IP列表的变化。 #### 2.2.1.2 通过源码构建 ##### 2.2.1.2.1 配置数据库连接信息 Apollo服务端需要知道如何连接到你前面创建的数据库,所以需要编辑[scripts/build.sh](https://github.com/apolloconfig/apollo/blob/master/scripts/build.sh),修改ApolloPortalDB和ApolloConfigDB相关的数据库连接串信息。 > 注意:填入的用户需要具备对ApolloPortalDB和ApolloConfigDB数据的读写权限。 ```sh #apollo config db info apollo_config_db_url=jdbc:mysql://localhost:3306/ApolloConfigDB?useSSL=false&characterEncoding=utf8 apollo_config_db_username=用户名 apollo_config_db_password=密码(如果没有密码,留空即可) # apollo portal db info apollo_portal_db_url=jdbc:mysql://localhost:3306/ApolloPortalDB?useSSL=false&characterEncoding=utf8 apollo_portal_db_username=用户名 apollo_portal_db_password=密码(如果没有密码,留空即可) ``` > 注1:由于ApolloConfigDB在每个环境都有部署,所以对不同的环境config-service和admin-service需要使用不同的数据库参数打不同的包,portal只需要打一次包即可 > 注2:如果不想config-service和admin-service每个环境打一个包的话,也可以通过运行时传入数据库连接串信息实现,具体可以参考 [Issue 869](https://github.com/apolloconfig/apollo/issues/869) > 注3:每个环境都需要独立部署一套config-service、admin-service和ApolloConfigDB ##### 2.2.1.2.2 配置各环境meta service地址 Apollo Portal需要在不同的环境访问不同的meta service(apollo-configservice)地址,所以需要在打包时提供这些信息。 假设DEV的apollo-configservice未绑定域名,地址是1.1.1.1:8080,FAT的apollo-configservice绑定了域名apollo.fat.xxx.com,UAT的apollo-configservice绑定了域名apollo.uat.xxx.com,PRO的apollo-configservice绑定了域名apollo.xxx.com,那么编辑[scripts/build.sh](https://github.com/apolloconfig/apollo/blob/master/scripts/build.sh),如下修改各环境meta service服务地址,格式为`${env}_meta=http://${config-service-url:port}`,如果某个环境不需要,也可以直接删除对应的配置项: ```sh dev_meta=http://1.1.1.1:8080 fat_meta=http://apollo.fat.xxx.com uat_meta=http://apollo.uat.xxx.com pro_meta=http://apollo.xxx.com META_SERVERS_OPTS="-Ddev_meta=$dev_meta -Dfat_meta=$fat_meta -Duat_meta=$uat_meta -Dpro_meta=$pro_meta" ``` 除了在打包时配置meta service以外,apollo也支持在运行时指定meta service: 1. 通过Java System Property `${env}_meta` * 可以通过Java的System Property `${env}_meta`来指定 * 如`java -Ddev_meta=http://config-service-url -jar xxx.jar` * 也可以通过程序指定,如`System.setProperty("dev_meta", "http://config-service-url");` 2. 通过操作系统的System Environment`${ENV}_META` * 如`DEV_META=http://config-service-url` * 注意key为全大写,且中间是`_`分隔 >注1: 为了实现meta service的高可用,推荐通过SLB(Software Load Balancer)做动态负载均衡 >注2: meta service地址也可以填入IP,0.11.0版本之前只支持填入一个IP。从0.11.0版本开始支持填入以逗号分隔的多个地址([PR #1214](https://github.com/apolloconfig/apollo/pull/1214)),如`http://1.1.1.1:8080,http://2.2.2.2:8080`,不过生产环境还是建议使用域名(走slb),因为机器扩容、缩容等都可能导致IP列表的变化。 ##### 2.2.1.2.3 执行编译、打包 做完上述配置后,就可以执行编译和打包了。 > 注:初次编译会从Maven中央仓库下载不少依赖,如果网络情况不佳时很容易出错,建议使用国内的Maven仓库源,比如[阿里云Maven镜像](http://www.cnblogs.com/geektown/p/5705405.html) ```sh ./build.sh ``` 该脚本会依次打包apollo-configservice, apollo-adminservice, apollo-portal。 > 注:由于ApolloConfigDB在每个环境都有部署,所以对不同环境的config-service和admin-service需要使用不同的数据库连接信息打不同的包,portal只需要打一次包即可 ##### 2.2.1.2.4 获取apollo-configservice安装包 位于`apollo-configservice/target/`目录下的`apollo-configservice-x.x.x-github.zip` 需要注意的是由于ApolloConfigDB在每个环境都有部署,所以对不同环境的config-service需要使用不同的数据库参数打不同的包后分别部署 ##### 2.2.1.2.5 获取apollo-adminservice安装包 位于`apollo-adminservice/target/`目录下的`apollo-adminservice-x.x.x-github.zip` 需要注意的是由于ApolloConfigDB在每个环境都有部署,所以对不同环境的admin-service需要使用不同的数据库参数打不同的包后分别部署 ##### 2.2.1.2.6 获取apollo-portal安装包 位于`apollo-portal/target/`目录下的`apollo-portal-x.x.x-github.zip` ### 2.2.2 部署Apollo服务端 #### 2.2.2.1 部署apollo-configservice 将对应环境的`apollo-configservice-x.x.x-github.zip`上传到服务器上,解压后执行scripts/startup.sh即可。如需停止服务,执行scripts/shutdown.sh. 记得在scripts/startup.sh中按照实际的环境设置一个JVM内存,以下是我们的默认设置,供参考: ```bash export JAVA_OPTS="-server -Xms6144m -Xmx6144m -Xss256k -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=384m -XX:NewSize=4096m -XX:MaxNewSize=4096m -XX:SurvivorRatio=18" ``` > 注1:如果需要修改JVM参数,可以修改scripts/startup.sh的`JAVA_OPTS`部分。 > 注2:如要调整服务的日志输出路径,可以修改scripts/startup.sh和apollo-configservice.conf中的`LOG_DIR`。 > 注3:如要调整服务的监听端口,可以修改scripts/startup.sh中的`SERVER_PORT`。另外apollo-configservice同时承担meta server职责,如果要修改端口,注意要同时ApolloConfigDB.ServerConfig表中的`eureka.service.url`配置项以及apollo-portal和apollo-client中的使用到的meta server信息,详见:[2.2.1.1.2.4 配置apollo-portal的meta service信息](#_221124-配置apollo-portal的meta-service信息)和[1.2.2 Apollo Meta Server](zh/client/java-sdk-user-guide#_122-apollo-meta-server)。 > 注4:如果ApolloConfigDB.ServerConfig的eureka.service.url只配了当前正在启动的机器的话,在启动apollo-configservice的过程中会在日志中输出eureka注册失败的信息,如`com.sun.jersey.api.client.ClientHandlerException: java.net.ConnectException: Connection refused`。需要注意的是,这个是预期的情况,因为apollo-configservice需要向Meta Server(它自己)注册服务,但是因为在启动过程中,自己还没起来,所以会报这个错。后面会进行重试的动作,所以等自己服务起来后就会注册正常了。 > 注5:apollo-configservice从2.5.0版本开始支持优雅下线功能。当服务收到停止信号时,会等待正在处理的请求完成后再关闭,默认等待时间为10秒。此功能通过Spring Boot的`server.shutdown=graceful`和`spring.lifecycle.timeout-per-shutdown-phase=${GRACEFUL_SHUTDOWN_TIMEOUT:10s}`配置启用。如需调整超时时间,可以通过环境变量`GRACEFUL_SHUTDOWN_TIMEOUT`设置(如`30s`、`60s`、`2m`等),或直接修改application.yml中的配置。在Kubernetes环境中,请确保Pod的`terminationGracePeriodSeconds`大于配置的超时时间(建议至少多10秒)。 > 注6:如果你看到了这里,相信你一定是一个细心阅读文档的人,而且离成功就差一点点了,继续加油,应该很快就能完成Apollo的分布式部署了!不过你是否有感觉Apollo的分布式部署步骤有点繁琐?是否有啥建议想要和作者说?如果答案是肯定的话,请移步 [#1424](https://github.com/apolloconfig/apollo/issues/1424),期待你的建议! #### 2.2.2.2 部署apollo-adminservice 将对应环境的`apollo-adminservice-x.x.x-github.zip`上传到服务器上,解压后执行scripts/startup.sh即可。如需停止服务,执行scripts/shutdown.sh. 记得在scripts/startup.sh中按照实际的环境设置一个JVM内存,以下是我们的默认设置,供参考: ```bash export JAVA_OPTS="-server -Xms2560m -Xmx2560m -Xss256k -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=384m -XX:NewSize=1024m -XX:MaxNewSize=1024m -XX:SurvivorRatio=22" ``` > 注1:如果需要修改JVM参数,可以修改scripts/startup.sh的`JAVA_OPTS`部分。 > 注2:如要调整服务的日志输出路径,可以修改scripts/startup.sh和apollo-adminservice.conf中的`LOG_DIR`。 > 注3:如要调整服务的监听端口,可以修改scripts/startup.sh中的`SERVER_PORT`。 > 注4:apollo-adminservice从2.5.0版本开始支持优雅下线功能。当服务收到停止信号时,会等待正在处理的请求完成后再关闭,默认等待时间为10秒。此功能通过Spring Boot的`server.shutdown=graceful`和`spring.lifecycle.timeout-per-shutdown-phase=${GRACEFUL_SHUTDOWN_TIMEOUT:10s}`配置启用。如需调整超时时间,可以通过环境变量`GRACEFUL_SHUTDOWN_TIMEOUT`设置(如`30s`、`60s`、`2m`等),或直接修改application.yml中的配置。在Kubernetes环境中,请确保Pod的`terminationGracePeriodSeconds`大于配置的超时时间(建议至少多10秒)。 #### 2.2.2.3 部署apollo-portal 将`apollo-portal-x.x.x-github.zip`上传到服务器上,解压后执行scripts/startup.sh即可。如需停止服务,执行scripts/shutdown.sh. 记得在startup.sh中按照实际的环境设置一个JVM内存,以下是我们的默认设置,供参考: ```bash export JAVA_OPTS="-server -Xms4096m -Xmx4096m -Xss256k -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=384m -XX:NewSize=1536m -XX:MaxNewSize=1536m -XX:SurvivorRatio=22" ``` > 注1:如果需要修改JVM参数,可以修改scripts/startup.sh的`JAVA_OPTS`部分。 > 注2:如要调整服务的日志输出路径,可以修改scripts/startup.sh和apollo-portal.conf中的`LOG_DIR`。 > 注3:如要调整服务的监听端口,可以修改scripts/startup.sh中的`SERVER_PORT`。 ### 2.2.3 使用其它服务注册中心替换内置eureka #### 2.2.3.1 nacos-discovery > 适用于1.8.0及以上版本 启用外部nacos服务注册中心替换内置eureka > 注意:需要重新打包 1. 修改build.sh/build.bat,将config-service和admin-service的maven编译命令更改为 ```shell mvn clean package -Pgithub,nacos-discovery -DskipTests -pl apollo-configservice,apollo-adminservice -am -Dapollo_profile=github,nacos-discovery -Dspring_datasource_url=$apollo_config_db_url -Dspring_datasource_username=$apollo_config_db_username -Dspring_datasource_password=$apollo_config_db_password ``` 2. 分别修改apollo-configservice和apollo-adminservice安装包中config目录下的application-github.properties,配置nacos服务器地址 ```properties nacos.discovery.server-addr=127.0.0.1:8848 # 更多 nacos 配置 nacos.discovery.access-key= nacos.discovery.username= nacos.discovery.password= nacos.discovery.secret-key= nacos.discovery.namespace= nacos.discovery.context-path= ``` #### 2.2.3.2 consul-discovery > 适用于1.9.0及以上版本 启用外部Consul服务注册中心替换内置eureka ##### 2.2.3.2.1 2.1.0 及以上版本 1. 修改`apollo-configservice-x.x.x-github.zip`和`apollo-adminservice-x.x.x-github.zip`解压后的`config/application.properties`,取消注释,把 ```properties #spring.profiles.active=github,consul-discovery ``` 变成 ```properties spring.profiles.active=github,consul-discovery ``` 2. 分别修改apollo-configservice和apollo-adminservice安装包中config目录下的application-github.properties,配置consul服务器地址 ```properties spring.cloud.consul.host=127.0.0.1 spring.cloud.consul.port=8500 ``` ##### 2.2.3.2.2 2.1.0 之前的版本 > 注意:需要重新打包 1. 修改build.sh/build.bat,将config-service和admin-service的maven编译命令更改为 ```shell mvn clean package -Pgithub -DskipTests -pl apollo-configservice,apollo-adminservice -am -Dapollo_profile=github,consul-discovery -Dspring_datasource_url=$apollo_config_db_url -Dspring_datasource_username=$apollo_config_db_username -Dspring_datasource_password=$apollo_config_db_password ``` 2. 分别修改apollo-configservice和apollo-adminservice安装包中config目录下的application-github.properties,配置consul服务器地址 ```properties spring.cloud.consul.host=127.0.0.1 spring.cloud.consul.port=8500 ``` #### 2.2.3.3 zookeeper-discovery > 适用于2.0.0及以上版本 启用外部Zookeeper服务注册中心替换内置eureka ##### 2.2.3.3.1 2.1.0 及以上版本 1. 修改`apollo-configservice-x.x.x-github.zip`和`apollo-adminservice-x.x.x-github.zip`解压后的`config/application.properties`,取消注释,把 ```properties #spring.profiles.active=github,zookeeper-discovery ``` 变成 ```properties spring.profiles.active=github,zookeeper-discovery ``` 2. 分别修改apollo-configservice和apollo-adminservice安装包中config目录下的application-github.properties,配置zookeeper服务器地址 ```properties spring.cloud.zookeeper.connect-string=127.0.0.1:2181 ``` 3.Zookeeper版本说明 * 支持Zookeeper3.5.x以上的版本; * 如果apollo-configservice应用启动报端口占用,请检查Zookeeper的如下配置; > 注:Zookeeper3.5.0新增了内置的[AdminServer](https://zookeeper.apache.org/doc/r3.5.0-alpha/zookeeperAdmin.html#sc_adminserver_config) ```properties admin.enableServer admin.serverPort ``` ##### 2.2.3.3.2 2.1.0 之前的版本 1. 修改build.sh/build.bat,将`config-service`和`admin-service`的maven编译命令更改为 ```shell mvn clean package -Pgithub -DskipTests -pl apollo-configservice,apollo-adminservice -am -Dapollo_profile=github,zookeeper-discovery -Dspring_datasource_url=$apollo_config_db_url -Dspring_datasource_username=$apollo_config_db_username -Dspring_datasource_password=$apollo_config_db_password ``` 2. 分别修改apollo-configservice和apollo-adminservice安装包中config目录下的application-github.properties,配置zookeeper服务器地址 ```properties spring.cloud.zookeeper.connect-string=127.0.0.1:2181 ``` 3.Zookeeper版本说明 * 支持Zookeeper3.5.x以上的版本; * 如果apollo-configservice应用启动报端口占用,请检查Zookeeper的如下配置; > 注:Zookeeper3.5.0新增了内置的[AdminServer](https://zookeeper.apache.org/doc/r3.5.0-alpha/zookeeperAdmin.html#sc_adminserver_config) ```properties admin.enableServer admin.serverPort ``` #### 2.2.3.4 custom-defined-discovery > 适用于2.0.0及以上版本 启用custom-defined-discovery替换内置eureka ##### 2.2.3.4.1 2.1.0 及以上版本 1. 修改`apollo-configservice-x.x.x-github.zip`和`apollo-adminservice-x.x.x-github.zip`解压后的`config/application.properties`,取消注释,把 ```properties #spring.profiles.active=github,custom-defined-discovery ``` 变成 ```properties spring.profiles.active=github,custom-defined-discovery ``` 2. 配置自定义的 config-service 与 admin-service 的访问地址有两种方式:一种在mysql数据库ApolloConfigDB,表ServerConfig当中写入两条数据。 ```sql INSERT INTO `ApolloConfigDB`.`ServerConfig` (`Key`, `Value`, `Comment`) VALUES ('apollo.config-service.url', 'http://apollo-config-service', 'ConfigService 访问地址'); INSERT INTO `ApolloConfigDB`.`ServerConfig` (`Key`, `Value`, `Comment`) VALUES ('apollo.admin-service.url', 'http://apollo-admin-service', 'AdminService 访问地址'); ``` 另外一种修改apollo-configservice安装包中config目录下的application-github.properties ```properties apollo.config-service.url=http://apollo-config-service apollo.admin-service.url=http://apollo-admin-service ``` ##### 2.2.3.4.2 2.1.0 之前的版本 > 注意:需要重新打包 1. 修改build.sh/build.bat,将`config-service`和`admin-service`的maven编译命令更改为 ```shell mvn clean package -Pgithub -DskipTests -pl apollo-configservice,apollo-adminservice -am -Dapollo_profile=github,custom-defined-discovery -Dspring_datasource_url=$apollo_config_db_url -Dspring_datasource_username=$apollo_config_db_username -Dspring_datasource_password=$apollo_config_db_password ``` 2. 配置自定义的 config-service 与 admin-service 的访问地址有两种方式:一种在mysql数据库ApolloConfigDB,表ServerConfig当中写入两条数据。 ```sql INSERT INTO `ApolloConfigDB`.`ServerConfig` (`Key`, `Value`, `Comment`) VALUES ('apollo.config-service.url', 'http://apollo-config-service', 'ConfigService 访问地址'); INSERT INTO `ApolloConfigDB`.`ServerConfig` (`Key`, `Value`, `Comment`) VALUES ('apollo.admin-service.url', 'http://apollo-admin-service', 'AdminService 访问地址'); ``` 另外一种修改apollo-configservice安装包中config目录下的application-github.properties ```properties apollo.config-service.url=http://apollo-config-service apollo.admin-service.url=http://apollo-admin-service ``` #### 2.2.3.5 database-discovery > 仅支持 2.1.0 及以上版本 启用database-discovery替换内置eureka Apollo支持使用内部的数据库表作为注册中心,不依赖第三方的注册中心 1. 修改`apollo-configservice-x.x.x-github.zip`和`apollo-adminservice-x.x.x-github.zip`解压后的`config/application.properties`,取消注释,把 ```properties #spring.profiles.active=github,database-discovery ``` 变成 ```properties spring.profiles.active=github,database-discovery ``` 2. (可选)在多机房部署时, 如果你需要apollo客户端只读取同机房内的Config Service, 你可以在Config Service和Admin Service安装包中`config/application-github.properties`新增一条配置 ```properties apollo.service.registry.cluster=与apollo的Cluster同名 ``` 3. (可选)如果你希望自定义Config Service和Admin Service给Client使用的uri, 例如在内网部署时, 如果不希望暴露内网ip, 你可以在Config Service和Admin Service安装包中`config/application-github.properties`新增一条配置 ```properties apollo.service.registry.uri=http://你的ip或者域名:${server.port}/ ``` ## 2.3 Docker部署 ### 2.3.1 1.7.0及以上版本 Apollo 1.7.0版本开始会默认上传Docker镜像到[Docker Hub](https://hub.docker.com/u/apolloconfig),可以按照如下步骤获取 #### 2.3.1.1 Apollo Config Service ##### 2.3.1.1.1 获取镜像 ```bash docker pull apolloconfig/apollo-configservice:${version} ``` ##### 2.3.1.1.2 运行镜像 示例: ```bash docker run -p 8080:8080 \ -e SPRING_DATASOURCE_URL="jdbc:mysql://fill-in-the-correct-server:3306/ApolloConfigDB?characterEncoding=utf8" \ -e SPRING_DATASOURCE_USERNAME=FillInCorrectUser -e SPRING_DATASOURCE_PASSWORD=FillInCorrectPassword \ -d -v /tmp/logs:/opt/logs --name apollo-configservice apolloconfig/apollo-configservice:${version} ``` 参数说明: * SPRING_DATASOURCE_URL: 对应环境ApolloConfigDB的地址 * SPRING_DATASOURCE_USERNAME: 对应环境ApolloConfigDB的用户名 * SPRING_DATASOURCE_PASSWORD: 对应环境ApolloConfigDB的密码 #### 2.3.1.2 Apollo Admin Service ##### 2.3.1.2.1 获取镜像 ```bash docker pull apolloconfig/apollo-adminservice:${version} ``` ##### 2.3.1.2.2 运行镜像 示例: ```bash docker run -p 8090:8090 \ -e SPRING_DATASOURCE_URL="jdbc:mysql://fill-in-the-correct-server:3306/ApolloConfigDB?characterEncoding=utf8" \ -e SPRING_DATASOURCE_USERNAME=FillInCorrectUser -e SPRING_DATASOURCE_PASSWORD=FillInCorrectPassword \ -d -v /tmp/logs:/opt/logs --name apollo-adminservice apolloconfig/apollo-adminservice:${version} ``` 参数说明: * SPRING_DATASOURCE_URL: 对应环境ApolloConfigDB的地址 * SPRING_DATASOURCE_USERNAME: 对应环境ApolloConfigDB的用户名 * SPRING_DATASOURCE_PASSWORD: 对应环境ApolloConfigDB的密码 #### 2.3.1.3 Apollo Portal ##### 2.3.1.3.1 获取镜像 ```bash docker pull apolloconfig/apollo-portal:${version} ``` ##### 2.3.1.3.2 运行镜像 示例: ```bash docker run -p 8070:8070 \ -e SPRING_DATASOURCE_URL="jdbc:mysql://fill-in-the-correct-server:3306/ApolloPortalDB?characterEncoding=utf8" \ -e SPRING_DATASOURCE_USERNAME=FillInCorrectUser -e SPRING_DATASOURCE_PASSWORD=FillInCorrectPassword \ -e APOLLO_PORTAL_ENVS=dev,pro \ -e DEV_META=http://fill-in-dev-meta-server:8080 -e PRO_META=http://fill-in-pro-meta-server:8080 \ -d -v /tmp/logs:/opt/logs --name apollo-portal apolloconfig/apollo-portal:${version} ``` 参数说明: * SPRING_DATASOURCE_URL: 对应环境ApolloPortalDB的地址 * SPRING_DATASOURCE_USERNAME: 对应环境ApolloPortalDB的用户名 * SPRING_DATASOURCE_PASSWORD: 对应环境ApolloPortalDB的密码 * APOLLO_PORTAL_ENVS(可选): 对应ApolloPortalDB中的[apollo.portal.envs](#_311-apolloportalenvs-可支持的环境列表)配置项,如果没有在数据库中配置的话,可以通过此环境参数配置 * DEV_META/PRO_META(可选): 配置对应环境的Meta Service地址,以${ENV}_META命名,需要注意的是如果配置了ApolloPortalDB中的[apollo.portal.meta.servers](#_312-apolloportalmetaservers-各环境meta-service列表)配置,则以apollo.portal.meta.servers中的配置为准 #### 2.3.1.4 通过源码构建 Docker 镜像 如果修改了 apollo 服务端的代码,希望通过源码构建 Docker 镜像,可以参考下面的步骤: 1. 通过源码构建安装包:`./scripts/build.sh` 2. 构建 Docker 镜像:`mvn docker:build -pl apollo-configservice,apollo-adminservice,apollo-portal` ### 2.3.2 1.7.0之前的版本 Apollo项目已经自带了Docker file,可以参照[2.2.1 获取安装包](#_221-获取安装包)配置好安装包后通过下面的文件来打Docker镜像: 1. [apollo-configservice](https://github.com/apolloconfig/apollo/blob/master/apollo-configservice/src/main/docker/Dockerfile) 2. [apollo-adminservice](https://github.com/apolloconfig/apollo/blob/master/apollo-adminservice/src/main/docker/Dockerfile) 3. [apollo-portal](https://github.com/apolloconfig/apollo/blob/master/apollo-portal/src/main/docker/Dockerfile) 也可以参考Apollo用户[@kulovecc](https://github.com/kulovecc)的[docker-apollo](https://github.com/kulovecc/docker-apollo)项目和[@idoop](https://github.com/idoop)的[docker-apollo](https://github.com/idoop/docker-apollo)项目。 ## 2.4 Kubernetes部署 ### 2.4.1 基于Kubernetes原生服务发现 Apollo 1.7.0版本增加了基于Kubernetes原生服务发现的部署模式,由于不再使用内置的Eureka,所以在整体部署上有很大简化,同时也提供了Helm Charts,便于部署。 > 更多设计说明可以参考[#3054](https://github.com/apolloconfig/apollo/issues/3054)。 #### 2.4.1.1 环境要求 - Kubernetes 1.10+ - Helm 3 #### 2.4.1.2 添加Apollo Helm Chart仓库 ```bash $ helm repo add apollo https://charts.apolloconfig.com $ helm search repo apollo ``` #### 2.4.1.3 部署apollo-configservice和apollo-adminservice ##### 2.4.1.3.1 安装apollo-configservice和apollo-adminservice 需要在每个环境中安装apollo-configservice和apollo-adminservice,所以建议在release名称中加入环境信息,例如:`apollo-service-dev` ```bash $ helm install apollo-service-dev \ --set configdb.host=1.2.3.4 \ --set configdb.userName=apollo \ --set configdb.password=apollo \ --set configdb.service.enabled=true \ --set configService.replicaCount=1 \ --set adminService.replicaCount=1 \ -n your-namespace \ apollo/apollo-service ``` 一般部署建议通过 values.yaml 来配置: ```bash $ helm install apollo-service-dev -f values.yaml -n your-namespace apollo/apollo-service ``` 安装完成后会提示对应环境的Meta Server地址,需要记录下来,apollo-portal安装时需要用到: ```bash Get meta service url for current release by running these commands: echo http://apollo-service-dev-apollo-configservice:8080 ``` > 更多配置项说明可以参考[2.4.1.3.3 配置项说明](#_24133-配置项说明) ##### 2.4.1.3.2 卸载apollo-configservice和apollo-adminservice 例如要卸载`apollo-service-dev`的部署: ```bash $ helm uninstall -n your-namespace apollo-service-dev ``` ##### 2.4.1.3.3 配置项说明 下表列出了apollo-service chart的可配置参数及其默认值: | Parameter | Description | Default | |----------------------|---------------------------------------------|---------------------| | `configdb.host` | The host for apollo config db | `nil` | | `configdb.port` | The port for apollo config db | `3306` | | `configdb.dbName` | The database name for apollo config db | `ApolloConfigDB` | | `configdb.userName` | The user name for apollo config db | `nil` | | `configdb.password` | The password for apollo config db | `nil` | | `configdb.connectionStringProperties` | The connection string properties for apollo config db | `characterEncoding=utf8` | | `configdb.service.enabled` | Whether to create a Kubernetes Service for `configdb.host` or not. Set it to `true` if `configdb.host` is an endpoint outside of the kubernetes cluster | `false` | | `configdb.service.fullNameOverride` | Override the service name for apollo config db | `nil` | | `configdb.service.port` | The port for the service of apollo config db | `3306` | | `configdb.service.type` | The service type of apollo config db: `ClusterIP` or `ExternalName`. If the host is a DNS name, please specify `ExternalName` as the service type, e.g. `xxx.mysql.rds.aliyuncs.com` | `ClusterIP` | | `configService.fullNameOverride` | Override the deployment name for apollo-configservice | `nil` | | `configService.replicaCount` | Replica count of apollo-configservice | `2` | | `configService.containerPort` | Container port of apollo-configservice | `8080` | | `configService.image.repository` | Image repository of apollo-configservice | `apolloconfig/apollo-configservice` | | `configService.image.tag` | Image tag of apollo-configservice, e.g. `1.8.0`, leave it to `nil` to use the default version. _(chart version >= 0.2.0)_ | `nil` | | `configService.image.pullPolicy` | Image pull policy of apollo-configservice | `IfNotPresent` | | `configService.imagePullSecrets` | Image pull secrets of apollo-configservice | `[]` | | `configService.service.fullNameOverride` | Override the service name for apollo-configservice | `nil` | | `configService.service.annotations` | The annotations of the service for apollo-configservice. _(chart version >= 0.9.0)_ | `{}` | | `configService.service.port` | The port for the service of apollo-configservice | `8080` | | `configService.service.targetPort` | The target port for the service of apollo-configservice | `8080` | | `configService.service.type` | The service type of apollo-configservice | `ClusterIP` | | `configService.ingress.enabled` | Whether to enable the ingress for config-service or not. _(chart version >= 0.2.0)_ | `false` | | `configService.ingress.annotations` | The annotations of the ingress for config-service. _(chart version >= 0.2.0)_ | `{}` | | `configService.ingress.hosts.host` | The host of the ingress for config-service. _(chart version >= 0.2.0)_ | `nil` | | `configService.ingress.hosts.paths` | The paths of the ingress for config-service. _(chart version >= 0.2.0)_ | `[]` | | `configService.ingress.tls` | The tls definition of the ingress for config-service. _(chart version >= 0.2.0)_ | `[]` | | `configService.liveness.initialDelaySeconds` | The initial delay seconds of liveness probe | `100` | | `configService.liveness.periodSeconds` | The period seconds of liveness probe | `10` | | `configService.readiness.initialDelaySeconds` | The initial delay seconds of readiness probe | `30` | | `configService.readiness.periodSeconds` | The period seconds of readiness probe | `5` | | `configService.config.profiles` | specify the spring profiles to activate | `github,kubernetes` | | `configService.config.configServiceUrlOverride` | Override `apollo.config-service.url`: config service url to be accessed by apollo-client, e.g. `http://apollo-config-service-dev:8080` | `nil` | | `configService.config.adminServiceUrlOverride` | Override `apollo.admin-service.url`: admin service url to be accessed by apollo-portal, e.g. `http://apollo-admin-service-dev:8090` | `nil` | | `configService.config.contextPath` | specify the context path, e.g. `/apollo`, then users could access config service via `http://{config_service_address}/apollo`. _(chart version >= 0.2.0)_ | `nil` | | `configService.env` | Environment variables passed to the container, e.g.
    `JAVA_OPTS: -Xss256k` | `{}` | | `configService.strategy` | The deployment strategy of apollo-configservice | `{}` | | `configService.resources` | The resources definition of apollo-configservice | `{}` | | `configService.nodeSelector` | The node selector definition of apollo-configservice | `{}` | | `configService.tolerations` | The tolerations definition of apollo-configservice | `[]` | | `configService.affinity` | The affinity definition of apollo-configservice | `{}` | | `adminService.fullNameOverride` | Override the deployment name for apollo-adminservice | `nil` | | `adminService.replicaCount` | Replica count of apollo-adminservice | `2` | | `adminService.containerPort` | Container port of apollo-adminservice | `8090` | | `adminService.image.repository` | Image repository of apollo-adminservice | `apolloconfig/apollo-adminservice` | | `adminService.image.tag` | Image tag of apollo-adminservice, e.g. `1.8.0`, leave it to `nil` to use the default version. _(chart version >= 0.2.0)_ | `nil` | | `adminService.image.pullPolicy` | Image pull policy of apollo-adminservice | `IfNotPresent` | | `adminService.imagePullSecrets` | Image pull secrets of apollo-adminservice | `[]` | | `adminService.service.fullNameOverride` | Override the service name for apollo-adminservice | `nil` | | `adminService.service.annotations` | The annotations of the service for apollo-adminservice. _(chart version >= 0.9.0)_ | `{}` | | `adminService.service.port` | The port for the service of apollo-adminservice | `8090` | | `adminService.service.targetPort` | The target port for the service of apollo-adminservice | `8090` | | `adminService.service.type` | The service type of apollo-adminservice | `ClusterIP` | | `adminService.ingress.enabled` | Whether to enable the ingress for admin-service or not. _(chart version >= 0.2.0)_ | `false` | | `adminService.ingress.annotations` | The annotations of the ingress for admin-service. _(chart version >= 0.2.0)_ | `{}` | | `adminService.ingress.hosts.host` | The host of the ingress for admin-service. _(chart version >= 0.2.0)_ | `nil` | | `adminService.ingress.hosts.paths` | The paths of the ingress for admin-service. _(chart version >= 0.2.0)_ | `[]` | | `adminService.ingress.tls` | The tls definition of the ingress for admin-service. _(chart version >= 0.2.0)_ | `[]` | | `adminService.liveness.initialDelaySeconds` | The initial delay seconds of liveness probe | `100` | | `adminService.liveness.periodSeconds` | The period seconds of liveness probe | `10` | | `adminService.readiness.initialDelaySeconds` | The initial delay seconds of readiness probe | `30` | | `adminService.readiness.periodSeconds` | The period seconds of readiness probe | `5` | | `adminService.config.profiles` | specify the spring profiles to activate | `github,kubernetes` | | `adminService.config.contextPath` | specify the context path, e.g. `/apollo`, then users could access admin service via `http://{admin_service_address}/apollo`. _(chart version >= 0.2.0)_ | `nil` | | `adminService.env` | Environment variables passed to the container, e.g.
    `JAVA_OPTS: -Xss256k` | `{}` | | `adminService.strategy` | The deployment strategy of apollo-adminservice | `{}` | | `adminService.resources` | The resources definition of apollo-adminservice | `{}` | | `adminService.nodeSelector` | The node selector definition of apollo-adminservice | `{}` | | `adminService.tolerations` | The tolerations definition of apollo-adminservice | `[]` | | `adminService.affinity` | The affinity definition of apollo-adminservice | `{}` | ##### 2.4.1.3.4 配置样例 ###### 2.4.1.3.4.1 ConfigDB的host是k8s集群外的IP ```yaml configdb: host: 1.2.3.4 dbName: ApolloConfigDBName userName: someUserName password: somePassword connectionStringProperties: characterEncoding=utf8&useSSL=false service: enabled: true ``` ###### 2.4.1.3.4.2 ConfigDB的host是k8s集群外的域名 ```yaml configdb: host: xxx.mysql.rds.aliyuncs.com dbName: ApolloConfigDBName userName: someUserName password: somePassword connectionStringProperties: characterEncoding=utf8&useSSL=false service: enabled: true type: ExternalName ``` ###### 2.4.1.3.4.3 ConfigDB的host是k8s集群内的一个服务 ```yaml configdb: host: apollodb-mysql.mysql dbName: ApolloConfigDBName userName: someUserName password: somePassword connectionStringProperties: characterEncoding=utf8&useSSL=false ``` ###### 2.4.1.3.4.4 指定Meta Server返回的apollo-configservice地址 如果apollo-client无法直接访问apollo-configservice的Service(比如不在同一个k8s集群),那么可以参照下面的示例指定Meta Server返回给apollo-client的地址(比如可以通过nodeport访问) ```yaml configService: config: configServiceUrlOverride: http://1.2.3.4:12345 ``` ###### 2.4.1.3.4.5 指定Meta Server返回的apollo-adminservice地址 如果apollo-portal无法直接访问apollo-adminservice的Service(比如不在同一个k8s集群),那么可以参照下面的示例指定Meta Server返回给apollo-portal的地址(比如可以通过nodeport访问) ```yaml configService: config: adminServiceUrlOverride: http://1.2.3.4:23456 ``` ###### 2.4.1.3.4.6 以Ingress配置自定义路径`/config`形式暴露apollo-configservice服务 ```yaml # use /config as root, should specify configService.config.contextPath as /config configService: config: contextPath: /config ingress: enabled: true hosts: - paths: - /config ``` ###### 2.4.1.3.4.7 以Ingress配置自定义路径`/admin`形式暴露apollo-adminservice服务 ```yaml # use /admin as root, should specify adminService.config.contextPath as /admin adminService: config: contextPath: /admin ingress: enabled: true hosts: - paths: - /admin ``` #### 2.4.1.4 部署apollo-portal ##### 2.4.1.4.1 安装apollo-portal 假设有dev, pro两个环境,且meta server地址分别为`http://apollo-service-dev-apollo-configservice:8080`和`http://apollo-service-pro-apollo-configservice:8080`: ```bash $ helm install apollo-portal \ --set portaldb.host=1.2.3.4 \ --set portaldb.userName=apollo \ --set portaldb.password=apollo \ --set portaldb.service.enabled=true \ --set config.envs="dev\,pro" \ --set config.metaServers.dev=http://apollo-service-dev-apollo-configservice:8080 \ --set config.metaServers.pro=http://apollo-service-pro-apollo-configservice:8080 \ --set replicaCount=1 \ -n your-namespace \ apollo/apollo-portal ``` 一般部署建议通过 values.yaml 来配置: ```bash $ helm install apollo-portal -f values.yaml -n your-namespace apollo/apollo-portal ``` > 更多配置项说明可以参考[2.4.1.4.3 配置项说明](#_24143-配置项说明) ##### 2.4.1.4.2 卸载apollo-portal 例如要卸载`apollo-portal`的部署: ```bash $ helm uninstall -n your-namespace apollo-portal ``` ##### 2.4.1.4.3 配置项说明 下表列出了apollo-portal chart的可配置参数及其默认值: | Parameter | Description | Default | |----------------------|---------------------------------------------|-----------------------| | `fullNameOverride` | Override the deployment name for apollo-portal | `nil` | | `replicaCount` | Replica count of apollo-portal | `2` | | `containerPort` | Container port of apollo-portal | `8070` | | `image.repository` | Image repository of apollo-portal | `apolloconfig/apollo-portal` | | `image.tag` | Image tag of apollo-portal, e.g. `1.8.0`, leave it to `nil` to use the default version. _(chart version >= 0.2.0)_ | `nil` | | `image.pullPolicy` | Image pull policy of apollo-portal | `IfNotPresent` | | `imagePullSecrets` | Image pull secrets of apollo-portal | `[]` | | `service.fullNameOverride` | Override the service name for apollo-portal | `nil` | | `service.annotations` | The annotations of the service for apollo-portal. _(chart version >= 0.9.0)_ | `{}` | | `service.port` | The port for the service of apollo-portal | `8070` | | `service.targetPort` | The target port for the service of apollo-portal | `8070` | | `service.type` | The service type of apollo-portal | `ClusterIP` | | `service.sessionAffinity` | The session affinity for the service of apollo-portal | `ClientIP` | | `ingress.enabled` | Whether to enable the ingress or not | `false` | | `ingress.annotations` | The annotations of the ingress | `{}` | | `ingress.hosts.host` | The host of the ingress | `nil` | | `ingress.hosts.paths` | The paths of the ingress | `[]` | | `ingress.tls` | The tls definition of the ingress | `[]` | | `liveness.initialDelaySeconds` | The initial delay seconds of liveness probe | `100` | | `liveness.periodSeconds` | The period seconds of liveness probe | `10` | | `readiness.initialDelaySeconds` | The initial delay seconds of readiness probe | `30` | | `readiness.periodSeconds` | The period seconds of readiness probe | `5` | | `env` | Environment variables passed to the container, e.g.
    `JAVA_OPTS: -Xss256k` | `{}` | | `strategy` | The deployment strategy of apollo-portal | `{}` | | `resources` | The resources definition of apollo-portal | `{}` | | `nodeSelector` | The node selector definition of apollo-portal | `{}` | | `tolerations` | The tolerations definition of apollo-portal | `[]` | | `affinity` | The affinity definition of apollo-portal | `{}` | | `config.profiles` | specify the spring profiles to activate | `github,auth` | | `config.envs` | specify the env names, e.g. `dev,pro` | `nil` | | `config.contextPath` | specify the context path, e.g. `/apollo`, then users could access portal via `http://{portal_address}/apollo` | `nil` | | `config.metaServers` | specify the meta servers, e.g.
    `dev: http://apollo-configservice-dev:8080`
    `pro: http://apollo-configservice-pro:8080` | `{}` | | `config.files` | specify the extra config files for apollo-portal, e.g. `application-ldap.yml` | `{}` | | `portaldb.host` | The host for apollo portal db | `nil` | | `portaldb.port` | The port for apollo portal db | `3306` | | `portaldb.dbName` | The database name for apollo portal db | `ApolloPortalDB` | | `portaldb.userName` | The user name for apollo portal db | `nil` | | `portaldb.password` | The password for apollo portal db | `nil` | | `portaldb.connectionStringProperties` | The connection string properties for apollo portal db | `characterEncoding=utf8` | | `portaldb.service.enabled` | Whether to create a Kubernetes Service for `portaldb.host` or not. Set it to `true` if `portaldb.host` is an endpoint outside of the kubernetes cluster | `false` | | `portaldb.service.fullNameOverride` | Override the service name for apollo portal db | `nil` | | `portaldb.service.port` | The port for the service of apollo portal db | `3306` | | `portaldb.service.type` | The service type of apollo portal db: `ClusterIP` or `ExternalName`. If the host is a DNS name, please specify `ExternalName` as the service type, e.g. `xxx.mysql.rds.aliyuncs.com` | `ClusterIP` | ##### 2.4.1.4.4 配置样例 ###### 2.4.1.4.4.1 PortalDB的host是k8s集群外的IP ```yaml portaldb: host: 1.2.3.4 dbName: ApolloPortalDBName userName: someUserName password: somePassword connectionStringProperties: characterEncoding=utf8&useSSL=false service: enabled: true ``` ###### 2.4.1.4.4.2 PortalDB的host是k8s集群外的域名 ```yaml portaldb: host: xxx.mysql.rds.aliyuncs.com dbName: ApolloPortalDBName userName: someUserName password: somePassword connectionStringProperties: characterEncoding=utf8&useSSL=false service: enabled: true type: ExternalName ``` ###### 2.4.1.4.4.3 PortalDB的host是k8s集群内的一个服务 ```yaml portaldb: host: apollodb-mysql.mysql dbName: ApolloPortalDBName userName: someUserName password: somePassword connectionStringProperties: characterEncoding=utf8&useSSL=false ``` ###### 2.4.1.4.4.4 配置环境信息 ```yaml config: envs: dev,pro metaServers: dev: http://apollo-service-dev-apollo-configservice:8080 pro: http://apollo-service-pro-apollo-configservice:8080 ``` ###### 2.4.1.4.4.5 以Load Balancer形式暴露服务 ```yaml service: type: LoadBalancer ``` ###### 2.4.1.4.4.6 以Ingress形式暴露服务 ```yaml ingress: enabled: true hosts: - paths: - / ``` ###### 2.4.1.4.4.7 以Ingress配置自定义路径`/apollo`形式暴露服务 ```yaml # use /apollo as root, should specify config.contextPath as /apollo ingress: enabled: true hosts: - paths: - /apollo config: ... contextPath: /apollo ... ``` ###### 2.4.1.4.4.8 以Ingress配置session affinity形式暴露服务 ```yaml ingress: enabled: true annotations: kubernetes.io/ingress.class: nginx nginx.ingress.kubernetes.io/affinity: "cookie" nginx.ingress.kubernetes.io/affinity-mode: "persistent" nginx.ingress.kubernetes.io/session-cookie-conditional-samesite-none: "true" nginx.ingress.kubernetes.io/session-cookie-expires: "172800" nginx.ingress.kubernetes.io/session-cookie-max-age: "172800" hosts: - host: xxx.somedomain.com # host is required to make session affinity work paths: - / ``` ###### 2.4.1.4.4.9 启用 LDAP 支持 ```yaml config: ... profiles: github,ldap ... files: application-ldap.yml: | spring: ldap: base: "dc=example,dc=org" username: "cn=admin,dc=example,dc=org" password: "password" search-filter: "(uid={0})" urls: - "ldap://xxx.somedomain.com:389" ldap: mapping: object-class: "inetOrgPerson" login-id: "uid" user-display-name: "cn" email: "mail" ``` #### 2.4.1.5 通过源码构建 Docker 镜像 如果修改了 apollo 服务端的代码,希望通过源码构建 Docker 镜像,可以参考[2.3.1.4 通过源码构建 Docker 镜像](#_2314-通过源码构建-docker-镜像)的步骤。 ### 2.4.2 基于内置的Eureka服务发现 感谢[AiotCEO](https://github.com/AiotCEO)提供了k8s的部署支持,使用说明可以参考[apollo-on-kubernetes](https://github.com/apolloconfig/apollo-on-kubernetes)。 感谢[qct](https://github.com/qct)提供的Helm Chart部署支持,使用说明可以参考[qct/apollo-helm](https://github.com/qct/apollo-helm)。 # 三、服务端配置说明 > 以下配置除了支持在数据库中配置以外,也支持通过-D参数、application.properties等配置,且-D参数、application.properties等优先级高于数据库中的配置 ## 3.1 调整ApolloPortalDB配置 配置项统一存储在ApolloPortalDB.ServerConfig表中,也可以通过`管理员工具 - 系统参数`页面进行配置,无特殊说明则修改完一分钟实时生效。 ### 3.1.1 apollo.portal.envs - 可支持的环境列表 默认值是dev,如果portal需要管理多个环境的话,以逗号分隔即可(大小写不敏感),如: ``` DEV,FAT,UAT,PRO ``` 修改完需要重启生效。 >注1:一套Portal可以管理多个环境,但是每个环境都需要独立部署一套Config Service、Admin Service和ApolloConfigDB,具体请参考:[2.1.2 创建ApolloConfigDB](#_212-创建apolloconfigdb),[3.2 调整ApolloConfigDB配置](zh/deployment/distributed-deployment-guide?id=_32-调整apolloconfigdb配置),[2.2.1.1.2 配置数据库连接信息](#_22112-配置数据库连接信息),另外如果是为已经运行了一段时间的Apollo配置中心增加环境,别忘了参考[2.1.2.4 从别的环境导入ApolloConfigDB的项目数据](#_2124-从别的环境导入apolloconfigdb的项目数据)对新的环境做初始化。 >注2:只在数据库添加环境是不起作用的,还需要为apollo-portal添加新增环境对应的meta server地址,具体参考:[2.2.1.1.2.4 配置apollo-portal的meta service信息](#_221124-配置apollo-portal的meta-service信息)。apollo-client在新的环境下使用时也需要做好相应的配置,具体参考:[1.2.2 Apollo Meta Server](zh/client/java-sdk-user-guide#_122-apollo-meta-server)。 >注3:如果希望添加自定义的环境名称,具体步骤可以参考[Portal如何增加环境](zh/faq/common-issues-in-deployment-and-development-phase?id=_4-portal如何增加环境?)。 >注4:1.1.0版本增加了系统信息页面(`管理员工具` -> `系统信息`),可以通过该页面检查配置是否正确 ### 3.1.2 apollo.portal.meta.servers - 各环境Meta Service列表 > 适用于1.6.0及以上版本 Apollo Portal需要在不同的环境访问不同的meta service(apollo-configservice)地址,所以我们需要在配置中提供这些信息。默认情况下,meta service和config service是部署在同一个JVM进程,所以meta service的地址就是config service的地址。 样例如下: ```json { "DEV":"http://1.1.1.1:8080", "FAT":"http://apollo.fat.xxx.com", "UAT":"http://apollo.uat.xxx.com", "PRO":"http://apollo.xxx.com" } ``` 修改完需要重启生效。 > 该配置优先级高于其它方式设置的Meta Service地址,更多信息可以参考[2.2.1.1.2.4 配置apollo-portal的meta service信息](#_221124-配置apollo-portal的meta-service信息)。 ### 3.1.3 organizations - 部门列表 Portal中新建的App都需要选择部门,所以需要在这里配置可选的部门信息,样例如下: ```json [{"orgId":"TEST1","orgName":"样例部门1"},{"orgId":"TEST2","orgName":"样例部门2"}] ``` ### 3.1.4 superAdmin - Portal超级管理员 超级管理员拥有所有权限,需要谨慎设置。 如果没有接入自己公司的SSO系统的话,可以先暂时使用默认值apollo(默认用户)。等接入后,修改为实际使用的账号,多个账号以英文逗号分隔(,)。 ### 3.1.5 consumer.token.salt - consumer token salt 如果会使用开放平台API的话,可以设置一个token salt。如果不使用,可以忽略。 ### 3.1.6 wiki.address portal上“帮助”链接的地址,默认是Apollo github的wiki首页,可自行设置。 ### 3.1.7 admin.createPrivateNamespace.switch 是否允许项目管理员创建private namespace。设置为`true`允许创建,设置为`false`则项目管理员在页面上看不到创建private namespace的选项。[了解更多Namespace](zh/design/apollo-core-concept-namespace) ### 3.1.8 emergencyPublish.supported.envs 配置允许紧急发布的环境列表,多个env以英文逗号分隔。 当config service开启一次发布只能有一个人修改开关(`namespace.lock.switch`)后,一次配置发布只能是一个人修改,另一个发布。为了避免遇到紧急情况时(如非工作时间、节假日)无法发布配置,可以配置此项以允许某些环境可以操作紧急发布,即同一个人可以修改并发布配置。 ### 3.1.9 configView.memberOnly.envs 只对项目成员显示配置信息的环境列表,多个env以英文逗号分隔。 对设定了只对项目成员显示配置信息的环境,只有该项目的管理员或拥有该namespace的编辑或发布权限的用户才能看到该私有namespace的配置信息和发布历史。公共namespace始终对所有用户可见。 > 从1.1.0版本开始支持,详见[PR 1531](https://github.com/apolloconfig/apollo/pull/1531) ### 3.1.10 role.create-application.enabled - 是否开启创建项目权限控制 > 适用于1.5.0及以上版本 默认为false,所有用户都可以创建项目 如果设置为true,那么只有超级管理员和拥有创建项目权限的帐号可以创建项目,超级管理员可以通过`管理员工具 - 系统权限管理`给用户分配创建项目权限 ### 3.1.11 role.manage-app-master.enabled - 是否开启项目管理员分配权限控制 > 适用于1.5.0及以上版本 默认为false,所有项目的管理员可以为项目添加/删除管理员 如果设置为true,那么只有超级管理员和拥有项目管理员分配权限的帐号可以为特定项目添加/删除管理员,超级管理员可以通过`管理员工具 - 系统权限管理`给用户分配特定项目的管理员分配权限 ### 3.1.12 admin-service.access.tokens - 设置apollo-portal访问各环境apollo-adminservice所需的access token > 适用于1.7.1及以上版本 如果对应环境的apollo-adminservice开启了[访问控制](#_326-admin-serviceaccesscontrolenabled-配置apollo-adminservice是否开启访问控制),那么需要在此配置apollo-portal访问该环境apollo-adminservice所需的access token,否则会访问失败 格式为json,如下所示: ```json { "dev" : "098f6bcd4621d373cade4e832627b4f6", "pro" : "ad0234829205b9033196ba818f7a872b" } ``` ### 3.1.13 searchByItem.switch - 控制台搜索框是否支持按配置项搜索 默认为 true,可以方便的按配置项快速搜索配置 如果设置为 false,则关闭此功能 ### 3.1.14 apollo.portal.search.perEnvMaxResults - 设置管理员工具-value的全局搜索功能单次单独环境最大搜索结果的数量 > 适用于2.4.0及以上版本 默认为200,意味着每个环境在单次搜索操作中最多返回200条结果 修改该参数可能会影响搜索功能的性能,因此在修改之前应该进行充分的测试,根据实际业务需求和系统资源情况,适当调整`apollo.portal.search.perEnvMaxResults`的值,以平衡性能和搜索结果的数量 ## 3.2 调整ApolloConfigDB配置 配置项统一存储在ApolloConfigDB.ServerConfig表中,需要注意每个环境的ApolloConfigDB.ServerConfig都需要单独配置,修改完一分钟实时生效。 ### 3.2.1 eureka.service.url - Eureka服务Url > 不适用于基于Kubernetes原生服务发现场景 不管是apollo-configservice还是apollo-adminservice都需要向eureka服务注册,所以需要配置eureka服务地址。 按照目前的实现,apollo-configservice本身就是一个eureka服务,所以只需要填入apollo-configservice的地址即可,如有多个,用逗号分隔(注意不要忘了/eureka/后缀)。 需要注意的是每个环境只填入自己环境的eureka服务地址,比如FAT的apollo-configservice是1.1.1.1:8080和2.2.2.2:8080,UAT的apollo-configservice是3.3.3.3:8080和4.4.4.4:8080,PRO的apollo-configservice是5.5.5.5:8080和6.6.6.6:8080,那么: 1. 在FAT环境的ApolloConfigDB.ServerConfig表中设置eureka.service.url为: ``` http://1.1.1.1:8080/eureka/,http://2.2.2.2:8080/eureka/ ``` 2. 在UAT环境的ApolloConfigDB.ServerConfig表中设置eureka.service.url为: ``` http://3.3.3.3:8080/eureka/,http://4.4.4.4:8080/eureka/ ``` 3. 在PRO环境的ApolloConfigDB.ServerConfig表中设置eureka.service.url为: ``` http://5.5.5.5:8080/eureka/,http://6.6.6.6:8080/eureka/ ``` >注1:这里需要填写本环境中全部的eureka服务地址,因为eureka需要互相复制注册信息 >注2:如果希望将Config Service和Admin Service注册到公司统一的Eureka上,可以参考[部署&开发遇到的常见问题 - 将Config Service和Admin Service注册到单独的Eureka Server上](zh/faq/common-issues-in-deployment-and-development-phase#_8-将config-service和admin-service注册到单独的eureka-server上)章节 >注3:在多机房部署时,往往希望config service和admin service只向同机房的eureka注册,要实现这个效果,需要利用`ServerConfig`表中的cluster字段,config service和admin service会读取所在机器的`/opt/settings/server.properties`(Mac/Linux)或`C:\opt\settings\server.properties`(Windows)中的idc属性,如果该idc有对应的eureka.service.url配置,那么就只会向该机房的eureka注册。比如config service和admin service会部署到`SHAOY`和`SHAJQ`两个IDC,那么为了实现这两个机房中的服务只向该机房注册,那么可以在`ServerConfig`表中新增两条记录,分别填入`SHAOY`和`SHAJQ`两个机房的eureka地址即可,`default` cluster的记录可以保留,如果有config service和admin service不是部署在`SHAOY`和`SHAJQ`这两个机房的,就会使用这条默认配置。 | Key |Cluster | Value | Comment | |--------------------|-----------|-------------------------------|---------------------| | eureka.service.url | default | http://1.1.1.1:8080/eureka/ | 默认的Eureka服务Url | | eureka.service.url | SHAOY | http://2.2.2.2:8080/eureka/ | SHAOY的Eureka服务Url | | eureka.service.url | SHAJQ | http://3.3.3.3:8080/eureka/ | SHAJQ的Eureka服务Url | ### 3.2.2 namespace.lock.switch - 一次发布只能有一个人修改开关,用于发布审核 这是一个功能开关,如果配置为true的话,那么一次配置发布只能是一个人修改,另一个发布。 > 生产环境建议开启此选项 ### 3.2.3 config-service.cache.enabled - 是否开启配置缓存 这是一个功能开关,如果配置为true的话,config service会缓存加载过的配置信息,从而加快后续配置获取性能。 默认为false,开启前请先评估总配置大小并调整config service内存配置。 > 开启缓存后必须确保应用中配置的`app.id`、`apollo.cluster`大小写正确,否则将获取不到正确的配置,另可参考`config-service.cache.key.ignore-case`配置做兼容处理。 > `config-service.cache.enabled` 配置调整必须重启 config service 才能生效 #### 3.2.3.1 config-service.cache.key.ignore-case - 是否忽略配置缓存key的大小写 > 适用于2.2.0及以上版本 该配置作用于`config-service.cache.enabled`为 true 时,用于控制配置缓存key是否忽略大小写。 默认为 false,即缓存键大小写严格匹配。此时需要确保应用中配置的`app.id`、`apollo.cluster`大小写正确,否则将获取不到正确的配置。可配置为 true, 则忽略大小写。 > 这个配置用于兼容未开启缓存时的配置获取逻辑,因为 MySQL 数据库查询默认字符串匹配大小写不敏感。如果开启了缓存,且用了 MySQL,建议配置 true。如果你 Apollo 使用的数据库字符串匹配大小写敏感,那么必须保持默认配置 false,否则将获取不到配置。 #### 3.2.3.2 config-service.cache.stats.enabled - 是否开启缓存metric统计功能 > 适用于2.4.0及以上版本 > `config-service.cache.stats.enabled` 配置调整必须重启 config service 才能生效 该配置作用于`config-service.cache.stats.enabled`为 true 时,用于控制开启缓存统计功能。 默认为 false,即不会开启缓存统计功能,当配置为 true 时,开启缓存metric统计功能 指标查看参考[监控相关-5.2 Metrics](zh/design/apollo-design#5.2-Metrics),如`http://${someIp:somePort}/prometheus` ### 3.2.4 item.key.length.limit - 配置项 key 最大长度限制 默认配置是128。 ### 3.2.5 item.value.length.limit - 配置项 value 最大长度限制 默认配置是20000。 #### 3.2.5.1 appid.value.length.limit.override - appId 维度的配置项 value 最大长度限制 此配置用来覆盖 `item.value.length.limit` 的配置,做到控制 appId 粒度下的 value 最大长度限制,配置的值是一个 json 格式,json 的 key 为 appId,格式如下: ``` appid.value.length.limit.override = {"appId-demo1":200,"appId-demo2":300} ``` 以上配置指定了 `appId-demo1` 下的所有 namespace 中的 value 最大长度限制为 200,`appId-demo2` 下的所有 namespace 中的 value 最大长度限制为 300 当 `appId-demo1` 或 `appId-demo2` 下新建的 namespace 时,会自动继承该 namespace 的 value 最大长度限制,除非该 namespace 的配置项 value 最大长度限制被 `namespace.value.length.limit.override` 覆盖。 #### 3.2.5.2 namespace.value.length.limit.override - namespace 的配置项 value 最大长度限制 此配置用来覆盖 `item.value.length.limit` 或者 `appid.value.length.limit.override` 的配置,做到细粒度控制 namespace 的 value 最大长度限制,配置的值是一个 json 格式,json 的 key 为 namespace 在数据库中的 id 值,格式如下: ``` namespace.value.length.limit.override = {1:200,3:20} ``` 以上配置指定了 ApolloConfigDB.Namespace 表中 id=1 的 namespace 的 value 最大长度限制为 200,id=3 的 namespace 的 value 最大长度限制为 20 ### 3.2.6 admin-service.access.control.enabled - 配置apollo-adminservice是否开启访问控制 > 适用于1.7.1及以上版本 默认为false,如果配置为true,那么apollo-portal就需要[正确配置](#_3112-admin-serviceaccesstokens-设置apollo-portal访问各环境apollo-adminservice所需的access-token)访问该环境的access token,否则访问会被拒绝 ### 3.2.7 admin-service.access.tokens - 配置允许访问apollo-adminservice的access token列表 > 适用于1.7.1及以上版本 如果该配置项为空,那么访问控制不会生效。如果允许多个token,token 之间以英文逗号分隔 样例: ```properties admin-service.access.tokens=098f6bcd4621d373cade4e832627b4f6 admin-service.access.tokens=098f6bcd4621d373cade4e832627b4f6,ad0234829205b9033196ba818f7a872b ``` ### 3.2.8 apollo.access-key.auth-time-diff-tolerance - 配置服务端AccessKey校验容忍的时间偏差 > 适用于2.0.0及以上版本 默认值为60,单位为秒。由于密钥认证时需要校验时间,客户端与服务端的时间可能存在时间偏差,如果偏差太大会导致认证失败,此配置可以配置容忍的时间偏差大小,默认为60秒。 ### 3.2.9 apollo.eureka.server.security.enabled - 配置是否开启eureka server的登录认证 > 适用于2.1.0及以上版本 默认为false,如果希望提升安全性(比如公网可访问的场景),可以设置该配置项为true启用登录认证。 需要注意的是,开启登录认证后,[eureka.service.url](#_321-eurekaserviceurl-eureka服务url)中的地址需要配置用户名和密码,如: ``` http://some-user-name:some-password@1.1.1.1:8080/eureka/,http://some-user-name:some-password@2.2.2.2:8080/eureka/ ``` 其中`some-user-name`和`some-password`需要和`apollo.eureka.server.security.username`以及`apollo.eureka.server.security.password`的配置项一致。 修改完需要重启生效。 ### 3.2.10 apollo.eureka.server.security.username - 配置eureka server的登录用户名 > 适用于2.1.0及以上版本 配置eureka server的登录用户名,需要和[apollo.eureka.server.security.enabled](#_329-apolloeurekaserversecurityenabled-配置是否开启eureka-server的登录认证)一起使用。 修改完需要重启生效。 > 注意用户名不能配置为apollo ### 3.2.11 apollo.eureka.server.security.password - 配置eureka server的登录密码 > 适用于2.1.0及以上版本 配置eureka server的登录密码,需要和[apollo.eureka.server.security.enabled](#_329-apolloeurekaserversecurityenabled-配置是否开启eureka-server的登录认证)一起使用。 修改完需要重启生效。 ### 3.2.12 apollo.release-history.retention.size - 配置发布历史的保留数量 > 适用于2.2.0及以上版本 默认为 -1,表示不限制保留数量。如果配置为正整数(最小值为 1,必须保留一条历史记录,保障基本的配置功能),则只会保留最近的指定数量的发布历史。这是为了防止发布历史过多导致数据库压力过大,建议根据业务对配置回滚的需求来配置该值。该配置项是全局的,清理时是以 appId+clusterName+namespaceName+branchName 为维度清理的。 ### 3.2.13 apollo.release-history.retention.size.override - 细粒度配置发布历史的保留数量 > 适用于2.2.0及以上版本 此配置用来覆盖 `apollo.release-history.retention.size` 的配置,做到细粒度控制 appId+clusterName+namespaceName+branchName 的发布历史保留数量,配置的值是一个 JSON 格式,JSON 的 key 为 appId、clusterName、namespaceName、branchName 使用 + 号的拼接值,格式如下: ``` json { "kl+bj+namespace1+bj": 10, "kl+bj+namespace2+bj": 20 } ``` 以上配置指定了 appId=kl、clusterName=bj、namespaceName=namespace1、branchName=bj 的发布历史保留数量为 10,appId=kl、clusterName=bj、namespaceName=namespace2、branchName=bj 的发布历史保留数量为 20,branchName 一般等于 clusterName,只有灰度发布时才会不同,灰度发布的 branchName 需要查询数据库 ReleaseHistory 表确认。 ### 3.2.14 instance.config.audit.max.size - 客户端拉取审计记录的队列大小 > 适用于2.5.0及以上版本 默认为 10000,最小为10,用于控制客户端拉取审计记录的队列大小,超过队列大小后会丢弃最早的审计记录。 修改完需要重启生效。 ### 3.2.15 instance.cache.max.size - 实例缓存的最大数量 > 适用于2.5.0及以上版本 默认为 50000,最小为10,用于控制实例缓存的最大数量,当缓存超过最大容量时,会触发缓存淘汰(Eviction) 机制。 修改完需要重启生效。 ### 3.2.16 instance.config.cache.max.size - 实例配置的缓存最大数量 > 适用于2.5.0及以上版本 默认为 50000,最小为10,用于控制实例配置的缓存最大数量,当缓存超过最大容量时,会触发缓存淘汰(Eviction) 机制。 修改完需要重启生效。 ### 3.2.17 instance.config.audit.time.threshold.minutes - 实例拉取审计记录的间隔时间 > 适用于2.5.0及以上版本 时间阈值单位为分钟,默认为 10,最小为5,用于控制在保存/更新客户端拉取配置审计记录时,当2次请求记录间隔大于该值时,才会保存/更新拉取记录,小于该值时,不会保存/更新拉取记录。 ### 3.2.18 config-service.incremental.change.enabled - 是否开启增量配置同步客户端 > 适用于服务端2.5.0及以上版本 && Java客户端2.4.0及以上版本 这是一个功能开关,如果配置为true的话,config service会缓存加载过的配置信息,发送给客户端增量配置,减少客户端对服务端的网络压力。 默认为false,开启前请先评估总配置大小并调整config service内存配置。 > 开启缓存后必须确保应用中配置的`app.id`、`apollo.cluster` > 大小写正确,否则将获取不到正确的配置,另可参考`config-service.cache.key.ignore-case`配置做兼容处理。 > `config-service.incremental.change.enabled` 配置调整必须重启 config service 才能生效 ================================================ FILE: docs/zh/deployment/quick-start-docker.md ================================================ 如果您对Docker非常熟悉,可以使用Docker的方式快速部署Apollo,从而快速的了解Apollo。如果您对Docker并不是很了解,请参考[常规方式部署Quick Start](zh/deployment/quick-start)。 另外需要说明的是,不管是Docker方式部署Quick Start还是常规方式部署的,Quick Start只是用来快速入门、了解Apollo。如果部署Apollo在公司中使用,请参考[分布式部署指南](zh/deployment/distributed-deployment-guide)。 > 由于Docker对windows的支持并不是很好,所以不建议您在windows环境下使用Docker方式部署,除非您对windows docker非常了解 ## 一、 准备工作 ### 1.1 安装Docker 具体步骤可以参考[Docker安装指南](https://yeasy.gitbooks.io/docker_practice/content/install/),通过以下命令测试是否成功安装 ``` docker -v ``` 为了加速Docker镜像下载,建议[配置镜像加速器](https://yeasy.gitbooks.io/docker_practice/content/install/mirror.html)。 ### 1.2 下载Docker Quick Start配置文件 下载[docker-compose.yml](https://github.com/apolloconfig/apollo-quick-start/blob/master/docker-compose.yml)和[sql 文件夹](https://github.com/apolloconfig/apollo-quick-start/tree/master/sql)到本地目录,如 docker-quick-start。 > 如果使用的是 arm 架构的机器,例如 mac m1,需要下载[docker-compose-arm64.yml](https://github.com/apolloconfig/apollo-quick-start/blob/master/docker-compose-arm64.yml) ```bash - docker-quick-start - docker-compose.yml - sql - apolloconfigdb.sql - apolloportaldb.sql ``` ## 二、启动Apollo配置中心 在docker-quick-start目录下执行`docker-compose up`,第一次执行会触发下载镜像等操作,需要耐心等待一些时间。 > 如果使用的是 arm 架构的机器,例如 mac m1,执行 `docker-compose -f docker-compose-arm64.yml up` 搜索所有`apollo-quick-start`开头的日志,看到以下日志说明启动成功: ```log apollo-quick-start | ==== starting service ==== apollo-quick-start | Service logging file is ./service/apollo-service.log apollo-quick-start | Started [45] apollo-quick-start | Waiting for config service startup....... apollo-quick-start | Config service started. You may visit http://localhost:8080 for service status now! apollo-quick-start | Waiting for admin service startup...... apollo-quick-start | Admin service started apollo-quick-start | ==== starting portal ==== apollo-quick-start | Portal logging file is ./portal/apollo-portal.log apollo-quick-start | Started [254] apollo-quick-start | Waiting for portal startup....... apollo-quick-start | Portal started. You can visit http://localhost:8070 now! ``` > 注1:数据库的端口映射为13306,所以如果希望在宿主机上访问数据库,可以通过localhost:13306,用户名是root,密码留空。 > 注2:如要查看更多服务的日志,可以通过`docker exec -it apollo-quick-start bash`登录, 然后到`/apollo-quick-start/service`和`/apollo-quick-start/portal`下查看日志信息。 ## 三、使用Apollo配置中心 使用相关步骤可以参考[Quick Start - 四、使用Apollo配置中心](zh/deployment/quick-start#四、使用apollo配置中心) 需要注意的是,在Docker环境下需要通过下面的命令运行Demo客户端: ```bash docker exec -i apollo-quick-start /apollo-quick-start/demo.sh client ``` 默认情况下 apollo-configservice 只会注册内网 IP,只有通过上述命令启动的客户端能连通,如果希望外部的客户端也能访问,请参考[网络策略](zh/deployment/distributed-deployment-guide?id=_14-网络策略)。 ================================================ FILE: docs/zh/deployment/quick-start.md ================================================ 为了让大家更快地上手了解Apollo配置中心,我们这里准备了一个Quick Start,能够在几分钟内在本地环境部署、启动Apollo配置中心。 考虑到Docker的便捷性,我们还提供了Quick Start的Docker版本,如果你对Docker比较熟悉的话,可以参考[Apollo Quick Start Docker部署](zh/deployment/quick-start-docker)通过Docker快速部署Apollo。 不过这里需要注意的是,Quick Start只针对本地测试使用,如果要部署到生产环境,还请另行参考[分布式部署指南](zh/deployment/distributed-deployment-guide)。 > 注:Quick Start需要有bash环境,Windows用户请安装[Git Bash](https://git-for-windows.github.io/),建议使用最新版本,老版本可能会遇到未知问题。也可以直接通过IDE环境启动,详见[Apollo开发指南](zh/contribution/apollo-development-guide)。 #   # 一、准备工作 ## 1.1 Java * Apollo服务端:17+ * Apollo客户端:1.8+ * 如需运行在 Java 1.7 运行时环境,请使用 1.x 版本的 apollo 客户端,如 1.9.1 在配置好后,可以通过如下命令检查: ```sh java -version ``` 样例输出: ```sh java version "17.0.14" Java(TM) SE Runtime Environment (build 17.0.14+7) Java HotSpot(TM) 64-Bit Server VM (build 17.0.14+7, mixed mode) ``` Windows用户请确保JAVA_HOME环境变量已经设置。 ## 1.2 MySQL * 如果使用 H2 内存数据库/H2 文件数据库,则无需 MySQL,可以跳过此步骤 * 版本要求:5.6.5+ Apollo的表结构对`timestamp`使用了多个default声明,所以需要5.6.5以上版本。 连接上MySQL后,可以通过如下命令检查: ```sql SHOW VARIABLES WHERE Variable_name = 'version'; ``` | Variable_name | Value | |---------------|--------| | version | 5.7.11 | ## 1.3 下载Quick Start安装包 我们准备好了一个Quick Start安装包,大家只需要下载到本地,就可以直接使用,免去了编译、打包过程。 安装包共50M,如果访问github网速不给力的话,可以从百度网盘下载。 1. 从GitHub下载 * checkout或下载[apollo-quick-start项目](https://github.com/apolloconfig/apollo-quick-start) * **由于Quick Start项目比较大,所以放在了另外的repository,请注意项目地址** * https://github.com/apolloconfig/apollo-quick-start 2. 从百度网盘下载 * 通过[网盘链接](https://pan.baidu.com/s/1Ieelw6y3adECgktO0ea0Gg)下载,提取码: 9wwe * 下载到本地后,在本地解压apollo-quick-start.zip 3. 为啥安装包要58M这么大? * 因为这是一个可以自启动的jar包,里面包含了所有依赖jar包以及一个内置的tomcat容器 ### 1.3.1 手动打包Quick Start安装包 Quick Start只针对本地测试使用,所以一般用户不需要自己下载源码打包,只需要下载已经打好的包即可。不过也有部分用户希望在修改代码后重新打包,那么可以参考如下步骤: 1. 修改apollo-configservice, apollo-adminservice和apollo-portal的pom.xml,注释掉spring-boot-maven-plugin和maven-assembly-plugin 2. 在根目录下执行`mvn clean package -pl apollo-assembly -am -DskipTests=true` 3. 复制apollo-assembly/target下的jar包,rename为apollo-all-in-one.jar # 二、数据库初始化及启动 #### 注意事项 1. apollo 服务端进程需要分别使用8070, 8080, 8090端口,请确保这3个端口当前没有被使用。 2. 脚本中的 SPRING_PROFILES_ACTIVE 环境变量中的 `github` 是必须的 profile,`database-discovery` 指定使用数据库服务发现, `auth` 是 portal 提供简单认证的 profile,不需要认证或者使用其它认证方式时可以去掉 ## 2.1 使用 H2 内存数据库,自动初始化 无需任何配置,直接使用如下命令启动即可 > 注:使用内存数据库时,任何操作都会在 apollo 进程重启后丢失 ```bash export SPRING_PROFILES_ACTIVE="github,database-discovery,auth" unset SPRING_SQL_CONFIG_INIT_MODE unset SPRING_SQL_PORTAL_INIT_MODE java -jar apollo-all-in-one.jar ``` ## 2.2 使用 H2 文件数据库,自动初始化 #### 注意事项 1. 脚本中环境变量中的路径 `~/apollo/apollo-config-db` 和 `~/apollo/apollo-portal-db` 可以替换为其它自定义路径,需要保证该路径有读写权限 ### 2.2.1 首次启动 首次启动使用 SPRING_SQL_CONFIG_INIT_MODE="always" 和 SPRING_SQL_PORTAL_INIT_MODE="always" 环境变量来进行初始化 ```bash export SPRING_PROFILES_ACTIVE="github,database-discovery,auth" # config db export SPRING_SQL_CONFIG_INIT_MODE="always" export SPRING_CONFIG_DATASOURCE_URL="jdbc:h2:file:~/apollo/apollo-config-db;mode=mysql;DB_CLOSE_ON_EXIT=FALSE;DB_CLOSE_DELAY=-1;BUILTIN_ALIAS_OVERRIDE=TRUE;DATABASE_TO_UPPER=FALSE" # portal db export SPRING_SQL_PORTAL_INIT_MODE="always" export SPRING_PORTAL_DATASOURCE_URL="jdbc:h2:file:~/apollo/apollo-portal-db;mode=mysql;DB_CLOSE_ON_EXIT=FALSE;DB_CLOSE_DELAY=-1;BUILTIN_ALIAS_OVERRIDE=TRUE;DATABASE_TO_UPPER=FALSE" java -jar apollo-all-in-one.jar ``` ### 2.2.2 后续启动 后续启动去掉 SPRING_SQL_CONFIG_INIT_MODE 和 SPRING_SQL_PORTAL_INIT_MODE 环境变量来避免重复初始化 ```bash export SPRING_PROFILES_ACTIVE="github,database-discovery,auth" # config db unset SPRING_SQL_CONFIG_INIT_MODE export SPRING_CONFIG_DATASOURCE_URL="jdbc:h2:file:~/apollo/apollo-config-db;mode=mysql;DB_CLOSE_ON_EXIT=FALSE;DB_CLOSE_DELAY=-1;BUILTIN_ALIAS_OVERRIDE=TRUE;DATABASE_TO_UPPER=FALSE" # portal db unset SPRING_SQL_PORTAL_INIT_MODE export SPRING_PORTAL_DATASOURCE_URL="jdbc:h2:file:~/apollo/apollo-portal-db;mode=mysql;DB_CLOSE_ON_EXIT=FALSE;DB_CLOSE_DELAY=-1;BUILTIN_ALIAS_OVERRIDE=TRUE;DATABASE_TO_UPPER=FALSE" java -jar apollo-all-in-one.jar ``` ## 2.3 使用 mysql 数据库,自动初始化 #### 注意事项 1. 脚本环境变量中的 your-mysql-server:3306 需要替换为实际的 mysql 服务器地址和端口,ApolloConfigDB 和 ApolloPortalDB 需要替换为实际的数据库名称 2. 脚本环境变量中的 "apollo-username" 和 "apollo-password" 需要填写实际的用户名和密码 ### 2.3.1 首次启动 首次启动使用 SPRING_SQL_INIT_MODE="always" 环境变量来进行初始化 ```bash export SPRING_PROFILES_ACTIVE="github,database-discovery,auth" # config db export SPRING_SQL_CONFIG_INIT_MODE="always" export SPRING_CONFIG_DATASOURCE_URL="jdbc:mysql://your-mysql-server:3306/ApolloConfigDB?useUnicode=true&characterEncoding=UTF8" export SPRING_CONFIG_DATASOURCE_USERNAME="apollo-username" export SPRING_CONFIG_DATASOURCE_PASSWORD="apollo-password" # portal db export SPRING_SQL_PORTAL_INIT_MODE="always" export SPRING_PORTAL_DATASOURCE_URL="jdbc:mysql://your-mysql-server:3306/ApolloPortalDB?useUnicode=true&characterEncoding=UTF8" export SPRING_PORTAL_DATASOURCE_USERNAME="apollo-username" export SPRING_PORTAL_DATASOURCE_PASSWORD="apollo-password" java -jar apollo-all-in-one.jar ``` ### 2.3.2 后续启动 后续启动去掉 SPRING_SQL_CONFIG_INIT_MODE 和 SPRING_SQL_PORTAL_INIT_MODE 环境变量来避免重复初始化 ```bash export SPRING_PROFILES_ACTIVE="github,database-discovery,auth" # config db unset SPRING_SQL_CONFIG_INIT_MODE export SPRING_CONFIG_DATASOURCE_URL="jdbc:mysql://your-mysql-server:3306/ApolloConfigDB?useUnicode=true&characterEncoding=UTF8" export SPRING_CONFIG_DATASOURCE_USERNAME="apollo-username" export SPRING_CONFIG_DATASOURCE_PASSWORD="apollo-password" # portal db unset SPRING_SQL_PORTAL_INIT_MODE export SPRING_PORTAL_DATASOURCE_URL="jdbc:mysql://your-mysql-server:3306/ApolloPortalDB?useUnicode=true&characterEncoding=UTF8" export SPRING_PORTAL_DATASOURCE_USERNAME="apollo-username" export SPRING_PORTAL_DATASOURCE_PASSWORD="apollo-password" java -jar apollo-all-in-one.jar ``` ## 2.4 使用 mysql 数据库,手动初始化 ### 2.4.1 手动初始化 ApolloConfigDB 和 ApolloPortalDB ApolloConfigDB 通过各种MySQL客户端导入[apolloconfigdb.sql](https://github.com/apolloconfig/apollo/blob/master/scripts/sql/profiles/mysql-default/apolloconfigdb.sql)即可。 ApolloPortalDB 通过各种MySQL客户端导入[apolloportaldb.sql](https://github.com/apolloconfig/apollo/blob/master/scripts/sql/profiles/mysql-default/apolloportaldb.sql)即可。 ### 2.4.2 运行 #### 注意事项 1. 脚本环境变量中的 your-mysql-server:3306 需要替换为实际的 mysql 服务器地址和端口,ApolloConfigDB 和 ApolloPortalDB 需要替换为实际的数据库名称 2. 脚本环境变量中的 "apollo-username" 和 "apollo-password" 需要填写实际的用户名和密码 ```bash export SPRING_PROFILES_ACTIVE="github,database-discovery,auth" # config db unset SPRING_SQL_CONFIG_INIT_MODE export SPRING_CONFIG_DATASOURCE_URL="jdbc:mysql://your-mysql-server:3306/ApolloConfigDB?useUnicode=true&characterEncoding=UTF8" export SPRING_CONFIG_DATASOURCE_USERNAME="apollo-username" export SPRING_CONFIG_DATASOURCE_PASSWORD="apollo-password" # portal db unset SPRING_SQL_PORTAL_INIT_MODE export SPRING_PORTAL_DATASOURCE_URL="jdbc:mysql://your-mysql-server:3306/ApolloPortalDB?useUnicode=true&characterEncoding=UTF8" export SPRING_PORTAL_DATASOURCE_USERNAME="apollo-username" export SPRING_PORTAL_DATASOURCE_PASSWORD="apollo-password" java -jar apollo-all-in-one.jar ``` # 三、注意 Quick Start只是用来帮助大家快速体验Apollo项目,具体实际使用时请参考:[分布式部署指南](zh/deployment/distributed-deployment-guide)。 另外需要注意的是Quick Start不支持增加环境,只有通过分布式部署才可以新增环境,同样请参考:[分布式部署指南](zh/deployment/distributed-deployment-guide) # 四、使用Apollo配置中心 ## 4.1 使用样例项目 ### 4.1.1 初始化样例配置 1. 打开http://localhost:8070 > Quick Start集成了[Spring Security简单认证](zh/extension/portal-how-to-implement-user-login-function#实现方式一:使用apollo提供的spring-security简单认证),更多信息可以参考[Portal 实现用户登录功能](zh/extension/portal-how-to-implement-user-login-function) 登录 2. 输入用户名apollo,密码admin后登录 ![首页](https://cdn.jsdelivr.net/gh/apolloconfig/apollo-quick-start@master/images/apollo-sample-home.jpg) 3. 点击创建应用,输入`SampleApp`信息并提交 ![创建应用](https://cdn.jsdelivr.net/gh/apolloconfig/apollo-quick-start@master/images/apollo-create-sample-app.jpg) 4. 进入SampleApp配置界面,点击新增配置,输入`timeout`信息并提交 ![创建配置](https://cdn.jsdelivr.net/gh/apolloconfig/apollo-quick-start@master/images/apollo-create-sample-config.jpg) 5. 点击发布按钮,并填写发布信息 ![配置界面](https://cdn.jsdelivr.net/gh/apolloconfig/apollo-quick-start@master/images/sample-app-config.jpg) ![发布界面](https://cdn.jsdelivr.net/gh/apolloconfig/apollo-quick-start@master/images/sample-app-release-detail.jpg) ### 4.1.2 运行客户端程序 我们准备了一个简单的[Demo客户端](https://github.com/apolloconfig/apollo-demo-java/blob/main/api-demo/src/main/java/com/apolloconfig/apollo/demo/api/SimpleApolloConfigDemo.java)来演示从Apollo配置中心获取配置。 程序很简单,就是用户输入一个key的名字,程序会输出这个key对应的值。 如果没找到这个key,则输出undefined。 同时,客户端还会监听配置变化事件,一旦有变化就会输出变化的配置信息。 运行`./demo.sh client`启动Demo客户端,忽略前面的调试信息,可以看到如下提示: ```sh Apollo Config Demo. Please input key to get the value. Input quit to exit. > ``` 输入`timeout`,会看到如下信息: ```sh > timeout Loading key : timeout with value: 1000 ``` > 如果运行客户端遇到问题,可以通过修改`client/log4j2.xml`中的level为DEBUG来查看更详细日志信息 > ```xml > > > > ``` ### 4.1.3 修改配置并发布 回到配置界面,修改`timeout`配置项的值为2000,并发布配置。 ![修改配置](https://cdn.jsdelivr.net/gh/apolloconfig/apollo-quick-start@master/images/sample-app-modify-config.jpg) ### 4.1.4 客户端查看修改后的值 如果客户端一直在运行的话,在配置发布后就会监听到配置变化,并输出修改的配置信息: ```sh Changes for namespace application Change - key: timeout, oldValue: 1000, newValue: 2000, changeType: MODIFIED ``` 再次输入`timeout`查看对应的值,会看到如下信息: ```sh > timeout Loading key : timeout with value: 2000 ``` ## 4.2 使用新的项目 ### 4.2.1 应用接入Apollo 这部分可以参考[Java应用接入指南](zh/client/java-sdk-user-guide) ### 4.2.2 运行客户端程序 由于使用了新的项目,所以客户端需要修改appId信息。 编辑`client/META-INF/app.properties`,修改app.id为你新创建的app id。 ```properties app.id=你的appId ``` 运行`./demo.sh client`启动Demo客户端即可。 ================================================ FILE: docs/zh/deployment/third-party-tool-btpanel.md ================================================ # 基于宝塔面板部署 Apollo ## 前提 - 仅适用于宝塔面板9.2.0及以上版本 - 安装宝塔面板,前往[宝塔面板](https://www.bt.cn/new/index.html)官网,选择正式版的脚本下载安装 ## 部署 1. 登录宝塔面板,在左侧菜单栏中点击 `Docker` ![Docker](../images/deployment/btpanel/docker-menu.png) 2. 首次会提示安装`Docker`和`Docker Compose`服务,点击立即安装,若已安装请忽略。 ![安装环境](../images/deployment/btpanel/install-docker.png) 3. 安装完成后在`Docker-应用商店`中找到 `Apollo`,点击`安装` ![安装](../images/deployment/btpanel/search-apollo.png) 4. 设置域名等基本信息,点击`确定` - 名称:应用名称,默认`apollo_随机字符` - 版本选择:默认`latest` - 允许外部访问:如您需通过`IP+Port`直接访问,请勾选,如您已经设置了域名,请不要勾选此处 - WEB 端口:默认`8070`,可自行修改 - 通信端口:默认`8080`,可自行修改 - 元数据端口:默认`8090`,可自行修改 - **安全须知:** - 请确保这些端口没有直接暴露在互联网上 - 配置您的防火墙以限制对这些端口的访问 - 验证这些端口没有被其他服务占用 5. 提交后面板会自动进行应用初始化,大概需要`1-3`分钟,初始化完成后即可访问 ## 访问 Apollo - 请在浏览器地址栏中输入域名访问 `http://<宝塔面板IP>:8070`,即可访问 `Apollo` 控制台。 ![控制台](../images/deployment/btpanel/console.png) > 默认登录信息: username `apollo`, password `admin`,为了服务器安全,请在登陆后立即修改密码。 ================================================ FILE: docs/zh/deployment/third-party-tool-rainbond.md ================================================ #   # 一、背景信息 当前文档描述如何通过云原生应用管理平台 [Rainbond](https://www.rainbond.com/?channel=apollo) 一键安装高可用 Apollo 集群。这种方式适合给不太了解 Kubernetes、容器化等复杂技术的用户使用,降低了在 Kubernetes 中部署 Apollo 的门槛。 ## 1.1 Rainbond 与 Apollo 的结合 [Rainbond](https://www.rainbond.com/?channel=apollo) 是一款易于使用的开源云原生应用管理平台。 借助于它,用户可以在图形化界面中完成微服务的部署与运维。 借助 Kubernetes 和容器化技术的能力,将故障自愈、弹性伸缩等自动化运维能力赋能给用户的业务。 Rainbond 内置原生 Service Mesh 微服务框架,同时与 Spring Cloud、Dubbo 等其他微服务框架也有很好的整合体验。 故而大量的 Rainbond 用户也可能是 Apollo 分布式配置管理中心的用户。 这类用户不必再关心如何部署 Apollo 集群,Rainbond 团队将 Apollo 制作成为可以一键部署的应用模版,供开源用户免费下载安装。 这种安装方式极大的降低了用户使用 Apollo 集群的部署负担,目前支持 1.9.2,2.0.1版本。 当前的安装方式,默认集成了一套 `PRO` 环境,追加其他环境,参见后文中的高级特性章节。 ## 1.2 关于应用模版 应用模版是面向 Rainbond 云原生应用管理平台的安装包,用户可以基于它一键安装业务系统到自己的 Rainbond 中去。无论这个业务系统多么复杂,应用模版都会将其抽象成为一个应用,裹挟着应用内所有组件的镜像、配置信息以及所有组件之间的关联关系一并安装起来。 # 二、前提条件 - 部署好的 Rainbond 云原生应用管理平台:例如 [快速体验版本](https://www.rainbond.com/docs/quick-start/quick-install/?channel=apollo),可以在个人 PC 环境中以启动一个容器的代价运行。 - 可以连接到互联网。 # 三、快速开始 ## 3.1 访问内置的开源应用商店 选择左侧的 **应用市场** 标签页,在页面中切换到 **开源应用商店** 标签页,搜索关键词 **apollo** 即可找到 Apollo 应用。 ![apollo-1](https://grstatic.oss-cn-shanghai.aliyuncs.com/wechat/apollo/apollo2.0.1/apollo-1.jpg) ## 3.2 一键安装 点击 Apollo 右侧的 **安装** 可以进入安装页面,填写简单的信息之后,点击 **确定** 即可开始安装,页面自动跳转到拓扑视图。 ![apollo-2](https://grstatic.oss-cn-shanghai.aliyuncs.com/wechat/apollo/apollo2.0.1/apollo-2.jpg) 参数说明: | 选择项 | 说明 | | -------- | ------------------------------------------------------------ | | 团队名称 | 用户自建的工作空间,以命名空间隔离 | | 集群名称 | 选择 Apollo 被部署到哪一个 K8s 集群 | | 选择应用 | 选择 Apollo 被部署到哪一个应用,应用中包含有若干有关联的组件 | | 应用版本 | 选择 Apollo 的版本,目前可选版本为 1.9.2,2.0.1 | 等待几分钟后,Apollo 集群就会安装完成,并运行起来。 ![apollo-3](https://grstatic.oss-cn-shanghai.aliyuncs.com/wechat/apollo/apollo2.0.1/apollo-3.jpg) ## 3.3 测试 访问组件 `Apollo-portal-2.0.1` 所提供的默认域名,即可登录 Apollo 控制台,在系统信息中,验证 `PRO` 环境已经就绪。 ![apollo-4](https://grstatic.oss-cn-shanghai.aliyuncs.com/wechat/apollo/apollo2.0.1/apollo-4.jpg) ## 3.4 配置 在 Rainbond 中,可以基于图形化界面对 Apollo 集群进行配置。主要包括环境变量、配置文件挂载、插件配置三个方面。 - 环境变量:通过在不同的组件页面中的环境配置中,可以自定义环境变量。比如为 `Apollo-portal-2.0.1` 默认添加了 `APOLLO_PORTAL_ENVS=pro` 用于定义当前 portal 纳管的环境。 - 配置文件:通过在不同的组件页面中的环境配置中,可以为组件设置配置文件。 - `Apollo-portal-2.0.1` 挂载 `/apollo-portal/config/apollo-env.properties` 用于定义不同环境的 meta 地址。 - `Apollo-config-2.0.1` 挂载 `/apollo-configservice/config/application-github.properties` 用于声明当前环境 config 和 admin 的服务地址。 - 插件配置:在 Rainbond 中通过为 `Apollo-portal-2.0.1` `Apollo-config-2.0.1` 安装出口网络治理插件来定义下游调用地址,这是一种 Service Mesh 微服务治理的实现方式。通过定义下游服务的域名,来访问下游服务的指定端口。如在 `Apollo-portal-2.0.1` 的插件中,访问 `Apollo-config-2.0.1` 8080 端口的域名为 `apollo-config-pro` ,这也是配置中只定义域名,而不需要定义端口的原因。 # 四、高级特性 ## 4.1 实例数量伸缩 Apollo 配置中心所包含的 `Apollo-portal-2.0.1` `Apollo-config-2.0.1` `Apollo-admin-2.0.1` 组件均使用 Deployment 控制器部署,通过 Rainbond 内置的 Service Mesh 微服务框架实现服务发现与通信。故而这三个组件均可以一键扩展多个实例,实现集群化部署。 以 `Apollo-portal-2.0.1` 为例,点击 **伸缩** ,修改 **实例数量** 后,点击 **设置** 即可。 ![apollo-5](https://grstatic.oss-cn-shanghai.aliyuncs.com/wechat/apollo/apollo2.0.1/apollo-5.jpg) ## 4.2 追加环境 Apollo 配置中心支持对接多套环境,并使用统一的 Portal 页面进行管理。基于 Rainbond 一键安装而来的 Apollo 集群默认附带了 `PRO` 环境。接下来讲解在 Rainbond 场景中,如何追加一套 `DEV` 环境,假设在 `DEV` 环境中,通过 `apollo-config-dev`、`apollo-admin-dev`来分别访问 `Apollo-config-Dev` `Apollo-admin-Dev` 组件。 1. 再部署一套 Apollo 集群,并去除新集群中 `Apollo-portal-2.0.1` `ApolloPortalDB`组件。为了便于管理,修改 `Apollo-config-2.0.1` `Apollo-admin-2.0.1` 组件的名称。添加 `Apollo-portal-2.0.1` 到 `Apollo-config-Dev` `Apollo-admin-Dev` 的依赖。拓扑展示如下: > 注意,这个步骤会触发连接信息环境变量冲突的情况,记得为 `Apollo-config-Dev` `Apollo-admin-Dev` 组件的对内端口,重新定义你喜欢的名字。 ![apollo-6](https://grstatic.oss-cn-shanghai.aliyuncs.com/wechat/apollo/apollo2.0.1/apollo-6.jpg) 2. 在 **环境配置** 页面,修改 `Apollo-config-Dev` 的配置文件 `/apollo-configservice/config/application-github.properties` ,将 config 和 admin 的服务地址修改成为预期的值。 ![apollo-10](https://static.goodrain.com/wechat/apollo/apollo-10.png) 3. 分别进入`Apollo-config-Dev` `Apollo-portal-2.0.1` 的插件页面,为其出口网络治理插件修改配置,Rainbond 内置的微服务框架,通过设定的域名(Domains)来定义下游服务的访问地址。以 `Apollo-portal-2.0.1` 为例,需要配置到 `Apollo-config-Dev` `Apollo-admin-Dev` 的访问域名。 ![apollo-7](https://static.goodrain.com/wechat/apollo/apollo-7.png) 配置完成后点击 **更新配置**, `Apollo-portal-2.0.1` 就可以通过 apollo-config-dev 这个域名访问到 `Apollo-config-Dev`。 同理,`Apollo-config-Dev` 需要配置到 `Apollo-admin-Dev` 的访问域名。配置完成后更新配置。 4. 修改 `Apollo-portal-2.0.1` 的配置,来加入新的 `DEV` 环境。 修改环境变量 `APOLLO_PORTAL_ENVS` 的值,加入 `dev` 环境。 ![apollo-8](https://grstatic.oss-cn-shanghai.aliyuncs.com/wechat/apollo/apollo2.0.1/apollo-7.jpg) 修改配置文件 `/apollo-portal/config/apollo-env.properties` ,写入 `dev` 环境的 meta 地址。 ![apollo-9](https://static.goodrain.com/wechat/apollo/apollo-9.png) 更新 `Apollo-portal-2.0.1` 组件,使所有配置生效。查看系统信息,验证环境加入完成。 ![apollo-11](https://static.goodrain.com/wechat/apollo/apollo-11.png) ================================================ FILE: docs/zh/design/apollo-core-concept-namespace.md ================================================ ### 1. 什么是Namespace? Namespace是配置项的集合,类似于一个配置文件的概念。 ### 2. 什么是“application”的Namespace? Apollo在创建项目的时候,都会默认创建一个“application”的Namespace。顾名思义,“application”是给应用自身使用的,熟悉Spring Boot的同学都知道,Spring Boot项目都有一个默认配置文件application.yml。在这里application.yml就等同于“application”的Namespace。对于90%的应用来说,“application”的Namespace已经满足日常配置使用场景了。 #### 客户端获取“application” Namespace的代码如下: ``` java Config config = ConfigService.getAppConfig(); ``` #### 客户端获取非“application” Namespace的代码如下: ``` java Config config = ConfigService.getConfig(namespaceName); ``` ### 3. Namespace的格式有哪些? 配置文件有多种格式,例如:properties、xml、yml、yaml、json等。同样Namespace也具有这些格式。在Portal UI中可以看到“application”的Namespace上有一个“properties”标签,表明“application”是properties格式的。 >注1:非properties格式的namespace,在客户端使用时需要调用`ConfigService.getConfigFile(String namespace, ConfigFileFormat configFileFormat)`来获取,如果使用[Http接口直接调用](zh/client/other-language-client-user-guide#_12-通过带缓存的http接口从apollo读取配置)时,对应的namespace参数需要传入namespace的名字加上后缀名,如datasources.json。 >注2:apollo-client 1.3.0版本开始对yaml/yml做了更好的支持,使用起来和properties格式一致:`Config config = ConfigService.getConfig("application.yml");`,Spring的注入方式也和properties一致。 ### 4. Namespace的获取权限分类 Namespace的获取权限分为两种: * private (私有的) * public (公共的) 这里的获取权限是相对于Apollo客户端来说的。 #### 4.1 private权限 private权限的Namespace,只能被所属的应用获取到。一个应用尝试获取其它应用private的Namespace,Apollo会报“404”异常。 #### 4.2 public权限 public权限的Namespace,能被任何应用获取。 ### 5. Namespace的类型 Namespace类型有三种: * 私有类型 * 公共类型 * 关联类型(继承类型) #### 5.1 私有类型 私有类型的Namespace具有private权限。例如上文提到的“application” Namespace就是私有类型。 #### 5.2 公共类型 ##### 5.2.1 含义 公共类型的Namespace具有public权限。公共类型的Namespace相当于游离于应用之外的配置,且通过Namespace的名称去标识公共Namespace,所以公共的Namespace的名称必须全局唯一。 ##### 5.2.2 使用场景 * 部门级别共享的配置 * 小组级别共享的配置 * 几个项目之间共享的配置 * 中间件客户端的配置 #### 5.3 关联类型 ##### 5.3.1 含义 关联类型又可称为继承类型,关联类型具有private权限。关联类型的Namespace继承于公共类型的Namespace,用于覆盖公共Namespace的某些配置。例如公共的Namespace有两个配置项 ``` k1 = v1 k2 = v2 ``` 然后应用A有一个关联类型的Namespace关联了此公共Namespace,且覆盖了配置项k1,新值为v3。那么在应用A实际运行时,获取到的公共Namespace的配置为: ``` k1 = v3 k2 = v2 ``` ##### 5.3.2 使用场景 举一个实际使用的场景。假设RPC框架的配置(如:timeout)有以下要求: * 提供一份全公司默认的配置且可动态调整 * RPC客户端项目可以自定义某些配置项且可动态调整 1. 如果把以上两点要求去掉“动态调整”,那么做法很简单。在rpc-client.jar包里有一份配置文件,每次修改配置文件然后重新发一个版本的jar包即可。同理,客户端项目修改配置也是如此。 2. 如果只支持客户端项目可动态调整配置。客户端项目可以在Apollo “application” Namespace上配置一些配置项。在初始化service的时候,从Apollo上读取配置即可。这样做的坏处就是,每个项目都要自定义一些key,不统一。 3. 那么如何完美支持以上需求呢?答案就是结合使用Apollo的公共类型的Namespace和关联类型的Namespace。RPC团队在Apollo上维护一个叫“rpc-client”的公共Namespace,在“rpc-client” Namespace上配置默认的参数值。rpc-client.jar里的代码读取“rpc-client”Namespace的配置即可。如果需要调整默认的配置,只需要修改公共类型“rpc-client” Namespace的配置。如果客户端项目想要自定义或动态修改某些配置项,只需要在Apollo 自己项目下关联“rpc-client”,就能创建关联类型“rpc-client”的Namespace。然后在关联类型“rpc-client”的Namespace下修改配置项即可。这里有一点需要指出的,那就是rpc-client.jar是在应用容器里运行的,所以rpc-client获取到的“rpc-client” Namespace的配置是应用的关联类型的Namespace加上公共类型的Namespace。 #### 5.4 例子 如下图所示,有三个应用:应用A、应用B、应用C。 * 应用A有两个私有类型的Namespace:application和NS-Private,以及一个关联类型的Namespace:NS-Public。 * 应用B有一个私有类型的Namespace:application,以及一个公共类型的Namespace:NS-Public。 * 应用C只有一个私有类型的Namespace:application ![Namespace例子](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/namespace-model-example.png) ##### 5.4.1 应用A获取Apollo配置 ```java //application Config appConfig = ConfigService.getAppConfig(); appConfig.getProperty("k1", null); // k1 = v11 appConfig.getProperty("k2", null); // k2 = v21 //NS-Private Config privateConfig = ConfigService.getConfig("NS-Private"); privateConfig.getProperty("k1", null); // k1 = v3 privateConfig.getProperty("k3", null); // k3 = v4 //NS-Public,覆盖公共类型配置的情况,k4被覆盖 Config publicConfig = ConfigService.getConfig("NS-Public"); publicConfig.getProperty("k4", null); // k4 = v6 cover publicConfig.getProperty("k6", null); // k6 = v6 publicConfig.getProperty("k7", null); // k7 = v7 ``` ##### 5.4.2 应用B获取Apollo配置 ```java //application Config appConfig = ConfigService.getAppConfig(); appConfig.getProperty("k1", null); // k1 = v12 appConfig.getProperty("k2", null); // k2 = null appConfig.getProperty("k3", null); // k3 = v32 //NS-Private,由于没有NS-Private Namespace 所以获取到default value Config privateConfig = ConfigService.getConfig("NS-Private"); privateConfig.getProperty("k1", "default value"); //NS-Public Config publicConfig = ConfigService.getConfig("NS-Public"); publicConfig.getProperty("k4", null); // k4 = v5 publicConfig.getProperty("k6", null); // k6 = v6 publicConfig.getProperty("k7", null); // k7 = v7 ``` ##### 5.4.3 应用C获取Apollo配置 ```java //application Config appConfig = ConfigService.getAppConfig(); appConfig.getProperty("k1", null); // k1 = v12 appConfig.getProperty("k2", null); // k2 = null appConfig.getProperty("k3", null); // k3 = v33 //NS-Private,由于没有NS-Private Namespace 所以获取到default value Config privateConfig = ConfigService.getConfig("NS-Private"); privateConfig.getProperty("k1", "default value"); //NS-Public,公共类型的Namespace,任何项目都可以获取到 Config publicConfig = ConfigService.getConfig("NS-Public"); publicConfig.getProperty("k4", null); // k4 = v5 publicConfig.getProperty("k6", null); // k6 = v6 publicConfig.getProperty("k7", null); // k7 = v7 ``` ##### 5.4.4 ChangeListener 以上代码例子中可以看到,在客户端Namespace映射成一个Config对象。Namespace配置变更的监听器是注册在Config对象上。 所以在应用A中监听application的Namespace代码如下: ```java Config appConfig = ConfigService.getAppConfig(); appConfig.addChangeListener(new ConfigChangeListener() { public void onChange(ConfigChangeEvent changeEvent) { //do something } }) ``` 在应用A中监听NS-Private的Namespace代码如下: ```java Config privateConfig = ConfigService.getConfig("NS-Private"); privateConfig.addChangeListener(new ConfigChangeListener() { public void onChange(ConfigChangeEvent changeEvent) { //do something } }) ``` 在应用A、应用B、应用C中监听NS-Public的Namespace代码如下: ```java Config publicConfig = ConfigService.getConfig("NS-Public"); publicConfig.addChangeListener(new ConfigChangeListener() { public void onChange(ConfigChangeEvent changeEvent) { //do something } }) ``` ================================================ FILE: docs/zh/design/apollo-design.md ================================================ #   # 一、总体设计 ## 1.1 基础模型 如下即是Apollo的基础模型: 1. 用户在配置中心对配置进行修改并发布 2. 配置中心通知Apollo客户端有配置更新 3. Apollo客户端从配置中心拉取最新的配置、更新本地配置并通知到应用 ![basic-architecture](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/basic-architecture.png) ## 1.2 架构模块 下图是Apollo架构模块的概览,详细说明可以参考[Apollo配置中心架构剖析](https://mp.weixin.qq.com/s/-hUaQPzfsl9Lm3IqQW3VDQ)。 ![overall-architecture](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/overall-architecture.png) 上图简要描述了Apollo的总体设计,我们可以从下往上看: * Config Service提供配置的读取、推送等功能,服务对象是Apollo客户端 ```mermaid sequenceDiagram Client ->> Config Service: request Config Service ->> ConfigDB: request ConfigDB -->> Config Service: ack Config Service -->> Client: ack ``` * Admin Service提供配置的修改、发布等功能,服务对象是Apollo Portal(管理界面) ```mermaid sequenceDiagram Portal ->> Admin Service: r/w, publish appId/cluster/namespace Admin Service ->> ConfigDB: r/w, publish appId/cluster/namespace ConfigDB -->> Admin Service: ack Admin Service -->> Portal: ack ``` * Config Service和Admin Service都是多实例、无状态部署,所以需要将自己注册到Eureka中并保持心跳 * 在Eureka之上我们架了一层Meta Server用于封装Eureka的服务发现接口 ```mermaid sequenceDiagram Client or Portal ->> Meta Server: discovery service's instances Meta Server ->> Eureka: discovery service's instances Eureka -->> Meta Server: service's instances Meta Server -->> Client or Portal: service's instances ``` * Client通过域名访问Meta Server获取Config Service服务列表(IP+Port),而后直接通过IP+Port访问服务,同时在Client侧会做load balance、错误重试 ```mermaid sequenceDiagram Client ->> Meta Server: discovery Config Service's instances Meta Server -->> Client: Config Service's instances(Multiple IP+Port) loop until success Client ->> Client: load balance choose a Config Service instance Client ->> Config Service: request Config Service -->> Client: ack end ``` * Portal通过域名访问Meta Server获取Admin Service服务列表(IP+Port),而后直接通过IP+Port访问服务,同时在Portal侧会做load balance、错误重试 ```mermaid sequenceDiagram Portal ->> Meta Server: discovery Admin Service's instances Meta Server -->> Portal: Admin Service's instances(Multiple IP+Port) loop until success Portal ->> Portal: load balance choose a Admin Service instance Portal ->> Admin Service: request Admin Service -->> Portal: ack end ``` * 为了简化部署,我们实际上会把Config Service、Eureka和Meta Server三个逻辑角色部署在同一个JVM进程中 ```mermaid graph subgraph JVM Process 1[Config Service] 2[Eureka] 3[Meta Server] end ``` 实际部署的架构可以参考[部署架构](zh/deployment/deployment-architecture.md) ### 1.2.1 Why Eureka 为什么我们采用Eureka作为服务注册中心,而不是使用传统的zk、etcd呢?我大致总结了一下,有以下几方面的原因: * 它提供了完整的Service Registry和Service Discovery实现 * 首先是提供了完整的实现,并且也经受住了Netflix自己的生产环境考验,相对使用起来会比较省心。 * 和Spring Cloud无缝集成 * 我们的项目本身就使用了Spring Cloud和Spring Boot,同时Spring Cloud还有一套非常完善的开源代码来整合Eureka,所以使用起来非常方便。 * 另外,Eureka还支持在我们应用自身的容器中启动,也就是说我们的应用启动完之后,既充当了Eureka的角色,同时也是服务的提供者。这样就极大的提高了服务的可用性。 * **这一点是我们选择Eureka而不是zk、etcd等的主要原因,为了提高配置中心的可用性和降低部署复杂度,我们需要尽可能地减少外部依赖。** * Open Source * 最后一点是开源,由于代码是开源的,所以非常便于我们了解它的实现原理和排查问题。 ## 1.3 各模块概要介绍 ### 1.3.1 Config Service * 提供配置获取接口 ```mermaid sequenceDiagram Client ->> Config Service: get content of appId/cluster/namespace opt if namespace is not cached Config Service ->> ConfigDB: get content of appId/cluster/namespace ConfigDB -->> Config Service: content of appId/cluster/namespace end Config Service -->> Client: content of appId/cluster/namespace ``` * 提供配置更新推送接口(基于Http long polling) * 服务端使用[Spring DeferredResult](http://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/context/request/async/DeferredResult.html)实现异步化,从而大大增加长连接数量 * 目前使用的tomcat embed默认配置是最多10000个连接(可以调整),使用了4C8G的虚拟机实测可以支撑10000个连接,所以满足需求(一个应用实例只会发起一个长连接)。 * 接口服务对象为Apollo客户端 ### 1.3.2 Admin Service * 提供配置管理接口 * 提供配置修改、发布、检索等接口 * 接口服务对象为Portal ### 1.3.3 Meta Server * Portal通过域名访问Meta Server获取Admin Service服务列表(IP+Port) * Client通过域名访问Meta Server获取Config Service服务列表(IP+Port) * Meta Server从Eureka获取Config Service和Admin Service的服务信息,相当于是一个Eureka Client * 增设一个Meta Server的角色主要是为了封装服务发现的细节,对Portal和Client而言,永远通过一个Http接口获取Admin Service和Config Service的服务信息,而不需要关心背后实际的服务注册和发现组件 * Meta Server只是一个逻辑角色,在部署时和Config Service是在一个JVM进程中的,所以IP、端口和Config Service一致 ### 1.3.4 Eureka * 基于[Eureka](https://github.com/Netflix/eureka)和[Spring Cloud Netflix](https://cloud.spring.io/spring-cloud-netflix/)提供服务注册和发现 * Config Service和Admin Service会向Eureka注册服务,并保持心跳 * 为了简单起见,目前Eureka在部署时和Config Service是在一个JVM进程中的(通过Spring Cloud Netflix) ### 1.3.5 Portal * 提供Web界面供用户管理配置 * 通过Meta Server获取Admin Service服务列表(IP+Port),通过IP+Port访问服务 * 在Portal侧做load balance、错误重试 ### 1.3.6 Client * Apollo提供的客户端程序,为应用提供配置获取、实时更新等功能 * 通过Meta Server获取Config Service服务列表(IP+Port),通过IP+Port访问服务 * 在Client侧做load balance、错误重试 ## 1.4 E-R Diagram ### 1.4.1 主体E-R Diagram ![apollo-erd](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/apollo-erd.png) * **App** * App信息 * **AppNamespace** * App下Namespace的元信息 * **Cluster** * 集群信息 * **Namespace** * 集群下的namespace * **Item** * Namespace的配置,每个Item是一个key, value组合 * **Release** * Namespace发布的配置,每个发布包含发布时该Namespace的所有配置 * **Commit** * Namespace下的配置更改记录 * **Audit** * 审计信息,记录用户在何时使用何种方式操作了哪个实体。 ### 1.4.2 权限相关E-R Diagram ![apollo-erd-role-permission](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/apollo-erd-role-permission.png) * **User** * Apollo portal用户 * **UserRole** * 用户和角色的关系 * **Role** * 角色 * **RolePermission** * 角色和权限的关系 * **Permission** * 权限 * 对应到具体的实体资源和操作,如修改NamespaceA的配置,发布NamespaceB的配置等。 * **Consumer** * 第三方应用 * **ConsumerToken** * 发给第三方应用的token * **ConsumerRole** * 第三方应用和角色的关系 * **ConsumerAudit** * 第三方应用访问审计 # 二、服务端设计 ## 2.1 配置发布后的实时推送设计 在配置中心中,一个重要的功能就是配置发布后实时推送到客户端。下面我们简要看一下这块是怎么设计实现的。 ![release-message-notification-design](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/release-message-notification-design.png) 上图简要描述了配置发布的大致过程: 1. 用户在Portal操作配置发布 2. Portal调用Admin Service的接口操作发布 3. Admin Service发布配置后,发送ReleaseMessage给各个Config Service 4. Config Service收到ReleaseMessage后,通知对应的客户端 ### 2.1.1 发送ReleaseMessage的实现方式 Admin Service在配置发布后,需要通知所有的Config Service有配置发布,从而Config Service可以通知对应的客户端来拉取最新的配置。 从概念上来看,这是一个典型的消息使用场景,Admin Service作为producer发出消息,各个Config Service作为consumer消费消息。通过一个消息组件(Message Queue)就能很好的实现Admin Service和Config Service的解耦。 在实现上,考虑到Apollo的实际使用场景,以及为了尽可能减少外部依赖,我们没有采用外部的消息中间件,而是通过数据库实现了一个简单的消息队列。 实现方式如下: 1. Admin Service在配置发布后会往ReleaseMessage表插入一条消息记录,消息内容就是配置发布的AppId+Cluster+Namespace,参见[DatabaseMessageSender](https://github.com/apolloconfig/apollo/blob/master/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/message/DatabaseMessageSender.java) 2. Config Service有一个线程会每秒扫描一次ReleaseMessage表,看看是否有新的消息记录,参见[ReleaseMessageScanner](https://github.com/apolloconfig/apollo/blob/master/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/message/ReleaseMessageScanner.java) 3. Config Service如果发现有新的消息记录,那么就会通知到所有的消息监听器([ReleaseMessageListener](https://github.com/apolloconfig/apollo/blob/master/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/message/ReleaseMessageListener.java)),如[NotificationControllerV2](https://github.com/apolloconfig/apollo/blob/master/apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/controller/NotificationControllerV2.java),消息监听器的注册过程参见[ConfigServiceAutoConfiguration](https://github.com/apolloconfig/apollo/blob/master/apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/ConfigServiceAutoConfiguration.java) 4. NotificationControllerV2得到配置发布的AppId+Cluster+Namespace后,会通知对应的客户端 示意图如下: release-message-design ### 2.1.2 Config Service通知客户端的实现方式 上一节中简要描述了NotificationControllerV2是如何得知有配置发布的,那NotificationControllerV2在得知有配置发布后是如何通知到客户端的呢? 实现方式如下: 1. 客户端会发起一个Http请求到Config Service的`notifications/v2`接口,也就是[NotificationControllerV2](https://github.com/apolloconfig/apollo/blob/master/apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/controller/NotificationControllerV2.java),参见[RemoteConfigLongPollService](https://github.com/apolloconfig/apollo-java/blob/main/apollo-client/src/main/java/com/ctrip/framework/apollo/internals/RemoteConfigLongPollService.java) 2. NotificationControllerV2不会立即返回结果,而是通过[Spring DeferredResult](http://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/context/request/async/DeferredResult.html)把请求挂起 3. 如果在60秒内没有该客户端关心的配置发布,那么会返回Http状态码304给客户端 4. 如果有该客户端关心的配置发布,NotificationControllerV2会调用DeferredResult的[setResult](http://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/context/request/async/DeferredResult.html#setResult-T-)方法,传入有配置变化的namespace信息,同时该请求会立即返回。客户端从返回的结果中获取到配置变化的namespace后,会立即请求Config Service获取该namespace的最新配置。 # 三、客户端设计 ![client-architecture](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/client-architecture.png) 上图简要描述了Apollo客户端的实现原理: 1. 客户端和服务端保持了一个长连接,从而能第一时间获得配置更新的推送。(通过Http Long Polling实现) 2. 客户端还会定时从Apollo配置中心服务端拉取应用的最新配置。 * 这是一个fallback机制,为了防止推送机制失效导致配置不更新 * 客户端定时拉取会上报本地版本,所以一般情况下,对于定时拉取的操作,服务端都会返回304 - Not Modified * 定时频率默认为每5分钟拉取一次,客户端也可以通过在运行时指定System Property: `apollo.refreshInterval`来覆盖,单位为分钟。 3. 客户端从Apollo配置中心服务端获取到应用的最新配置后,会保存在内存中 4. 客户端会把从服务端获取到的配置在本地文件系统缓存一份 * 在遇到服务不可用,或网络不通的时候,依然能从本地恢复配置 5. 应用程序可以从Apollo客户端获取最新的配置、订阅配置更新通知 ## 3.1 和Spring集成的原理 Apollo除了支持API方式获取配置,也支持和Spring/Spring Boot集成,集成原理简述如下。 Spring从3.1版本开始增加了`ConfigurableEnvironment`和`PropertySource`: * ConfigurableEnvironment * Spring的ApplicationContext会包含一个Environment(实现ConfigurableEnvironment接口) * ConfigurableEnvironment自身包含了很多个PropertySource * PropertySource * 属性源 * 可以理解为很多个Key - Value的属性配置 在运行时的结构形如: ![Overview](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/environment.png) 需要注意的是,PropertySource之间是有优先级顺序的,如果有一个Key在多个property source中都存在,那么在前面的property source优先。 所以对上图的例子: * env.getProperty(“key1”) -> value1 * **env.getProperty(“key2”) -> value2** * env.getProperty(“key3”) -> value4 在理解了上述原理后,Apollo和Spring/Spring Boot集成的手段就呼之欲出了:在应用启动阶段,Apollo从远端获取配置,然后组装成PropertySource并插入到第一个即可,如下图所示: ![Overview](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/environment-remote-source.png) 相关代码可以参考[PropertySourcesProcessor](https://github.com/apolloconfig/apollo-java/blob/main/apollo-client/src/main/java/com/ctrip/framework/apollo/spring/config/PropertySourcesProcessor.java) # 四、可用性考虑
    场景 影响 降级 原因
    某台Config Service下线 无影响 Config Service无状态,客户端重连其它Config Service
    所有Config Service下线 客户端无法读取最新配置,Portal无影响 客户端重启时,可以读取本地缓存配置文件。如果是新扩容的机器,可以从其它机器上获取已缓存的配置文件,具体信息可以参考Java客户端使用指南 - 1.2.3 本地缓存路径
    某台Admin Service下线 无影响 Admin Service无状态,Portal重连其它Admin Service
    所有Admin Service下线 客户端无影响,Portal无法更新配置
    某台Portal下线 无影响 Portal域名通过SLB绑定多台服务器,重试后指向可用的服务器
    全部Portal下线 客户端无影响,Portal无法更新配置
    某个数据中心下线 无影响 多数据中心部署,数据完全同步,Meta Server/Portal域名通过SLB自动切换到其它存活的数据中心
    数据库宕机 客户端无影响,Portal无法更新配置 Config Service开启配置缓存后,对配置的读取不受数据库宕机影响
    # 五、监控相关 ## 5.1 Tracing ### 5.1.1 CAT Apollo客户端和服务端目前支持[CAT](https://github.com/dianping/cat)自动打点,所以如果自己公司内部部署了CAT的话,只要引入cat-client后Apollo就会自动启用CAT打点。 如果不使用CAT的话,也不用担心,只要不引入cat-client,Apollo是不会启用CAT打点的。 Apollo也提供了Tracer相关的SPI,可以方便地对接自己公司的监控系统。 更多信息,可以参考[v0.4.0 Release Note](https://github.com/apolloconfig/apollo/releases/tag/v0.4.0) ### 5.1.2 SkyWalking 可以参考[@hepyu](https://github.com/hepyu)贡献的[apollo-skywalking-pro样例](https://github.com/hepyu/k8s-app-config/tree/master/product/standard/apollo-skywalking-pro)。 ## 5.2 Metrics 从1.5.0版本开始,Apollo服务端支持通过`/prometheus`暴露prometheus格式的metrics,如`http://${someIp:somePort}/prometheus` ================================================ FILE: docs/zh/design/apollo-introduction.md ================================================ #   # 1、What is Apollo ## 1.1 背景 随着程序功能的日益复杂,程序的配置日益增多:各种功能的开关、参数的配置、服务器的地址…… 对程序配置的期望值也越来越高:配置修改后实时生效,灰度发布,分环境、分集群管理配置,完善的权限、审核机制…… 在这样的大环境下,传统的通过配置文件、数据库等方式已经越来越无法满足开发人员对配置管理的需求。 Apollo配置中心应运而生! ## 1.2 Apollo简介 Apollo(阿波罗)是一款可靠的分布式配置管理中心,诞生于携程框架研发部,能够集中化管理应用不同环境、不同集群的配置,配置修改后能够实时推送到应用端,并且具备规范的权限、流程治理等特性,适用于微服务配置管理场景。 Apollo支持4个维度管理Key-Value格式的配置: 1. application (应用) 2. environment (环境) 3. cluster (集群) 4. namespace (命名空间) 同时,Apollo基于开源模式开发,开源地址:https://github.com/ctripcorp/apollo ## 1.3 配置基本概念 既然Apollo定位于配置中心,那么在这里有必要先简单介绍一下什么是配置。 按照我们的理解,配置有以下几个属性: * **配置是独立于程序的只读变量** * 配置首先是独立于程序的,同一份程序在不同的配置下会有不同的行为。 * 其次,配置对于程序是只读的,程序通过读取配置来改变自己的行为,但是程序不应该去改变配置。 * 常见的配置有:DB Connection Str、Thread Pool Size、Buffer Size、Request Timeout、Feature Switch、Server Urls等。 * **配置伴随应用的整个生命周期** * 配置贯穿于应用的整个生命周期,应用在启动时通过读取配置来初始化,在运行时根据配置调整行为。 * **配置可以有多种加载方式** * 配置也有很多种加载方式,常见的有程序内部hard code,配置文件,环境变量,启动参数,基于数据库等 * **配置需要治理** * 权限控制 * 由于配置能改变程序的行为,不正确的配置甚至能引起灾难,所以对配置的修改必须有比较完善的权限控制 * 不同环境、集群配置管理 * 同一份程序在不同的环境(开发,测试,生产)、不同的集群(如不同的数据中心)经常需要有不同的配置,所以需要有完善的环境、集群配置管理 * 框架类组件配置管理 * 还有一类比较特殊的配置 - 框架类组件配置,比如CAT客户端的配置。 * 虽然这类框架类组件是由其他团队开发、维护,但是运行时是在业务实际应用内的,所以本质上可以认为框架类组件也是应用的一部分。 * 这类组件对应的配置也需要有比较完善的管理方式。 # 2、Why Apollo 正是基于配置的特殊性,所以Apollo从设计之初就立志于成为一个有治理能力的配置发布平台,目前提供了以下的特性: * **统一管理不同环境、不同集群的配置** * Apollo提供了一个统一界面集中式管理不同环境(environment)、不同集群(cluster)、不同命名空间(namespace)的配置。 * 同一份代码部署在不同的集群,可以有不同的配置,比如zookeeper的地址等 * 通过命名空间(namespace)可以很方便地支持多个不同应用共享同一份配置,同时还允许应用对共享的配置进行覆盖 * **配置修改实时生效(热发布)** * 用户在Apollo修改完配置并发布后,客户端能实时(1秒)接收到最新的配置,并通知到应用程序 * **版本发布管理** * 所有的配置发布都有版本概念,从而可以方便地支持配置的回滚 * **灰度发布** * 支持配置的灰度发布,比如点了发布后,只对部分应用实例生效,等观察一段时间没问题后再推给所有应用实例 * **配置项的全局视角搜索** * 通过对配置项的key与value进行的模糊检索,找到拥有对应值的配置项在哪个应用、环境、集群、命名空间中被使用 * 通过高亮显示、分页与跳转配置等操作,便于让管理员以及SRE角色快速、便捷地找到与更改资源的配置值 * **权限管理、发布审核、操作审计** * 应用和配置的管理都有完善的权限管理机制,对配置的管理还分为了编辑和发布两个环节,从而减少人为的错误。 * 所有的操作都有审计日志,可以方便地追踪问题 * **客户端配置信息监控** * 可以在界面上方便地看到配置在被哪些实例使用 * **提供Java和.Net原生客户端** * 提供了Java和.Net的原生客户端,方便应用集成 * 支持Spring Placeholder, Annotation和Spring Boot的ConfigurationProperties,方便应用使用(需要Spring 3.1.1+) * 同时提供了Http接口,非Java和.Net应用也可以方便地使用 * **提供开放平台API** * Apollo自身提供了比较完善的统一配置管理界面,支持多环境、多数据中心配置管理、权限、流程治理等特性。不过Apollo出于通用性考虑,不会对配置的修改做过多限制,只要符合基本的格式就能保存,不会针对不同的配置值进行针对性的校验,如数据库用户名、密码,Redis服务地址等 * 对于这类应用配置,Apollo支持应用方通过开放平台API在Apollo进行配置的修改和发布,并且具备完善的授权和权限控制 * **部署简单** * 配置中心作为基础服务,可用性要求非常高,这就要求Apollo对外部依赖尽可能地少 * 目前唯一的外部依赖是MySQL,所以部署非常简单,只要安装好Java和MySQL就可以让Apollo跑起来 * Apollo还提供了打包脚本,一键就可以生成所有需要的安装包,并且支持自定义运行时参数 # 3、Apollo at a glance ## 3.1 基础模型 如下即是Apollo的基础模型: 1. 用户在配置中心对配置进行修改并发布 2. 配置中心通知Apollo客户端有配置更新 3. Apollo客户端从配置中心拉取最新的配置、更新本地配置并通知到应用 ![basic-architecture](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/basic-architecture.png) ## 3.2 界面概览 ![apollo-home-screenshot](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/apollo-home-screenshot.jpg) 上图是Apollo配置中心中一个项目的配置首页 * 在页面左上方的环境列表模块展示了所有的环境和集群,用户可以随时切换。 * 页面中央展示了两个namespace(application和FX.apollo)的配置信息,默认按照表格模式展示、编辑。用户也可以切换到文本模式,以文件形式查看、编辑。 * 页面上可以方便地进行发布、回滚、灰度、授权、查看更改历史和发布历史等操作 ## 3.3 添加/修改配置项 用户可以通过配置中心界面方便的添加/修改配置项,更多使用说明请参见[应用接入指南](zh/portal/apollo-user-guide) ![edit-item-entry](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/edit-item-entry.png) 输入配置信息: ![edit-item](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/edit-item.png) ## 3.4 发布配置 通过配置中心发布配置: ![publish-items](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/publish-items-entry.png) 填写发布信息: ![publish-items](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/publish-items.png) ## 3.5 客户端获取配置(Java API样例) 配置发布后,就能在客户端获取到了,以Java为例,获取配置的示例代码如下。Apollo客户端还支持和Spring整合,更多客户端使用说明请参见[Java客户端使用指南](zh/client/java-sdk-user-guide)和[.Net客户端使用指南](zh/client/java-sdk-user-guide)。 ```java Config config = ConfigService.getAppConfig(); Integer defaultRequestTimeout = 200; Integer requestTimeout = config.getIntProperty("requestTimeout", defaultRequestTimeout); ``` ## 3.6 客户端监听配置变化 通过上述获取配置代码,应用就能实时获取到最新的配置了。 不过在某些场景下,应用还需要在配置变化时获得通知,比如数据库连接的切换等,所以Apollo还提供了监听配置变化的功能,Java示例如下: ```java Config config = ConfigService.getAppConfig(); config.addChangeListener(new ConfigChangeListener() { @Override public void onChange(ConfigChangeEvent changeEvent) { for (String key : changeEvent.changedKeys()) { ConfigChange change = changeEvent.getChange(key); System.out.println(String.format( "Found change - key: %s, oldValue: %s, newValue: %s, changeType: %s", change.getPropertyName(), change.getOldValue(), change.getNewValue(), change.getChangeType())); } } }); ``` ## 3.7 Spring集成样例 Apollo和Spring也可以很方便地集成,只需要标注`@EnableApolloConfig`后就可以通过`@Value`获取配置信息: ```java @Configuration @EnableApolloConfig public class AppConfig {} ``` ```java @Component public class SomeBean { //timeout的值会自动更新 @Value("${request.timeout:200}") private int timeout; } ``` # 4、Apollo in depth 通过上面的介绍,相信大家已经对Apollo有了一个初步的了解,并且相信已经覆盖到了大部分的使用场景。 接下来会主要介绍Apollo的cluster管理(集群)、namespace管理(命名空间)和对应的配置获取规则。 ## 4.1 Core Concepts 在介绍高级特性前,我们有必要先来了解一下Apollo中的几个核心概念: 1. **application (应用)** * 这个很好理解,就是实际使用配置的应用,Apollo客户端在运行时需要知道当前应用是谁,从而可以去获取对应的配置 * 每个应用都需要有唯一的身份标识 -- appId,我们认为应用身份是跟着代码走的,所以需要在代码中配置,具体信息请参见[Java客户端使用指南](zh/client/java-sdk-user-guide)。 2. **environment (环境)** * 配置对应的环境,Apollo客户端在运行时需要知道当前应用处于哪个环境,从而可以去获取应用的配置 * 我们认为环境和代码无关,同一份代码部署在不同的环境就应该能够获取到不同环境的配置 * 所以环境默认是通过读取机器上的配置(server.properties中的env属性)指定的,不过为了开发方便,我们也支持运行时通过System Property等指定,具体信息请参见[Java客户端使用指南](zh/client/java-sdk-user-guide)。 3. **cluster (集群)** * 一个应用下不同实例的分组,比如典型的可以按照数据中心分,把上海机房的应用实例分为一个集群,把北京机房的应用实例分为另一个集群。 * 对不同的cluster,同一个配置可以有不一样的值,如zookeeper地址。 * 集群默认是通过读取机器上的配置(server.properties中的idc属性)指定的,不过也支持运行时通过System Property指定,具体信息请参见[Java客户端使用指南](zh/client/java-sdk-user-guide)。 4. **namespace (命名空间)** * 一个应用下不同配置的分组,可以简单地把namespace类比为文件,不同类型的配置存放在不同的文件中,如数据库配置文件,RPC配置文件,应用自身的配置文件等 * 应用可以直接读取到公共组件的配置namespace,如DAL,RPC等 * 应用也可以通过继承公共组件的配置namespace来对公共组件的配置做调整,如DAL的初始数据库连接数 ## 4.2 自定义Cluster > 【本节内容仅对应用需要对不同集群应用不同配置才需要,如没有相关需求,可以跳过本节】 比如我们有应用在A数据中心和B数据中心都有部署,那么如果希望两个数据中心的配置不一样的话,我们可以通过新建cluster来解决。 ### 4.2.1 新建Cluster 新建Cluster只有项目的管理员才有权限,管理员可以在页面左侧看到“添加集群”按钮。 ![create-cluster](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/create-cluster.png) 点击后就进入到集群添加页面,一般情况下可以按照数据中心来划分集群,如SHAJQ、SHAOY等。 不过也支持自定义集群,比如可以为A机房的某一台机器和B机房的某一台机创建一个集群,使用一套配置。 ![create-cluster-detail](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/create-cluster-detail.png) ### 4.2.2 在Cluster中添加配置并发布 集群添加成功后,就可以为该集群添加配置了,首先需要按照下图所示切换到SHAJQ集群,之后配置添加流程和[3.3 添加/修改配置项](#_33-添加修改配置项)一样,这里就不再赘述了。 ![cluster-created](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/cluster-created.png) ### 4.2.3 指定应用实例所属的Cluster Apollo会默认使用应用实例所在的数据中心作为cluster,所以如果两者一致的话,不需要额外配置。 如果cluster和数据中心不一致的话,那么就需要通过System Property方式来指定运行时cluster: * -Dapollo.cluster=SomeCluster * 这里注意`apollo.cluster`为全小写 ## 4.3 自定义Namespace > 【本节仅对公共组件配置或需要多个应用共享配置才需要,如没有相关需求,可以跳过本节】 如果应用有公共组件(如hermes-producer,cat-client等)供其它应用使用,就需要通过自定义namespace来实现公共组件的配置。 ### 4.3.1 新建Namespace 以hermes-producer为例,需要先新建一个namespace,新建namespace只有项目的管理员才有权限,管理员可以在页面左侧看到“添加Namespace”按钮。 ![create-namespace](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/create-namespace.png) 点击后就进入namespace添加页面,Apollo会把应用所属的部门作为namespace的前缀,如FX。 ![create-namespace-detail](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/create-namespace-detail.png) ### 4.3.2 关联到环境和集群 Namespace创建完,需要选择在哪些环境和集群下使用 ![link-namespace-detail](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/link-namespace-detail.png) ### 4.3.3 在Namespace中添加配置项 接下来在这个新建的namespace下添加配置项 ![add-item-in-new-namespace](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/add-item-in-new-namespace.png) 添加完成后就能在FX.Hermes.Producer的namespace中看到配置。 ![item-created-in-new-namespace](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/item-created-in-new-namespace.png) ### 4.3.4 发布namespace的配置 ![publish-items-in-new-namespace](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/publish-items-in-new-namespace.png) ### 4.3.5 客户端获取Namespace配置 对自定义namespace的配置获取,稍有不同,需要程序传入namespace的名字。Apollo客户端还支持和Spring整合,更多客户端使用说明请参见[Java客户端使用指南](zh/client/java-sdk-user-guide)和[.Net客户端使用指南](zh/client/java-sdk-user-guide)。 ```java Config config = ConfigService.getConfig("FX.Hermes.Producer"); Integer defaultSenderBatchSize = 200; Integer senderBatchSize = config.getIntProperty("sender.batchsize", defaultSenderBatchSize); ``` ### 4.3.6 客户端监听Namespace配置变化 ```java Config config = ConfigService.getConfig("FX.Hermes.Producer"); config.addChangeListener(new ConfigChangeListener() { @Override public void onChange(ConfigChangeEvent changeEvent) { System.out.println("Changes for namespace " + changeEvent.getNamespace()); for (String key : changeEvent.changedKeys()) { ConfigChange change = changeEvent.getChange(key); System.out.println(String.format( "Found change - key: %s, oldValue: %s, newValue: %s, changeType: %s", change.getPropertyName(), change.getOldValue(), change.getNewValue(), change.getChangeType())); } } }); ``` ### 4.3.7 Spring集成样例 ```java @Configuration @EnableApolloConfig("FX.Hermes.Producer") public class AppConfig {} ``` ```java @Component public class SomeBean { //timeout的值会自动更新 @Value("${request.timeout:200}") private int timeout; } ``` ## 4.4 配置获取规则 > 【本节仅当应用自定义了集群或namespace才需要,如无相关需求,可以跳过本节】 在有了cluster概念后,配置的规则就显得重要了。 比如应用部署在A机房,但是并没有在Apollo新建cluster,这个时候Apollo的行为是怎样的? 或者在运行时指定了cluster=SomeCluster,但是并没有在Apollo新建cluster,这个时候Apollo的行为是怎样的? 接下来就来介绍一下配置获取的规则。 ### 4.4.1 应用自身配置的获取规则 当应用使用下面的语句获取配置时,我们称之为获取应用自身的配置,也就是应用自身的application namespace的配置。 ```java Config config = ConfigService.getAppConfig(); ``` 对这种情况的配置获取规则,简而言之如下: 1. 首先查找运行时cluster的配置(通过apollo.cluster指定) 2. 如果没有找到,则查找数据中心cluster的配置 3. 如果还是没有找到,则返回默认cluster的配置 图示如下: ![application-config-precedence](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/application-config-precedence.png) 所以如果应用部署在A数据中心,但是用户没有在Apollo创建cluster,那么获取的配置就是默认cluster(default)的。 如果应用部署在A数据中心,同时在运行时指定了SomeCluster,但是没有在Apollo创建cluster,那么获取的配置就是A数据中心cluster的配置,如果A数据中心cluster没有配置的话,那么获取的配置就是默认cluster(default)的。 ### 4.4.2 公共组件配置的获取规则 以`FX.Hermes.Producer`为例,hermes producer是hermes发布的公共组件。当使用下面的语句获取配置时,我们称之为获取公共组件的配置。 ```java Config config = ConfigService.getConfig("FX.Hermes.Producer"); ``` 对这种情况的配置获取规则,简而言之如下: 1. 首先获取当前应用下的`FX.Hermes.Producer` namespace的配置 2. 然后获取hermes应用下`FX.Hermes.Producer` namespace的配置 3. 上面两部分配置的并集就是最终使用的配置,如有key一样的部分,以当前应用优先 图示如下: ![public-namespace-config-precedence](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/public-namespace-config-precedence.png) 通过这种方式,就实现了对框架类组件的配置管理,框架组件提供方提供配置的默认值,应用如果有特殊需求,可以自行覆盖。 ## 4.5 总体设计 ![overall-architecture](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/overall-architecture.png) 上图简要描述了Apollo的总体设计,我们可以从下往上看: * Config Service提供配置的读取、推送等功能,服务对象是Apollo客户端 * Admin Service提供配置的修改、发布等功能,服务对象是Apollo Portal(管理界面) * Config Service和Admin Service都是多实例、无状态部署,所以需要将自己注册到Eureka中并保持心跳 * 在Eureka之上我们架了一层Meta Server用于封装Eureka的服务发现接口 * Client通过域名访问Meta Server获取Config Service服务列表(IP+Port),而后直接通过IP+Port访问服务,同时在Client侧会做load balance、错误重试 * Portal通过域名访问Meta Server获取Admin Service服务列表(IP+Port),而后直接通过IP+Port访问服务,同时在Portal侧会做load balance、错误重试 * 为了简化部署,我们实际上会把Config Service、Eureka和Meta Server三个逻辑角色部署在同一个JVM进程中 ### 4.5.1 Why Eureka 为什么我们采用Eureka作为服务注册中心,而不是使用传统的zk、etcd呢?我大致总结了一下,有以下几方面的原因: * 它提供了完整的Service Registry和Service Discovery实现 * 首先是提供了完整的实现,并且也经受住了Netflix自己的生产环境考验,相对使用起来会比较省心。 * 和Spring Cloud无缝集成 * 我们的项目本身就使用了Spring Cloud和Spring Boot,同时Spring Cloud还有一套非常完善的开源代码来整合Eureka,所以使用起来非常方便。 * 另外,Eureka还支持在我们应用自身的容器中启动,也就是说我们的应用启动完之后,既充当了Eureka的角色,同时也是服务的提供者。这样就极大的提高了服务的可用性。 * **这一点是我们选择Eureka而不是zk、etcd等的主要原因,为了提高配置中心的可用性和降低部署复杂度,我们需要尽可能地减少外部依赖。** * Open Source * 最后一点是开源,由于代码是开源的,所以非常便于我们了解它的实现原理和排查问题。 ## 4.6 客户端设计 ![client-architecture](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/client-architecture.png) 上图简要描述了Apollo客户端的实现原理: 1. 客户端和服务端保持了一个长连接,从而能第一时间获得配置更新的推送。 2. 客户端还会定时从Apollo配置中心服务端拉取应用的最新配置。 * 这是一个fallback机制,为了防止推送机制失效导致配置不更新 * 客户端定时拉取会上报本地版本,所以一般情况下,对于定时拉取的操作,服务端都会返回304 - Not Modified * 定时频率默认为每5分钟拉取一次,客户端也可以通过在运行时指定System Property: `apollo.refreshInterval`来覆盖,单位为分钟。 3. 客户端从Apollo配置中心服务端获取到应用的最新配置后,会保存在内存中 4. 客户端会把从服务端获取到的配置在本地文件系统缓存一份 * 在遇到服务不可用,或网络不通的时候,依然能从本地恢复配置 5. 应用程序从Apollo客户端获取最新的配置、订阅配置更新通知 ### 4.6.1 配置更新推送实现 前面提到了Apollo客户端和服务端保持了一个长连接,从而能第一时间获得配置更新的推送。 长连接实际上我们是通过Http Long Polling实现的,具体而言: * 客户端发起一个Http请求到服务端 * 服务端会保持住这个连接60秒 * 如果在60秒内有客户端关心的配置变化,被保持住的客户端请求会立即返回,并告知客户端有配置变化的namespace信息,客户端会据此拉取对应namespace的最新配置 * 如果在60秒内没有客户端关心的配置变化,那么会返回Http状态码304给客户端 * 客户端在收到服务端请求后会立即重新发起连接,回到第一步 考虑到会有数万客户端向服务端发起长连,在服务端我们使用了async servlet(Spring DeferredResult)来服务Http Long Polling请求。 ## 4.7 可用性考虑 配置中心作为基础服务,可用性要求非常高,下面的表格描述了不同场景下Apollo的可用性: | 场景 | 影响 | 降级 | 原因 | |------------------------|--------------------------------------|---------------------------------------|-----------------------------------------------------------------------------------------| | 某台config service下线 | 无影响 | | Config service无状态,客户端重连其它config service | | 所有config service下线 | 客户端无法读取最新配置,Portal无影响 | 客户端重启时,可以读取本地缓存配置文件 | | | 某台admin service下线 | 无影响 | | Admin service无状态,Portal重连其它admin service | | 所有admin service下线 | 客户端无影响,portal无法更新配置 | | | | 某台portal下线 | 无影响 | | Portal域名通过slb绑定多台服务器,重试后指向可用的服务器 | | 全部portal下线 | 客户端无影响,portal无法更新配置 | | | | 某个数据中心下线 | 无影响 | | 多数据中心部署,数据完全同步,Meta Server/Portal域名通过slb自动切换到其它存活的数据中心 | # 5、Contribute to Apollo Apollo从开发之初就是以开源模式开发的,所以也非常欢迎有兴趣、有余力的朋友一起加入进来。 服务端开发使用的是Java,基于Spring Cloud和Spring Boot框架。客户端目前提供了Java和.Net两种实现。 GitHub地址:https://github.com/ctripcorp/apollo 欢迎大家发起Pull Request! ================================================ FILE: docs/zh/extension/portal-how-to-enable-email-service.md ================================================ 在配置发布时候,我们希望发布信息邮件通知到相关的负责人。现支持发送邮件的动作有:普通发布、灰度发布、全量发布、回滚,通知对象包括:具有namespace编辑和发布权限的人员以及App负责人。 由于各公司的邮件服务往往有不同的实现,所以Apollo定义了一些SPI用来解耦,Apollo接入邮件服务的关键就是实现这些SPI。 ## 一、实现方式一:使用Apollo提供的smtp邮件服务 ### 1.1 接入步骤 在ApolloPortalDB.ServerConfig表配置以下参数,也可以通过管理员工具 - 系统参数页面进行配置,修改完一分钟实时生效。如下: * **email.enabled** 设置为true即可启用默认的smtp邮件服务 * **email.config.host** smtp的服务地址,如`smtp.163.com` * **email.config.user** smtp帐号用户名 * **email.config.password** smtp帐号密码 * **email.supported.envs** 支持发送邮件的环境列表,英文逗号隔开。我们不希望发布邮件变成用户的垃圾邮件,只有某些环境下的发布动作才会发送邮件。 * **email.sender** 邮件的发送人,可以不配置,默认为`email.config.user`。 * **apollo.portal.address** Apollo Portal的地址。方便用户从邮件点击跳转到Apollo Portal查看详细的发布信息。 * **email.template.framework** 邮件内容模板框架。将邮件内容模板化、可配置化,方便管理和变更邮件内容。 * **email.template.release.module.diff** 发布邮件的diff模块。 * **email.template.rollback.module.diff** 回滚邮件的diff模块。 * **email.template.release.module.rules** 灰度发布的灰度规则模块。 我们提供了[邮件模板样例](#三、邮件模板样例),方便大家使用。 ## 二、实现方式二:接入公司的统一邮件服务 和SSO类似,每个公司也有自己的邮件服务实现,所以我们相应的定义了[EmailService](https://github.com/apolloconfig/apollo/blob/master/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/EmailService.java)接口,现有两个实现类: 1. CtripEmailService:携程实现的EmailService 2. DefaultEmailService:smtp实现 ### 2.1 接入步骤 1. 提供自己公司的[EmailService](https://github.com/apolloconfig/apollo/blob/master/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/EmailService.java)实现,并在[EmailConfiguration](https://github.com/apolloconfig/apollo/blob/master/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/configuration/EmailConfiguration.java)中注册。 2. 在ApolloPortalDB.ServerConfig表配置以下参数,也可以通过管理员工具 - 系统参数页面进行配置,修改完一分钟实时生效。如下: * **email.supported.envs** 支持发送邮件的环境列表,英文逗号隔开。我们不希望发布邮件变成用户的垃圾邮件,只有某些环境下的发布动作才会发送邮件。 * **email.sender** 邮件的发送人。 * **apollo.portal.address** Apollo Portal的地址。方便用户从邮件点击跳转到Apollo Portal查看详细的发布信息。 * **email.template.framework** 邮件内容模板框架。将邮件内容模板化、可配置化,方便管理和变更邮件内容。 * **email.template.release.module.diff** 发布邮件的diff模块。 * **email.template.rollback.module.diff** 回滚邮件的diff模块。 * **email.template.release.module.rules** 灰度发布的灰度规则模块。 我们提供了[邮件模板样例](#三、邮件模板样例),方便大家使用。 >注:运行时使用不同的实现是通过[Profiles](http://docs.spring.io/autorepo/docs/spring-boot/current/reference/html/boot-features-profiles.html)实现的,比如你自己的Email实现是在`custom` profile中的话,在打包脚本中可以指定-Dapollo_profile=github,custom。其中`github`是Apollo必须的一个profile,用于数据库的配置,`custom`是你自己实现的profile。同时需要注意在[EmailConfiguration](https://github.com/apolloconfig/apollo/blob/master/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/configuration/EmailConfiguration.java)中修改默认实现的条件`@Profile({"!custom"})`。 ### 2.2 相关代码 1. [ConfigPublishListener](https://github.com/apolloconfig/apollo/blob/master/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/listener/ConfigPublishListener.java)监听发布事件,调用emailbuilder构建邮件内容,然后调用EmailService发送邮件 2. [emailbuilder](https://github.com/apolloconfig/apollo/blob/master/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/components/emailbuilder)包是构建邮件内容的实现 3. [EmailService](https://github.com/apolloconfig/apollo/blob/master/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/EmailService.java) 邮件发送服务 4. [EmailConfiguration](https://github.com/apolloconfig/apollo/blob/master/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/configuration/EmailConfiguration.java) 邮件服务注册类 ## 三、邮件模板样例 以下为发布邮件和回滚邮件的模板内容样式,邮件模板为html格式,发送html格式的邮件时,可能需要做一些额外的处理,取决于每个公司的邮件服务实现。为了减少字符数,模板经过了压缩处理,可自行格式化提高可读性。 ### 3.1 email.template.framework ```html

    发布基本信息

    AppId#{appId}环境#{env}集群#{clusterName}Namespace#{namespaceName}
    发布者#{operator}发布时间#{releaseTime}发布标题#{releaseTitle}备注#{releaseComment}
    #{diffModule}#{rulesModule}
    点击查看详细的发布信息

    如有Apollo使用问题请先查阅文档,或直接回复本邮件咨询。 ``` > 注:使用此模板需要在 portal 的系统参数中配置 apollo.portal.address,指向 apollo portal 的地址 ### 3.2 email.template.release.module.diff ```html

    变更的配置

    #{diffContent}
    Type Key Old Value New Value
    ``` ### 3.3 email.template.rollback.module.diff ```html


    变更的配置

    #{diffContent}
    Type Key 回滚前 回滚后

    ``` ### 3.4 email.template.release.module.rules ```html

    灰度规则

    #{rulesContent}
    ``` ### 3.5 发布邮件样例 ![发布邮件模板](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/email-template-release.png) ### 3.6 回滚邮件样例 ![回滚邮件模板](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/email-template-rollback.png) ================================================ FILE: docs/zh/extension/portal-how-to-enable-session-store.md ================================================ 从 1.9.0 版本开始,apollo-portal 增加了 session 共享支持,从而可以在集群部署 apollo-portal 时实现 session 共享。 ## 使用方式 ### 1. 基于 JDBC 的 session 共享(默认) 默认配置即为 基于 JDBC 的 session 共享 所以清除 session 共享相关的配置并配置数据库连接即可,需要清理的配置如下 外置配置文件(properties/yml)里面的 `spring.session.store-type` 配置项 环境变量里面的 `SPRING_SESSION_STORE_TYPE` System Property 里面的 `spring.session.store-type` 数据库连接有以下几种方式设置,按照优先级从高到低分别为: #### 1.1 System Property ```bash -Dspring.datasource.url=xxx -Dspring.datasource.username=xxx -Dspring.datasource.password=xxx ``` #### 1.2 环境变量 ```bash export SPRING_DATASOURCE_URL="xxx" export SPRING_DATASOURCE_USERNAME="xxx" export SPRING_DATASOURCE_PASSWORD="xxx" ``` #### 1.3 外部配置文件 例如 `config/application-github.properties` ```properties spring.datasource.url=xxx spring.datasource.username=xxx spring.datasource.password=xxx ``` #### 1.4 关于非 mysql 数据库初始化 session 的表 apollo 的 sql 当中的建表语句为 mysql 格式, 如果需要使用其它数据库可以参考 [spring-session](https://github.com/spring-projects/spring-session) 提供的其它建表 sql 请根据所使用的数据库选择对应的 sql 脚本 [db2.sql](https://github.com/spring-projects/spring-session/blob/faee8f1bdb8822a5653a81eba838dddf224d92d6/spring-session-jdbc/src/main/resources/org/springframework/session/jdbc/schema-db2.sql) [derby.sql](https://github.com/spring-projects/spring-session/blob/faee8f1bdb8822a5653a81eba838dddf224d92d6/spring-session-jdbc/src/main/resources/org/springframework/session/jdbc/schema-derby.sql) [h2.sql](https://github.com/spring-projects/spring-session/blob/faee8f1bdb8822a5653a81eba838dddf224d92d6/spring-session-jdbc/src/main/resources/org/springframework/session/jdbc/schema-h2.sql) [hsqldb.sql](https://github.com/spring-projects/spring-session/blob/faee8f1bdb8822a5653a81eba838dddf224d92d6/spring-session-jdbc/src/main/resources/org/springframework/session/jdbc/schema-hsqldb.sql) [mysql.sql](https://github.com/spring-projects/spring-session/blob/faee8f1bdb8822a5653a81eba838dddf224d92d6/spring-session-jdbc/src/main/resources/org/springframework/session/jdbc/schema-mysql.sql) [oracle.sql](https://github.com/spring-projects/spring-session/blob/faee8f1bdb8822a5653a81eba838dddf224d92d6/spring-session-jdbc/src/main/resources/org/springframework/session/jdbc/schema-oracle.sql) [postgresql.sql](https://github.com/spring-projects/spring-session/blob/faee8f1bdb8822a5653a81eba838dddf224d92d6/spring-session-jdbc/src/main/resources/org/springframework/session/jdbc/schema-postgresql.sql) [sqlite.sql](https://github.com/spring-projects/spring-session/blob/faee8f1bdb8822a5653a81eba838dddf224d92d6/spring-session-jdbc/src/main/resources/org/springframework/session/jdbc/schema-sqlite.sql) [sqlserver.sql](https://github.com/spring-projects/spring-session/blob/faee8f1bdb8822a5653a81eba838dddf224d92d6/spring-session-jdbc/src/main/resources/org/springframework/session/jdbc/schema-sqlserver.sql) [sybase.sql](https://github.com/spring-projects/spring-session/blob/faee8f1bdb8822a5653a81eba838dddf224d92d6/spring-session-jdbc/src/main/resources/org/springframework/session/jdbc/schema-sybase.sql) ### 2. 基于 Redis 的 session 共享 有以下几种方式设置,按照优先级从高到低分别为: 注:redis 也支持集群、哨兵模式,配置方式为标准的 `Spring Data Redis` 模式(以 `spring.redis` 开头的配置项),具体方式请自行研究 `Spring Data Redis` 相关文档或咨询 `Spring Data` Group #### 2.1 System Property ```bash -Dspring.session.store-type=redis -Dspring.redis.host=xxx -Dspring.redis.port=xxx -Dspring.redis.username=xxx -Dspring.redis.password=xxx ``` #### 2.2 环境变量 ```bash export SPRING_SESSION_STORE_TYPE="redis" export SPRING_REDIS_HOST="xxx" export SPRING_REDIS_PORT="xxx" export SPRING_REDIS_USERNAME="xxx" export SPRING_REDIS_PASSWORD="xxx" ``` #### 2.3 外部配置文件 例如 `config/application-github.properties` ```properties spring.session.store-type=redis spring.redis.host=xxx spring.redis.port=xxx spring.redis.username=xxx spring.redis.password=xxx ``` ### 3. 不启用 session 共享 有以下几种方式设置,按照优先级从高到低分别为: #### 3.1 System Property ```bash -Dspring.session.store-type=none ``` #### 3.2 环境变量 ```bash export SPRING_SESSION_STORE_TYPE="none" ``` #### 3.3 外部配置文件 例如 `config/application-github.properties` ```properties spring.session.store-type=none ``` ================================================ FILE: docs/zh/extension/portal-how-to-enable-webhook-notification.md ================================================ 从 1.8.0 版本开始,apollo 增加了 webhook 支持,从而可以在配置发布时触发 webhook 并告知配置发布的信息。 ## 启用方式 > 配置项统一存储在ApolloPortalDB.ServerConfig表中,也可以通过`管理员工具 - 系统参数`页面进行配置,修改完一分钟实时生效。 1. webhook.supported.envs 开启 webhook 的环境列表,多个环境以英文逗号分隔,如 ``` DEV,FAT,UAT,PRO ``` 2. config.release.webhook.service.url webhook 通知的 url 地址,需要接收 HTTP POST 请求。如有多个地址,以英文逗号分隔,如 ``` http://www.xxx.com/webhook1,http://www.xxx.com/webhook2 ``` ## Webhook 接入方式 1. URL 参数 参数名 | 参数说明 --- | --- env | 该次配置发布所在的环境 2. Request body sample ```json { "appId": "", // appId "clusterName": "", // 集群 "namespaceName": "", // namespace "operator": "", // 发布人 "releaseId": 2, // releaseId "releaseTitle": "", // releaseTitle "releaseComment": "", // releaseComment "releaseTime": "", // 发布时间 eg:2020-01-01T00:00:00.000+0800 "configuration": [ { // 发布后的全部配置,如果为灰度发布,则为灰度发布后的全部配置 "firstEntity": "", // 配置的key "secondEntity": "" // 配置的value } ], "isReleaseAbandoned": false, "previousReleaseId": 1, // 上一次正式发布的releaseId "operation": // 0-正常发布 1-配置回滚 2-灰度发布 4-全量发布 "operationContext": { // 操作设置的属性配置 "isEmergencyPublish": true/false, // 是否紧急发布 "rules": [ { // 灰度规则 "clientAppId": "", // appId "clientIpList": [ "10.0.0.2", "10.0.0.3" ] // IP列表 } ], "branchReleaseKeys": [ "", "" ] // 灰度发布的key } } ``` ================================================ FILE: docs/zh/extension/portal-how-to-implement-user-login-function.md ================================================ Apollo是配置管理系统,会提供权限管理(Authorization),理论上是不负责用户登录认证功能的实现(Authentication)。 所以Apollo定义了一些SPI用来解耦,Apollo接入登录的关键就是实现这些SPI。 ## 实现方式一:使用Apollo提供的Spring Security简单认证 可能很多公司并没有统一的登录认证系统,如果自己实现一套会比较麻烦。Apollo针对这种情况,从0.9.0开始提供了利用Spring Security实现的Http Basic简单认证版本。 使用步骤如下: ### 1. 安装0.9.0以上版本 >如果之前是0.8.0版本,需要导入[apolloportaldb-v080-v090.sql](https://github.com/apolloconfig/apollo/blob/master/scripts/sql/profiles/mysql-default/delta/v080-v090/apolloportaldb-v080-v090.sql) 查看ApolloPortalDB,应该已经存在`Users`表,并有一条初始记录。初始用户名是apollo,密码是admin。 |Id|Username|Password|Email|Enabled| |-|-|-|-|-| |1|apollo|$2a$10$7r20uS.BQ9uBpf3Baj3uQOZvMVvB1RN3PYoKE94gtz2.WAOuiiwXS|apollo@acme.com|1| ### 2. 重启Portal 如果是IDE启动的话,确保`-Dapollo_profile=github,auth` ### 3. 添加用户 超级管理员登录系统后打开`管理员工具 - 用户管理`即可添加用户。 ### 4. 修改用户密码 超级管理员登录系统后打开`管理员工具 - 用户管理`,输入用户名和密码后即可修改用户密码,建议同时修改超级管理员apollo的密码。 ## 实现方式二: 接入LDAP 从1.2.0版本开始,Apollo支持了ldap协议的登录,按照如下方式配置即可。 > 如果采用helm chart部署方式,建议通过配置方式实现,不用修改镜像,可以参考[启用 LDAP 支持](zh/deployment/distributed-deployment-guide#_241449-启用-ldap-支持) ### 1. OpenLDAP接入方式 #### 1.1 配置`application-ldap.yml` 解压`apollo-portal-x.x.x-github.zip`后,在`config`目录下创建`application-ldap.yml`,内容参考如下([样例](https://github.com/apolloconfig/apollo/blob/master/apollo-portal/src/main/resources/application-ldap-openldap-sample.yml)),相关的内容需要按照具体情况调整: ```yml spring: ldap: base: "dc=example,dc=org" username: "cn=admin,dc=example,dc=org" # 配置管理员账号,用于搜索、匹配用户 password: "password" search-filter: "(uid={0})" # 用户过滤器,登录的时候用这个过滤器来搜索用户 urls: - "ldap://localhost:389" ldap: mapping: # 配置 ldap 属性 object-class: "inetOrgPerson" # ldap 用户 objectClass 配置 login-id: "uid" # ldap 用户惟一 id,用来作为登录的 id user-display-name: "cn" # ldap 用户名,用来作为显示名 email: "mail" # ldap 邮箱属性 ``` ##### 1.1.1 基于memberOf过滤用户 OpenLDAP[开启memberOf特性](https://myanbin.github.io/post/enable-memberof-in-openldap.html)后,可以配置filter从而缩小用户搜索的范围: ```yml spring: ldap: base: "dc=example,dc=org" username: "cn=admin,dc=example,dc=org" # 配置管理员账号,用于搜索、匹配用户 password: "password" search-filter: "(uid={0})" # 用户过滤器,登录的时候用这个过滤器来搜索用户 urls: - "ldap://localhost:389" ldap: mapping: # 配置 ldap 属性 object-class: "inetOrgPerson" # ldap 用户 objectClass 配置 login-id: "uid" # ldap 用户惟一 id,用来作为登录的 id user-display-name: "cn" # ldap 用户名,用来作为显示名 email: "mail" # ldap 邮箱属性 filter: # 配置过滤,目前只支持 memberOf memberOf: "cn=ServiceDEV,ou=DEV,dc=example,dc=org|cn=WebDEV,ou=DEV,dc=example,dc=org" # 只允许 memberOf 属性为 ServiceDEV 和 WebDEV 的用户访问 ``` ##### 1.1.2 基于Group过滤用户 从1.3.0版本开始支持基于Group过滤用户,从而可以控制只有特定Group的用户可以登录和使用apollo: ```yml spring: ldap: base: "dc=example,dc=org" username: "cn=admin,dc=example,dc=org" # 配置管理员账号,用于搜索、匹配用户 password: "password" search-filter: "(uid={0})" # 用户过滤器,登录的时候用这个过滤器来搜索用户 urls: - "ldap://localhost:389" ldap: mapping: # 配置 ldap 属性 object-class: "inetOrgPerson" # ldap 用户 objectClass 配置 login-id: "uid" # ldap 用户惟一 id,用来作为登录的 id rdnKey: "uid" # ldap rdn key user-display-name: "cn" # ldap 用户名,用来作为显示名 email: "mail" # ldap 邮箱属性 group: # 启用group search,启用后只有特定group的用户可以登录apollo object-class: "posixGroup" # 配置groupClassName group-base: "ou=group" # group search base group-search: "(&(cn=dev))" # group filter group-membership: "memberUid" # group memberShip eg. member or memberUid ``` #### 1.2 配置`startup.sh` 修改`scripts/startup.sh`,指定`spring.profiles.active`为`github,ldap`。 ```bash SERVICE_NAME=apollo-portal ## Adjust log dir if necessary LOG_DIR=/opt/logs ## Adjust server port if necessary SERVER_PORT=8070 export JAVA_OPTS="$JAVA_OPTS -Dspring.profiles.active=github,ldap" ``` ### 2. Active Directory接入方式 #### 2.1 配置`application-ldap.yml` 解压`apollo-portal-x.x.x-github.zip`后,在`config`目录下创建`application-ldap.yml`,内容参考如下([样例](https://github.com/apolloconfig/apollo/blob/master/apollo-portal/src/main/resources/application-ldap-activedirectory-sample.yml)),相关的内容需要按照具体情况调整: ```yml spring: ldap: base: "dc=example,dc=com" username: "admin" # 配置管理员账号,用于搜索、匹配用户 password: "password" search-filter: "(sAMAccountName={0})" # 用户过滤器,登录的时候用这个过滤器来搜索用户 urls: - "ldap://1.1.1.1:389" ldap: mapping: # 配置 ldap 属性 object-class: "user" # ldap 用户 objectClass 配置 login-id: "sAMAccountName" # ldap 用户惟一 id,用来作为登录的 id user-display-name: "cn" # ldap 用户名,用来作为显示名 email: "userPrincipalName" # ldap 邮箱属性 filter: # 可选项,配置过滤,目前只支持 memberOf memberOf: "CN=ServiceDEV,OU=test,DC=example,DC=com|CN=WebDEV,OU=test,DC=example,DC=com" # 只允许 memberOf 属性为 ServiceDEV 和 WebDEV 的用户访问 ``` #### 2.2 配置`startup.sh` 修改`scripts/startup.sh`,指定`spring.profiles.active`为`github,ldap`。 ```bash SERVICE_NAME=apollo-portal ## Adjust log dir if necessary LOG_DIR=/opt/logs ## Adjust server port if necessary SERVER_PORT=8070 export JAVA_OPTS="$JAVA_OPTS -Dspring.profiles.active=github,ldap" ``` ### 3. ApacheDS接入方式 #### 3.1 配置`application-ldap.yml` 解压`apollo-portal-x.x.x-github.zip`后,在`config`目录下创建`application-ldap.yml`,内容参考如下([样例](https://github.com/apolloconfig/apollo/blob/master/apollo-portal/src/main/resources/application-ldap-apacheds-sample.yml)),相关的内容需要按照具体情况调整: ```yml spring: ldap: base: "dc=example,dc=com" username: "uid=admin,ou=system" # 配置管理员账号,用于搜索、匹配用户 password: "password" search-filter: "(uid={0})" # 用户过滤器,登录的时候用这个过滤器来搜索用户 urls: - "ldap://localhost:10389" ldap: mapping: # 配置 ldap 属性 object-class: "inetOrgPerson" # ldap 用户 objectClass 配置 login-id: "uid" # ldap 用户惟一 id,用来作为登录的 id user-display-name: "displayName" # ldap 用户名,用来作为显示名 email: "mail" # ldap 邮箱属性 ``` ##### 3.1.1 基于Group过滤用户 从1.3.0版本开始支持基于Group过滤用户,从而可以控制只有特定Group的用户可以登录和使用apollo: ```yml spring: ldap: base: "dc=example,dc=com" username: "uid=admin,ou=system" # 配置管理员账号,用于搜索、匹配用户 password: "password" search-filter: "(uid={0})" # 用户过滤器,登录的时候用这个过滤器来搜索用户 urls: - "ldap://localhost:10389" ldap: mapping: # 配置 ldap 属性 object-class: "inetOrgPerson" # ldap 用户 objectClass 配置 login-id: "uid" # ldap 用户惟一 id,用来作为登录的 id rdnKey: "cn" # ldap rdn key user-display-name: "displayName" # ldap 用户名,用来作为显示名 email: "mail" # ldap 邮箱属性 group: # 配置ldap group,启用后只有特定group的用户可以登录apollo object-class: "groupOfNames" # 配置groupClassName group-base: "ou=group" # group search base group-search: "(&(cn=dev))" # group filter group-membership: "member" # group memberShip eg. member or memberUid ``` #### 3.2 配置`startup.sh` 修改`scripts/startup.sh`,指定`spring.profiles.active`为`github,ldap`。 ```bash SERVICE_NAME=apollo-portal ## Adjust log dir if necessary LOG_DIR=/opt/logs ## Adjust server port if necessary SERVER_PORT=8070 export JAVA_OPTS="$JAVA_OPTS -Dspring.profiles.active=github,ldap" ``` ## 实现方式三: 接入OIDC 从 1.8.0 版本开始支持 OpenID Connect 登录, 这种实现方式的前提是已经部署了 OpenID Connect 登录服务 配置前需要准备: * OpenID Connect 的提供者配置端点(符合 RFC 8414 标准的 issuer-uri), 需要是 **https** 的, 例如 https://host:port/auth/realms/apollo/.well-known/openid-configuration * 在 OpenID Connect 服务里创建一个 client, idToken 的签名算法必须设置为 **RS256**, 获取 client-id 以及对应的 client-secret ### 1. 配置 `application-oidc.yml` 解压`apollo-portal-x.x.x-github.zip`后,在`config`目录下创建`application-oidc.yml`,内容参考如下([样例](https://github.com/apolloconfig/apollo/blob/master/apollo-portal/src/main/resources/application-oidc-sample.yml)),相关的内容需要按照具体情况调整: #### 1.1 最小配置 ```yml server: # 解析反向代理请求头 forward-headers-strategy: framework spring: security: oauth2: client: provider: # provider-name 是 oidc 提供者的名称, 任意字符均可, registration 的配置需要用到这个名称 : # 必须是 https, oidc 的 issuer-uri # 例如 你的 issuer-uri 是 https://host:port/auth/realms/apollo/.well-known/openid-configuration, 那么此处只需要配置 https://host:port/auth/realms/apollo 即可, spring boot 处理的时候会加上 /.well-known/openid-configuration 的后缀 issuer-uri: https://host:port/auth/realms/apollo registration: # registration-name 是 oidc 客户端的名称, 任意字符均可, oidc 登录必须配置一个 authorization_code 类型的 registration : # oidc 登录必须配置一个 authorization_code 类型的 registration authorization-grant-type: authorization_code client-authentication-method: client_secret_basic # client-id 是在 oidc 提供者处配置的客户端ID, 用于登录 provider client-id: apollo-portal # provider 的名称, 需要和上面配置的 provider 名称保持一致 provider: # openid 为 oidc 登录的必须 scope, 此处可以添加其它自定义的 scope scope: - openid # client-secret 是在 oidc 提供者处配置的客户端密码, 用于登录 provider # 从安全角度考虑更推荐使用环境变量来配置, 环境变量的命名规则为: 将配置项的 key 当中的 点(.)、横杠(-)替换为下划线(_), 然后将所有字母改为大写, spring boot 会自动处理符合此规则的环境变量 # 例如 spring.security.oauth2.client.registration..client-secret -> SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION__CLIENT_SECRET ( 可以替换为自定义的 oidc 客户端的名称) client-secret: d43c91c0-xxxx-xxxx-xxxx-xxxxxxxxxxxx ``` #### 1.2 扩展配置 * 如果 OpenID Connect 登录服务支持 client_credentials 模式, 还可以再配置一个 client_credentials 类型的 registration, 用于 apollo-portal 作为客户端请求其它被 oidc 保护的资源 * 如果 OpenID Connect 登录服务支持 jwt, 还可以配置 ${spring.security.oauth2.resourceserver.jwt.issuer-uri}, 以支持通过 jwt 访问 apollo-portal ```yml server: # 解析反向代理请求头 forward-headers-strategy: framework spring: security: oauth2: client: provider: # provider-name 是 oidc 提供者的名称, 任意字符均可, registration 的配置需要用到这个名称 : # 必须是 https, oidc 的 issuer-uri, 和 jwt 的 issuer-uri 一致的话直接引用即可, 也可以单独设置 issuer-uri: ${spring.security.oauth2.resourceserver.jwt.issuer-uri} registration: # registration-name 是 oidc 客户端的名称, 任意字符均可, oidc 登录必须配置一个 authorization_code 类型的 registration : # oidc 登录必须配置一个 authorization_code 类型的 registration authorization-grant-type: authorization_code client-authentication-method: client_secret_basic # client-id 是在 oidc 提供者处配置的客户端ID, 用于登录 provider client-id: apollo-portal # provider 的名称, 需要和上面配置的 provider 名称保持一致 provider: # openid 为 oidc 登录的必须 scope, 此处可以添加其它自定义的 scope scope: - openid # client-secret 是在 oidc 提供者处配置的客户端密码, 用于登录 provider # 从安全角度考虑更推荐使用环境变量来配置, 环境变量的命名规则为: 将配置项的 key 当中的 点(.)、横杠(-)替换为下划线(_), 然后将所有字母改为大写, spring boot 会自动处理符合此规则的环境变量 # 例如 spring.security.oauth2.client.registration..client-secret -> SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION__CLIENT_SECRET ( 可以替换为自定义的 oidc 客户端的名称) client-secret: d43c91c0-xxxx-xxxx-xxxx-xxxxxxxxxxxx # registration-name-client 是 oidc 客户端的名称, 任意字符均可, client_credentials 类型的 registration 为选填项, 可以不配置 registration-name-client: # client_credentials 类型的 registration 为选填项, 用于 apollo-portal 作为客户端请求其它被 oidc 保护的资源, 可以不配置 authorization-grant-type: client_credentials client-authentication-method: client_secret_basic # client-id 是在 oidc 提供者处配置的客户端ID, 用于登录 provider client-id: apollo-portal # provider 的名称, 需要和上面配置的 provider 名称保持一致 provider: # openid 为 oidc 登录的必须 scope, 此处可以添加其它自定义的 scope scope: - openid # client-secret 是在 oidc 提供者处配置的客户端密码, 用于登录 provider, 多个 registration 的密码如果一致可以直接引用 client-secret: ${spring.security.oauth2.client.registration.registration-name.client-secret} resourceserver: jwt: # 必须是 https, jwt 的 issuer-uri # 例如 你的 issuer-uri 是 https://host:port/auth/realms/apollo/.well-known/openid-configuration, 那么此处只需要配置 https://host:port/auth/realms/apollo 即可, spring boot 处理的时候会自动加上 /.well-known/openid-configuration 的后缀 issuer-uri: https://host:port/auth/realms/apollo ``` #### 1.3 用户显示名配置 用户的显示名支持自定义配置, 在 `application-oidc.yml` 添加配置项即可 * 可以使用的 oidc 标准 claim name 详见 https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims , 非标准个性化 claim name 请咨询你的 OpenID Connect 登录服务管理员 * oidc 交互式登录用户的显示名配置项为 `spring.security.oidc.user-display-name-claim-name`, 未配置的情况下默认取 `preferred_username`, 该字段为空则尝试获取 `name` * oidc jwt 方式登录用户的显示名配置项为 `spring.security.oidc.jwt-user-display-name-claim-name`, 无默认值 ##### 1.3.1 用户显示名配置示例 * 例如在进行 oidc 交互式登录时使用 `name` 作为显示名, 则配置如下 ```yml spring: security: oidc: user-display-name-claim-name: "name" ``` * 例如在进行 oidc 交互式登录时使用 `email` 作为显示名, 则配置如下 ```yml spring: security: oidc: user-display-name-claim-name: "email" ``` * jwt 的标准 claim name (https://tools.ietf.org/html/rfc7519#section-4) 里面没有适合作为用户显示名的字段, 所以需要 OpenID Connect 登录服务管理员添加非标准的个性化字段 * 例如使用 oidc jwt 登录时, OpenID Connect 登录服务提供了一个名为 `user_display_name` 的个性化字段, 你想要将这个字段作为显示名, 则配置如下 ```yml spring: security: oidc: jwt-user-display-name-claim-name: "user_display_name" ``` * 支持同时配置 oidc 交互式登录名 和 oidc jwt 登录名 * 例如根据登录方式不同, 进行 oidc 交互式登录时候使用 `name` 作为显示名, 进行 oidc jwt 登录时使用 `user_display_name` 作为显示名, 则配置如下 ```yml spring: security: oidc: user-display-name-claim-name: "name" jwt-user-display-name-claim-name: "user_display_name" ``` ### 2. 配置 `startup.sh` 修改`scripts/startup.sh`,指定`spring.profiles.active`为`github,oidc`。 ```bash SERVICE_NAME=apollo-portal ## Adjust log dir if necessary LOG_DIR=/opt/logs ## Adjust server port if necessary SERVER_PORT=8070 export JAVA_OPTS="$JAVA_OPTS -Dspring.profiles.active=github,oidc" ``` ### 3. 配置 apollo-portal 启用 https #### 3.1 添加反向代理 header 这里以 nginx 为例, 将以下配置直接添加或者 include (推荐) 到 nginx 的 http 配置段内 ```nginx server { listen 80 default_server; location / { # 把 80 端口的请求全部都重定向到 https return 301 https://$http_host$request_uri; } } server { # nginx 版本较低不支持 http2 的, 则配置 listen 443 ssl; listen 443 ssl http2; server_name xxx; # ssl 证书, nginx 需要使用完整证书链的证书 ssl_certificate /etc/nginx/ssl/xxx.crt; ssl_certificate_key /etc/nginx/ssl/xxx.key; # ... 其余 ssl 配置 location / { proxy_pass http://apollo-portal-dev:8070; proxy_set_header x-real-ip $remote_addr; proxy_set_header x-forwarded-for $proxy_add_x_forwarded_for; # !!!这里必须是 $http_host, 如果配置成 $host 会导致跳转的时候端口错误 proxy_set_header host $http_host; proxy_set_header x-forwarded-proto $scheme; proxy_http_version 1.1; } } ``` #### 3.2 检查 application-oidc.yml 配置 在 `application-oidc.yml` 里必须存在配置项 `server.forward-headers-strategy=framework` ```yml server: # 解析反向代理请求头 forward-headers-strategy: framework ``` #### 3.3 添加 OpenID Connect 登录服务的重定向地址白名单 出于安全考虑, 一般来说 OpenID Connect 登录服务对重定向的地址会有白名单限制, 所以需要将 apollo-portal 的 https 地址添加到白名单才能正常重定向 ## 实现方式四: 接入公司的统一登录认证系统 这种实现方式的前提是公司已经有统一的登录认证系统,最常见的比如SSO、LDAP等。接入时,实现以下SPI。其中UserService和UserInfoHolder是必须要实现的。 接口说明如下: * UserService(Required):用户服务,用来给Portal提供用户搜索相关功能 * UserInfoHolder(Required):获取当前登录用户信息,SSO一般都是把当前登录用户信息放在线程ThreadLocal上 * LogoutHandler(Optional):用来实现登出功能 * SsoHeartbeatHandler(Optional):Portal页面如果长时间不刷新,登录信息会过期。通过此接口来刷新登录信息 可以参考apollo-portal下的[com.ctrip.framework.apollo.portal.spi](https://github.com/apolloconfig/apollo/tree/master/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi)这个包下面的四个实现: 1. defaultimpl:默认实现,全局只有apollo一个账号 2. ctrip:ctrip实现,接入了SSO并实现用户搜索、查询接口 3. springsecurity: spring security实现,可以新增用户,修改用户密码等 4. ldap: [@pandalin](https://github.com/pandalin)和[codepiano](https://github.com/codepiano)贡献的ldap实现 实现了相关接口后,可以通过[com.ctrip.framework.apollo.portal.configuration.AuthConfiguration](https://github.com/apolloconfig/apollo/blob/master/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/configuration/AuthConfiguration.java)在运行时替换默认的实现。 接入SSO的思路如下: 1. SSO会提供一个jar包,需要配置一个filter 2. filter会拦截所有请求,检查是否已经登录 3. 如果没有登录,那么就会跳转到SSO的登录页面 4. 在SSO登录页面登录成功后,会跳转回apollo的页面,带上认证的信息 5. 再次进入SSO的filter,校验认证信息,把用户的信息保存下来,并且把用户凭证写入cookie或分布式session,以免下次还要重新登录 6. 进入Apollo的代码,Apollo的代码会调用UserInfoHolder.getUser获取当前登录用户 注意,以上1-5这几步都是SSO的代码,不是Apollo的代码,Apollo的代码只需要你实现第6步。 >注:运行时使用不同的实现是通过[Profiles](http://docs.spring.io/autorepo/docs/spring-boot/current/reference/html/boot-features-profiles.html)实现的,比如你自己的sso实现是在`custom` profile中的话,在打包脚本中可以指定-Dapollo_profile=github,custom。其中`github`是Apollo必须的一个profile,用于数据库的配置,`custom`是你自己实现的profile。同时需要注意在[AuthConfiguration](https://github.com/apolloconfig/apollo/blob/master/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/configuration/AuthConfiguration.java)中修改默认实现的条件 ,从`@ConditionalOnMissingProfile({"ctrip", "auth", "ldap"})`改为`@ConditionalOnMissingProfile({"ctrip", "auth", "ldap", "custom"})`。 ================================================ FILE: docs/zh/faq/common-issues-in-deployment-and-development-phase.md ================================================ ### 1. windows怎么执行build.sh? 安装[Git Bash](https://git-for-windows.github.io/),然后运行 “./build.sh” 注意前面 “./” ### 2. 本地运行时Portal一直报Env is down. 默认config service启动在8080端口,admin service启动在8090端口。请确认这两个端口是否被其它应用程序占用。 如果还伴有异常信息:org.springframework.web.client.HttpClientErrorException: 405 Method Not Allowed,一般是由于本地启动了`ShadowSocks`,因为`ShadowSocks`默认会占用8090端口。 1.1.0版本增加了**系统信息**页面,可以通过`管理员工具` -> `系统信息`查看当前各个环境的Meta Server以及admin service信息,有助于排查问题。 ### 3. admin server 或者 config server 注册了内网IP,导致portal或者client访问不了admin server或config server 请参考[网络策略](zh/deployment/distributed-deployment-guide?id=_14-网络策略)。 ### 4. Portal如何增加环境? #### 4.1 1.6.0及以上的版本 1.6.0版本增加了自定义环境的功能,可以在不修改代码的情况增加环境 1. protaldb增加环境,参考[3.1 调整ApolloPortalDB配置](zh/deployment/distributed-deployment-guide?id=_31-调整apolloportaldb配置) 2. 为apollo-portal添加新增环境对应的meta server地址,具体参考:[2.2.1.1.2.4 配置apollo-portal的meta service信息](zh/deployment/distributed-deployment-guide?id=_221124-配置apollo-portal的meta-service信息)。apollo-client在新的环境下使用时也需要做好相应的配置,具体参考:[1.2.2 Apollo Meta Server](zh/client/java-sdk-user-guide?id=_122-apollo-meta-server)。 >注1:一套Portal可以管理多个环境,但是每个环境都需要独立部署一套Config Service、Admin Service和ApolloConfigDB,具体请参考:[2.1.2 创建ApolloConfigDB](zh/deployment/distributed-deployment-guide?id=_212-创建apolloconfigdb),[3.2 调整ApolloConfigDB配置](zh/deployment/distributed-deployment-guide?id=_32-调整apolloconfigdb配置),[2.2.1.1.2 配置数据库连接信息](zh/deployment/distributed-deployment-guide?id=_22112-配置数据库连接信息) > 注2:如果是为已经运行了一段时间的Apollo配置中心增加环境,别忘了参考[2.1.2.4 从别的环境导入ApolloConfigDB的项目数据](zh/deployment/distributed-deployment-guide?id=_2124-从别的环境导入apolloconfigdb的项目数据)对新的环境做初始化 > 注3:如果自定义的环境名称为 PROD,会被强制转换为 PRO。FWS 会被强制转换为 FAT。 #### 4.2 1.5.1及之前的版本 ##### 4.2.1 添加Apollo预先定义好的环境 如果需要添加的环境是Apollo预先定义的环境(DEV, FAT, UAT, PRO),需要两步操作: 1. protaldb增加环境,参考[3.1 调整ApolloPortalDB配置](zh/deployment/distributed-deployment-guide?id=_31-调整apolloportaldb配置) 2. 为apollo-portal添加新增环境对应的meta server地址,具体参考:[2.2.1.1.2.4 配置apollo-portal的meta service信息](zh/deployment/distributed-deployment-guide?id=_221124-配置apollo-portal的meta-service信息)。apollo-client在新的环境下使用时也需要做好相应的配置,具体参考:[1.2.2 Apollo Meta Server](zh/client/java-sdk-user-guide?id=_122-apollo-meta-server)。 >注1:一套Portal可以管理多个环境,但是每个环境都需要独立部署一套Config Service、Admin Service和ApolloConfigDB,具体请参考:[2.1.2 创建ApolloConfigDB](zh/deployment/distributed-deployment-guide?id=_212-创建apolloconfigdb),[3.2 调整ApolloConfigDB配置](zh/deployment/distributed-deployment-guide?id=_32-调整apolloconfigdb配置),[2.2.1.1.2 配置数据库连接信息](zh/deployment/distributed-deployment-guide?id=_22112-配置数据库连接信息) > 注2:如果是为已经运行了一段时间的Apollo配置中心增加环境,别忘了参考[2.1.2.4 从别的环境导入ApolloConfigDB的项目数据](zh/deployment/distributed-deployment-guide?id=_2124-从别的环境导入apolloconfigdb的项目数据)对新的环境做初始化 ##### 4.2.2 添加自定义的环境 如果需要添加的环境不是Apollo预先定义的环境,请参照如下步骤操作: 1. 假设需要添加的环境名称叫beta 2. 修改[com.ctrip.framework.apollo.core.enums.Env](https://github.com/apolloconfig/apollo/blob/master/apollo-core/src/main/java/com/ctrip/framework/apollo/core/enums/Env.java)类,在其中加入`BETA`枚举: ```java public enum Env{ LOCAL, DEV, BETA, FWS, FAT, UAT, LPT, PRO, TOOLS, UNKNOWN; ... } ``` 3. 修改[com.ctrip.framework.apollo.core.enums.EnvUtils](https://github.com/apolloconfig/apollo/blob/master/apollo-core/src/main/java/com/ctrip/framework/apollo/core/enums/EnvUtils.java)类,在其中加入`BETA`枚举的转换逻辑: ```java public final class EnvUtils { public static Env transformEnv(String envName) { if (StringUtils.isBlank(envName)) { return Env.UNKNOWN; } switch (envName.trim().toUpperCase()) { ... case "BETA": return Env.BETA; ... default: return Env.UNKNOWN; } } } ``` 4. 修改[apollo-env.properties](https://github.com/apolloconfig/apollo/blob/master/apollo-portal/src/main/resources/apollo-env.properties),增加`beta.meta`占位符: ```properties local.meta=http://localhost:8080 dev.meta=${dev_meta} fat.meta=${fat_meta} beta.meta=${beta_meta} uat.meta=${uat_meta} lpt.meta=${lpt_meta} pro.meta=${pro_meta} ``` 5. 修改[com.ctrip.framework.apollo.core.internals.LegacyMetaServerProvider](https://github.com/apolloconfig/apollo/blob/master/apollo-core/src/main/java/com/ctrip/framework/apollo/core/internals/LegacyMetaServerProvider.java)类,增加读取`BETA`环境的meta server地址逻辑: ```java public class LegacyMetaServerProvider { ... domains.put(Env.BETA, getMetaServerAddress(prop, "beta_meta", "beta.meta")); ... } ``` 6. protaldb增加`BETA`环境,参考[3.1 调整ApolloPortalDB配置](zh/deployment/distributed-deployment-guide?id=_31-调整apolloportaldb配置) 7. 为apollo-portal添加新增环境对应的meta server地址,具体参考:[2.2.1.1.2.4 配置apollo-portal的meta service信息](zh/deployment/distributed-deployment-guide?id=_221124-配置apollo-portal的meta-service信息)。apollo-client在新的环境下使用时也需要做好相应的配置,具体参考:[1.2.2 Apollo Meta Server](zh/client/java-sdk-user-guide?id=_122-apollo-meta-server)。 >注1:一套Portal可以管理多个环境,但是每个环境都需要独立部署一套Config Service、Admin Service和ApolloConfigDB,具体请参考:[2.1.2 创建ApolloConfigDB](zh/deployment/distributed-deployment-guide?id=_212-创建apolloconfigdb),[3.2 调整ApolloConfigDB配置](zh/deployment/distributed-deployment-guide?id=_32-调整apolloconfigdb配置),[2.2.1.1.2 配置数据库连接信息](zh/deployment/distributed-deployment-guide?id=_22112-配置数据库连接信息) > 注2:如果是为已经运行了一段时间的Apollo配置中心增加环境,别忘了参考[2.1.2.4 从别的环境导入ApolloConfigDB的项目数据](zh/deployment/distributed-deployment-guide?id=_2124-从别的环境导入apolloconfigdb的项目数据)对新的环境做初始化 ### 5. 如何删除应用、集群、Namespace? 0.11.0版本开始Apollo管理员增加了删除应用、集群和AppNamespace的页面,建议使用该页面进行删除。 页面入口: ![delete-app-cluster-namespace-entry](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/delete-app-cluster-namespace-entry.png) 页面详情: ![delete-app-cluster-namespace-detail](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/delete-app-cluster-namespace-detail.png) ### 6. 客户端多块网卡造成获取IP不准,如何解决? 获取客户端网卡逻辑在1.4.0版本有所调整,所以需要根据客户端版本区分 #### 6.1 apollo-client为1.3.0及之前的版本 如果有多网卡,且都是普通网卡的话,需要在/etc/hosts里面加一条映射关系来提升权重。 格式:`ip ${hostname}` 这里的${hostname}就是你在机器上执行hostname的结果。 比如正确IP为:192.168.1.50,hostname执行结果为:jim-ubuntu-pc 那么最终在hosts文件映射的记录为: ``` 192.168.1.50 jim-ubuntu-pc ``` #### 6.2 apollo-client为1.4.0及之后的版本 如果有多网卡,且都是普通网卡的话,可以通过调整它们在系统中的顺序来改变优先级,顺序在前的优先级更高。 ### 7. 通过Apollo动态调整Spring Boot的Logging level 可以参考[apollo-use-cases](https://github.com/ctripcorp/apollo-use-cases)项目中的[spring-cloud-logger](https://github.com/ctripcorp/apollo-use-cases/tree/master/spring-cloud-logger)和[spring-boot-logger](https://github.com/ctripcorp/apollo-use-cases/tree/master/spring-boot-logger)代码示例。 ### 8. 将Config Service和Admin Service注册到单独的Eureka Server上 Apollo默认自带了Eureka作为内部的注册中心实现,一般情况下不需要考虑为Apollo单独部署注册中心。 不过有些公司自己已经有了一套Eureka,如果希望把Apollo的Config Service和Admin Service也注册过去实现统一管理的话,可以按照如下步骤操作: #### 1. 配置Config Service不启动内置Eureka Server ##### 1.1 1.5.0及以上版本 为apollo-configservice配置`apollo.eureka.server.enabled=false`即可,通过bootstrap.yml或-D参数等方式皆可。 ##### 1.2 1.5.0之前的版本 修改[com.ctrip.framework.apollo.configservice.ConfigServiceApplication](https://github.com/apolloconfig/apollo/blob/master/apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/ConfigServiceApplication.java),把`@EnableEurekaServer`改为`@EnableEurekaClient` ```java @EnableEurekaClient @EnableAspectJAutoProxy @EnableAutoConfiguration // (exclude = EurekaClientConfigBean.class) @Configuration @EnableTransactionManagement @PropertySource(value = {"classpath:configservice.properties"}) @ComponentScan(basePackageClasses = {ApolloCommonConfig.class, ApolloBizConfig.class, ConfigServiceApplication.class, ApolloMetaServiceConfig.class}) public class ConfigServiceApplication { ... } ``` #### 2. 修改ApolloConfigDB.ServerConfig表中的`eureka.service.url`,指向自己的Eureka地址 比如自己的Eureka服务地址是1.1.1.1:8761和2.2.2.2:8761,那么就将ApolloConfigDB.ServerConfig表中设置eureka.service.url为: ``` http://1.1.1.1:8761/eureka/,http://2.2.2.2:8761/eureka/ ``` 需要注意的是更改Eureka地址只需要改ApolloConfigDB.ServerConfig表中的`eureka.service.url`即可,不需要修改meta server地址。 > 默认情况下,meta service和config service是部署在同一个JVM进程,所以meta service的地址就是config service的地址,修改Eureka地址时不需要修改meta server地址。 ### 9. Spring Boot中使用`ConditionalOnProperty`读取不到配置 `@ConditionalOnProperty`功能从0.10.0版本开始支持,具体可以参考 [Spring Boot集成方式](zh/client/java-sdk-user-guide?id=_3213-spring-boot集成方式(推荐)) ### 10. 多机房如何实现A机房的客户端就近读取A机房的config service,B机房的客户端就近读取B机房的config service? 请参考[Issue 1294](https://github.com/apolloconfig/apollo/issues/1294),该案例中由于中美机房相距甚远,所以需要config db两地部署,如果是同城多机房的话,两个机房的config service可以连同一个config db。 ### 11. apollo是否有支持HEAD请求的页面?阿里云slb配置健康检查只支持HEAD请求 apollo的每个服务都有`/health`页面的,该页面是apollo用来做健康检测的,支持各种请求方法,如GET, POST, HEAD等。 ### 12. apollo如何配置查看权限? 从1.1.0版本开始,apollo-portal增加了查看权限的支持,可以支持配置某个环境只允许项目成员查看私有Namespace的配置。 这里的项目成员是指: 1. 项目的管理员 2. 具备该私有Namespace在该环境下的修改或发布权限 配置方式很简单,用超级管理员账号登录后,进入`管理员工具 - 系统参数`页面新增或修改`configView.memberOnly.envs`配置项即可。 ![configView.memberOnly.envs](https://user-images.githubusercontent.com/837658/46456519-c155e100-c7e1-11e8-969b-8f332379fa29.png) ### 13. apollo如何放在独立的tomcat中跑? 有些公司的运维策略可能会要求必须使用独立的tomcat跑应用,不允许apollo默认的startup.sh方式运行,下面以apollo-configservice为例简述一下如何使apollo服务端运行在独立的tomcat中: 1. 获取apollo代码(生产部署建议用release的版本) 2. 修改apollo-configservice的pom.xml,增加`war` 3. 按照分布式部署文档配置build.sh,然后打包 4. 把apollo-configservice的war包放到tomcat下 * cp apollo-configservice/target/apollo-configservice-xxx.war ${tomcat-dir}/webapps/ROOT.war 运行tomcat的startup.sh 5. 运行tomcat的startup.sh 另外,apollo还有一些调优参数建议在tomcat的server.xml中配置一下,可以参考[application.properties](https://github.com/apolloconfig/apollo/blob/master/apollo-common/src/main/resources/application.properties#L12) ### 14. 注册中心Eureka如何替换为zookeeper? 许多公司微服务项目已经在使用zookeeper,如果出于方便服务管理的目的,希望Eureka替换为zookeeper的情况,可以参考[@hanyidreamer](https://github.com/hanyidreamer)贡献的改造步骤:[注册中心Eureka替换为zookeeper](https://blog.csdn.net/u014732209/article/details/89555535) ### 15. 本地多人同时开发,如何实现配置不一样且互不影响? 参考[#1560](https://github.com/apolloconfig/apollo/issues/1560) ### 16. Portal挂载到nginx/slb后如何设置相对路径? 一般情况下建议直接使用根目录来挂载portal,不过如果有些情况希望和其它应用共用nginx/slb,需要加上相对路径(如/apollo),那么可以按照下面的方式配置。 #### 16.1 Portal为1.7.0及以上版本 首先为apollo-portal增加-D参数`server.servlet.context-path=/apollo`或系统环境变量`SERVER_SERVLET_CONTEXT_PATH=/apollo`。 然后在nginx/slb上配置转发即可,以nginx为例: ``` location /apollo/ { proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Forwarded-Server $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_pass http://127.0.0.1:8070/apollo/; } ``` #### 16.2 Portal为1.6.0及以上版本 首先为portal加上`prefix.path=/apollo`配置参数,配置方式很简单,用超级管理员账号登录后,进入`管理员工具 - 系统参数`页面新增或修改`prefix.path`配置项即可。 然后在nginx/slb上配置转发即可,以nginx为例: ``` location /apollo/ { proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Forwarded-Server $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_pass http://127.0.0.1:8070/; } ``` ### 17. Portal挂载到nginx/slb后如何配置https? 1. 在nginx/slb上配置https访问配置,以nginx为例: ``` server { listen 80 default_server; location / { # 把 80 端口的请求全部都重定向到 https return 301 https://$http_host$request_uri; } } server { # nginx 版本较低不支持 http2 的, 则配置 listen 443 ssl; listen 443 ssl http2; server_name your-domain-name; # ssl 证书, nginx 需要使用完整证书链的证书 ssl_certificate /etc/nginx/ssl/xxx.crt; ssl_certificate_key /etc/nginx/ssl/xxx.key; location / { proxy_pass http://apollo-portal-address:8070; proxy_set_header x-real-ip $remote_addr; proxy_set_header x-forwarded-for $proxy_add_x_forwarded_for; # !!!这里必须是 $http_host, 如果配置成 $host 会导致跳转的时候端口错误 proxy_set_header host $http_host; proxy_set_header x-forwarded-proto $scheme; proxy_http_version 1.1; } } ``` 2. 配置apollo-portal解析反向代理的header信息 修改apollo-portal安装包中config目录下的application-github.properties,增加以下配置: ```properties server.forward-headers-strategy=framework ``` 也可以通过环境变量配置: ``` SERVER_FORWARD_HEADERS_STRATEGY=framework ``` ================================================ FILE: docs/zh/faq/faq.md ================================================ ## 1. Apollo是什么? Apollo(阿波罗)是一款可靠的分布式配置管理中心,诞生于携程框架研发部,能够集中化管理应用不同环境、不同集群的配置,配置修改后能够实时推送到应用端,并且具备规范的权限、流程治理等特性,适用于微服务配置管理场景。 更多介绍,可以参考[Apollo配置中心介绍](zh/design/apollo-introduction) ## 2. Cluster是什么? 一个应用下不同实例的分组,比如典型的可以按照数据中心分,把A机房的应用实例分为一个集群,把B机房的应用实例分为另一个集群。 ## 3. Namespace是什么? 一个应用下不同配置的分组。 请参考[Apollo核心概念之“Namespace”](zh/design/apollo-core-concept-namespace) ## 4. 我想要接入Apollo,该如何操作? 请参考[Apollo使用指南](zh/portal/apollo-user-guide) ## 5. 我的应用需要不同机房的配置不一样,Apollo是否能支持? Apollo是支持的。请参考[Apollo使用指南](zh/portal/apollo-user-guide)中的`三、集群独立配置说明` ## 6. 我有多个应用需要使用同一份配置,Apollo是否能支持? Apollo是支持的。请参考[Apollo使用指南](zh/portal/apollo-user-guide)中的`四、多个AppId使用同一份配置` ## 7. Apollo是否支持查看权限控制或者配置加密? 从1.1.0版本开始,apollo-portal增加了查看权限的支持,可以支持配置某个环境只允许项目成员查看私有Namespace的配置。 这里的项目成员是指: 1. 项目的管理员 2. 具备该私有Namespace在该环境下的修改或发布权限 配置方式很简单,用超级管理员账号登录后,进入`管理员工具 - 系统参数`页面新增或修改`configView.memberOnly.envs`配置项即可。 ![configView.memberOnly.envs](https://user-images.githubusercontent.com/837658/46456519-c155e100-c7e1-11e8-969b-8f332379fa29.png) 配置加密可以参考[spring-boot-encrypt demo项目](https://github.com/ctripcorp/apollo-use-cases/tree/master/spring-boot-encrypt) ## 8. 如果有多个config server,打包时如何配置meta server地址? 有多台meta server可以通过nginx反向代理,通过一个域名代理多个meta server实现ha。 ## 9. Apollo相比于Spring Cloud Config有什么优势? Spring Cloud Config的精妙之处在于它的配置存储于Git,这就天然的把配置的修改、权限、版本等问题隔离在外。通过这个设计使得Spring Cloud Config整体很简单,不过也带来了一些不便之处。 下面尝试做一个简单的小结: | 功能点 | Apollo | Spring Cloud Config | 备注 | |------------------|--------------------------------------------|-------------------------------------------------|----------------------------------------------------------------------------| | 配置界面 | 一个界面管理不同环境、不同集群配置 | 无,需要通过git操作 | | | 配置生效时间 | 实时 | 重启生效,或手动refresh生效 | Spring Cloud Config需要通过Git webhook,加上额外的消息队列才能支持实时生效 | | 版本管理 | 界面上直接提供发布历史和回滚按钮 | 无,需要通过git操作 | | | 灰度发布 | 支持 | 不支持 | | | 授权、审核、审计 | 界面上直接支持,而且支持修改、发布权限分离 | 需要通过git仓库设置,且不支持修改、发布权限分离 | | | 实例配置监控 | 可以方便的看到当前哪些客户端在使用哪些配置 | 不支持 | | | 配置获取性能 | 快,通过数据库访问,还有缓存支持 | 较慢,需要从git clone repository,然后从文件系统读取 | | | 客户端支持 | 原生支持所有Java和.Net应用,提供API支持其它语言应用,同时也支持Spring annotation获取配置 | 支持Spring应用,提供annotation获取配置 | Apollo的适用范围更广一些 | ## 10. Apollo和Disconf相比有什么优点? 由于我们自己并非Disconf的资深用户,所以无法主观地给出评价。 不过之前Apollo技术支持群中的热心网友[@Krast](https://github.com/krast)做了一个[开源配置中心对比矩阵](https://github.com/apolloconfig/apollo/files/983064/default.pdf),可以参考一下。 ## 11. 拉取最新代码后提示找不到 OpenAppDTO 等 `OpenXxxDTO` 类怎么办? `apollo-portal` 模块通过 OpenAPI 的 YAML 描述文件在编译阶段生成 `OpenXxxDTO` 类。如果在本地开发时发现这类 DTO 消失或编译报错,请先执行一次 Maven 编译流程触发代码生成: ```bash mvn clean compile -pl apollo-portal -am ``` 也可以进入 `apollo-portal` 模块目录直接执行: ```bash mvn clean compile ``` 命令完成后,OpenAPI 相关的 DTO 会在 `com.ctrip.framework.apollo.openapi.model` 包下重新生成。 ================================================ FILE: docs/zh/misc/apollo-benchmark.md ================================================ 很多同学关心Apollo的性能和可靠性,以下数据是采集携程内部生产环境单台机器的数据。监控工具是[Cat](https://github.com/dianping/cat)。 ### 一、测试机器配置 #### 1.1 机器配置 4C12G #### 1.2 JVM参数 ``` -Xms6144m -Xmx6144m -Xss256k -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=384m -XX:NewSize=4096m -XX:MaxNewSize=4096m -XX:SurvivorRatio=8 -XX:+UseParNewGC -XX:ParallelGCThreads=4 -XX:MaxTenuringThreshold=9 -XX:+UseConcMarkSweepGC -XX:+DisableExplicitGC -XX:+UseCMSInitiatingOccupancyOnly -XX:+ScavengeBeforeFullGC -XX:+UseCMSCompactAtFullCollection -XX:+CMSParallelRemarkEnabled -XX:CMSFullGCsBeforeCompaction=9 -XX:CMSInitiatingOccupancyFraction=60 -XX:+CMSClassUnloadingEnabled -XX:SoftRefLRUPolicyMSPerMB=0 -XX:+CMSPermGenSweepingEnabled -XX:CMSInitiatingPermOccupancyFraction=70 -XX:+ExplicitGCInvokesConcurrent -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCApplicationConcurrentTime -XX:+PrintHeapAtGC -XX:+HeapDumpOnOutOfMemoryError -XX:-OmitStackTraceInFastThrow -Duser.timezone=Asia/Shanghai -Dclient.encoding.override=UTF-8 -Dfile.encoding=UTF-8 -Djava.security.egd=file:/dev/./urandom ``` #### 1.3 JVM版本 1.8.0_60 #### 1.4 Apollo版本 0.9.0 #### 1.5 单台机器客户端连接数(客户端数) 5600 #### 1.6 集群总客户端连接数(客户端数) 10W+ ### 二、性能指标 #### 2.1 获取配置Http接口响应时间 QPS: 160 平均响应时间: 0.1ms 95线响应时间: 0.3ms 999线响应时间: 2.5ms >注:config service开启了配置缓存,更多信息可以参考[分布式部署指南中的缓存配置](zh/deployment/distributed-deployment-guide#_323-config-servicecacheenabled-是否开启配置缓存) #### 2.2 Config Server GC情况 YGC: 平均2Min一次,一次耗时300ms OGC: 平均1H一次,一次耗时380ms #### 2.3 CPU指标 LoadAverage:0.5 System CPU利用率:6% Process CPU利用率:8% ================================================ FILE: docs/zh/portal/apollo-open-api-platform.md ================================================ ### 一、 什么是开放平台? Apollo提供了一套的Http REST接口,使第三方应用能够自己管理配置。虽然Apollo系统本身提供了Portal来管理配置,但是在有些情景下,应用需要通过程序去管理配置。 ### 二、 第三方应用接入Apollo开放平台 #### 2.1 注册第三方应用 第三方应用负责人需要向Apollo管理员提供一些第三方应用基本信息。 基本信息如下: * 第三方应用的AppId、应用名、部门 * 第三方应用负责人 Apollo管理员在 `http://{portal_address}/open/add-consumer.html` 创建第三方应用,创建之前最好先查询此AppId是否已经创建。创建成功之后会生成一个token,如下图所示: ![开放平台管理](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/apollo-open-manage.png) #### 2.2 查看第三方应用 Apollo管理员在 `http://{portal_address}/open/manage.html` 页面可以查看第三方应用列表。并提供了【查看Token并赋权】、【删除】等管理操作,如下图所示: ![第三方应用列表](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/apollo-open-manage-list.png) 【查看Token并赋权】的模态框页面如下图所示: ![查看Token并赋权](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/apollo-open-manage-token.png) #### 2.3 给已注册的第三方应用授权 第三方应用不应该能操作任何Namespace的配置,所以需要给token绑定可以操作的Namespace。Apollo管理员在 `http://{portal_address}/open/add-consumer.html` 页面给token赋权。赋权之后,第三方应用就可以通过Apollo提供的Http REST接口来管理已授权的Namespace的配置了。 #### 2.4 第三方应用调用Apollo Open API ##### 2.4.1 调用Http REST接口 任何语言的第三方应用都可以调用Apollo的Open API,在调用接口时,需要设置注意以下两点: * Http Header中增加一个Authorization字段,字段值为申请的token * Http Header的Content-Type字段需要设置成application/json;charset=UTF-8 ##### 2.4.2 Java应用通过apollo-openapi调用Apollo Open API 从1.1.0版本开始,Apollo提供了[apollo-openapi](https://github.com/apolloconfig/apollo/tree/master/apollo-openapi)客户端,所以Java语言的第三方应用可以更方便地调用Apollo Open API。 首先引入`apollo-openapi`依赖: ```xml com.ctrip.framework.apollo apollo-openapi 1.7.0 ``` 在程序中构造`ApolloOpenApiClient`: ```java String portalUrl = "http://localhost:8070"; // portal url String token = "e16e5cd903fd0c97a116c873b448544b9d086de9"; // 申请的token ApolloOpenApiClient client = ApolloOpenApiClient.newBuilder() .withPortalUrl(portalUrl) .withToken(token) .build(); ``` 后续就可以通过`ApolloOpenApiClient`的接口直接操作Apollo Open API了,接口说明参见下面的Rest接口文档。 ##### 2.4.3 .Net core应用调用Apollo Open API .Net core也提供了open api的客户端,详见https://github.com/ctripcorp/apollo.net/pull/77 ##### 2.4.4 Shell Scripts调用Apollo Open API 封装了bash的function,底层使用curl来发送HTTP请求 * bash函数:[openapi.sh](https://github.com/apolloconfig/apollo/blob/master/scripts/openapi/bash/openapi.sh) * 使用示例:[openapi-usage-example.sh](https://github.com/apolloconfig/apollo/blob/master/scripts/openapi/bash/openapi-usage-example.sh) * 全部和openapi有关的shell脚本在文件夹 https://github.com/apolloconfig/apollo/tree/master/scripts/openapi/bash 下 ### 三、 接口文档 #### 3.1 URL路径参数说明 参数名 | 参数说明 --- | --- env | 所管理的配置环境 appId | 所管理的配置AppId clusterName | 所管理的配置集群名, 一般情况下传入 default 即可。如果是特殊集群,传入相应集群的名称即可 namespaceName | 所管理的Namespace的名称,如果是非properties格式,需要加上后缀名,如`sample.yml` #### 3.2 API接口列表 - [3.2.1 获取 App 的环境,集群信息](#_321-获取app的环境,集群信息) - [3.2.2 获取 App 信息](#_322-获取app信息) - [3.2.3 获取集群详细信息](#_323-获取集群接口) - [3.2.4 创建集群](#_324-创建集群接口) - [3.2.5 获取集群下所有 Namespace 信息](#_325-获取集群下所有namespace信息接口) - [3.2.6 获取 Namespace 信息](#_326-获取某个namespace信息接口) - [3.2.7 创建 Namespace](#_327-创建namespace) - [3.2.8 获取 Namespace 当前编辑人](#_328-获取某个namespace当前编辑人接口) - [3.2.9 获取具体配置项](#_329-读取配置接口) - [3.2.10 新增配置项](#_3210-新增配置接口) - [3.2.11 修改配置项](#_3211-修改配置接口) - [3.2.12 删除配置项](#_3212-删除配置接口) - [3.2.13 发布 Namespace](#_3213-发布配置接口) - [3.2.14 获取 Namespace 最后一次发布的内容](#_3214-获取某个namespace当前生效的已发布配置接口) - [3.2.15 回滚 Namespace](#_3215-回滚已发布配置接口) - [3.2.16 分页获取配置项](#_3216-分页获取配置项接口) - [3.2.17 创建App并获取管理员权限](#_3217-创建App并获取管理员权限) ##### 3.2.1 获取App的环境,集群信息 * **URL** : http://{portal_address}/openapi/v1/apps/{appId}/envclusters * **Method** : GET * **Request Params** : 无 * **返回值Sample**: ``` json [ { "env":"FAT", "clusters":[ //集群列表 "default", "FAT381" ] }, { "env":"UAT", "clusters":[ "default" ] }, { "env":"PRO", "clusters":[ "default", "SHAOY", "SHAJQ" ] } ] ``` ##### 3.2.2 获取App信息 * **URL** : http://{portal_address}/openapi/v1/apps * **Method** : GET * **Request Params** : 参数名 | 必选 | 类型 | 说明 --- | --- | --- | --- appIds | false | String | appId列表,以逗号分隔,如果为空则返回所有App信息 * **返回值Sample**: ``` json [ { "name":"first_app", "appId":"100003171", "orgId":"development", "orgName":"研发部", "ownerName":"apollo", "ownerEmail":"test@test.com", "dataChangeCreatedBy":"apollo", "dataChangeLastModifiedBy":"apollo", "dataChangeCreatedTime":"2019-05-08T09:13:31.000+0800", "dataChangeLastModifiedTime":"2019-05-08T09:13:31.000+0800" }, { "name":"apollo-demo", "appId":"100004458", "orgId":"development", "orgName":"产品研发部", "ownerName":"apollo", "ownerEmail":"apollo@cmcm.com", "dataChangeCreatedBy":"apollo", "dataChangeLastModifiedBy":"apollo", "dataChangeCreatedTime":"2018-12-23T12:35:16.000+0800", "dataChangeLastModifiedTime":"2019-04-08T13:58:36.000+0800" } ] ``` ##### 3.2.3 获取集群接口 * **URL** : http://{portal_address}/openapi/v1/envs/{env}/apps/{appId}/clusters/{clusterName} * **Method** : GET * **Request Params** :无 * **返回值Sample**: ``` json { "name":"default", "appId":"100004458", "dataChangeCreatedBy":"apollo", "dataChangeLastModifiedBy":"apollo", "dataChangeCreatedTime":"2018-12-23T12:35:16.000+0800", "dataChangeLastModifiedTime":"2018-12-23T12:35:16.000+0800" } ``` ##### 3.2.4 创建集群接口 可以通过此接口创建集群,调用此接口需要授予第三方APP对目标APP的管理权限。 * **URL** : http://{portal_address}/openapi/v1/envs/{env}/apps/{appId}/clusters * **Method** : POST * **Request Params** :无 * **请求内容(Request Body, JSON格式)** : 参数名 | 必选 | 类型 | 说明 ---- | --- | --- | --- name | true | String | Cluster的名字 appId | true | String | Cluster所属的AppId dataChangeCreatedBy | true | String | namespace的创建人,格式为域账号,也就是sso系统的User ID * **返回值 Sample** : ``` json { "name":"someClusterName", "appId":"100004458", "dataChangeCreatedBy":"apollo", "dataChangeLastModifiedBy":"apollo", "dataChangeCreatedTime":"2018-12-23T12:35:16.000+0800", "dataChangeLastModifiedTime":"2018-12-23T12:35:16.000+0800" } ``` ##### 3.2.5 获取集群下所有Namespace信息接口 * **URL** : http://{portal_address}/openapi/v1/envs/{env}/apps/{appId}/clusters/{clusterName}/namespaces * **Method**: GET * **Request Params**: 无 * **返回值Sample**: ``` json [ { "appId": "100003171", "clusterName": "default", "namespaceName": "application", "comment": "default app namespace", "format": "properties", //Namespace格式可能取值为:properties、xml、json、yml、yaml "isPublic": false, //是否为公共的Namespace "items": [ // Namespace下所有的配置集合 { "key": "batch", "value": "100", "dataChangeCreatedBy": "song_s", "dataChangeLastModifiedBy": "song_s", "dataChangeCreatedTime": "2016-07-21T16:03:43.000+0800", "dataChangeLastModifiedTime": "2016-07-21T16:03:43.000+0800" } ], "dataChangeCreatedBy": "song_s", "dataChangeLastModifiedBy": "song_s", "dataChangeCreatedTime": "2016-07-20T14:05:58.000+0800", "dataChangeLastModifiedTime": "2016-07-20T14:05:58.000+0800" }, { "appId": "100003171", "clusterName": "default", "namespaceName": "FX.apollo", "comment": "apollo public namespace", "format": "properties", "isPublic": true, "items": [ { "key": "request.timeout", "value": "3000", "comment": "", "dataChangeCreatedBy": "song_s", "dataChangeLastModifiedBy": "song_s", "dataChangeCreatedTime": "2016-07-20T14:08:30.000+0800", "dataChangeLastModifiedTime": "2016-08-01T13:56:25.000+0800" }, { "id": 1116, "key": "batch", "value": "3000", "comment": "", "dataChangeCreatedBy": "song_s", "dataChangeLastModifiedBy": "song_s", "dataChangeCreatedTime": "2016-07-28T15:13:42.000+0800", "dataChangeLastModifiedTime": "2016-08-01T13:51:00.000+0800" } ], "dataChangeCreatedBy": "song_s", "dataChangeLastModifiedBy": "song_s", "dataChangeCreatedTime": "2016-07-20T14:08:13.000+0800", "dataChangeLastModifiedTime": "2016-07-20T14:08:13.000+0800" } ] ``` ##### 3.2.6 获取某个Namespace信息接口 * **URL** : http://{portal_address}/openapi/v1/envs/{env}/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName} * **Method** : GET * **Request Params** :无 * **返回值Sample** : ``` json { "appId": "100003171", "clusterName": "default", "namespaceName": "application", "comment": "default app namespace", "format": "properties", //Namespace格式可能取值为:properties、xml、json、yml、yaml "isPublic": false, //是否为公共的Namespace "items": [ // Namespace下所有的配置集合 { "key": "batch", "value": "100", "dataChangeCreatedBy": "song_s", "dataChangeLastModifiedBy": "song_s", "dataChangeCreatedTime": "2016-07-21T16:03:43.000+0800", "dataChangeLastModifiedTime": "2016-07-21T16:03:43.000+0800" } ], "dataChangeCreatedBy": "song_s", "dataChangeLastModifiedBy": "song_s", "dataChangeCreatedTime": "2016-07-20T14:05:58.000+0800", "dataChangeLastModifiedTime": "2016-07-20T14:05:58.000+0800" } ``` ##### 3.2.7 创建Namespace 可以通过此接口创建Namespace,调用此接口需要授予第三方APP对目标APP的管理权限。 * **URL** : http://{portal_address}/openapi/v1/apps/{appId}/appnamespaces * **Method** : POST * **Request Params** :无 * **请求内容(Request Body, JSON格式)** : 参数名 | 必选 | 类型 | 说明 ---- | --- | --- | --- name | true | String | Namespace的名字 appId | true | String | Namespace所属的AppId format |true | String | Namespace的格式,**只能是以下类型: properties、xml、json、yml、yaml** isPublic |true | boolean | 是否是公共文件 comment |false | String | Namespace说明 dataChangeCreatedBy | true | String | namespace的创建人,格式为域账号,也就是sso系统的User ID * **返回值 Sample** : ``` json { "name": "FX.public-0420-11", "appId": "100003173", "format": "properties", "isPublic": true, "comment": "test", "dataChangeCreatedBy": "zhanglea", "dataChangeLastModifiedBy": "zhanglea", "dataChangeCreatedTime": "2017-04-20T18:25:49.033+0800", "dataChangeLastModifiedTime": "2017-04-20T18:25:49.033+0800" } ``` * **返回值说明** : > 如果是properties文件,name = ${appId所属的部门}.${传入的name值} ,例如调用接口传入的name=xy-z, format=properties,应用的部门为框架(FX),那么name=FX.xy-z > 如果不是properties文件 name = ${appId所属的部门}.${传入的name值}.${format},例如调用接口传入的name=xy-z, format=json,应用的部门为框架(FX),那么name=FX.xy-z.json ##### 3.2.8 获取某个Namespace当前编辑人接口 Apollo在生产环境(PRO)有限制规则:每次发布只能有一个人编辑配置,且该次发布的人不能是该次发布的编辑人。 也就是说如果一个用户A修改了某个namespace的配置,那么在这个namespace发布前,只能由A修改,其它用户无法修改。同时,该用户A无法发布自己修改的配置,必须找另一个有发布权限的人操作。 这个接口就是用来获取当前namespace是否有人锁定的接口。在非生产环境(FAT、UAT),该接口始终返回没有人锁定。 * **URL** : http://{portal_address}/openapi/v1/envs/{env}/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/lock * **Method** : GET * **Request Params** :无 * **返回值 Sample(未锁定)** : ``` json { "namespaceName": "application", "isLocked": false } ``` * **返回值Sample(被锁定)** : ``` json { "namespaceName": "application", "isLocked": true, "lockedBy": "song_s" //锁owner } ``` ##### 3.2.9 读取配置接口 * **URL** : http://{portal_address}/openapi/v1/envs/{env}/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/items/{key} * **Method** : GET * **Request Params** :无 * **返回值Sample** : ``` json { "key": "timeout", "value": "3000", "comment": "超时时间", "dataChangeCreatedBy": "zhanglea", "dataChangeLastModifiedBy": "zhanglea", "dataChangeCreatedTime": "2016-08-11T12:06:41.818+0800", "dataChangeLastModifiedTime": "2016-08-11T12:06:41.818+0800" } ``` ##### 3.2.10 新增配置接口 * **URL** : http://{portal_address}/openapi/v1/envs/{env}/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/items * **Method** : POST * **Request Params** :无 * **请求内容(Request Body, JSON格式)** : 参数名 | 必选 | 类型 | 说明 ---- | --- | --- | --- key | true | String | 配置的key,长度不能超过128个字符。非properties格式,key固定为`content` value | true | String | 配置的value,长度不能超过20000个字符,非properties格式,value为文件全部内容 comment | false | String | 配置的备注,长度不能超过256个字符 dataChangeCreatedBy | true | String | item的创建人,格式为域账号,也就是sso系统的User ID * **Request body sample** : ``` json { "key":"timeout", "value":"3000", "comment":"超时时间", "dataChangeCreatedBy":"zhanglea" } ``` * **返回值Sample** : ``` json { "key": "timeout", "value": "3000", "comment": "超时时间", "dataChangeCreatedBy": "zhanglea", "dataChangeLastModifiedBy": "zhanglea", "dataChangeCreatedTime": "2016-08-11T12:06:41.818+0800", "dataChangeLastModifiedTime": "2016-08-11T12:06:41.818+0800" } ``` ##### 3.2.11 修改配置接口 * **URL** : http://{portal_address}/openapi/v1/envs/{env}/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/items/{key} * **Method** : PUT * **Request Params** : 参数名 | 必选 | 类型 | 说明 --- | --- | --- | --- createIfNotExists | false | Boolean | 当配置不存在时是否自动创建 * **请求内容(Request Body, JSON格式)** : 参数名 | 必选 | 类型 | 说明 ---- | --- | --- | --- key | true | String | 配置的key,需和url中的key值一致。非properties格式,key固定为`content` value | true | String | 配置的value,长度不能超过20000个字符,非properties格式,value为文件全部内容 comment | false | String | 配置的备注,长度不能超过256个字符 dataChangeLastModifiedBy | true | String | item的修改人,格式为域账号,也就是sso系统的User ID dataChangeCreatedBy | false | String | 当createIfNotExists为true时必选。item的创建人,格式为域账号,也就是sso系统的User ID * **Request body sample** : ```json { "key":"timeout", "value":"3000", "comment":"超时时间", "dataChangeLastModifiedBy":"zhanglea" } ``` * **返回值** :无 ##### 3.2.12 删除配置接口 * **URL** : http://{portal_address}/openapi/v1/envs/{env}/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/items/{key}?operator={operator} * **Method** : DELETE * **Request Params** : 参数名 | 必选 | 类型 | 说明 --- | --- | --- | --- key | true | String | 配置的key。非properties格式,key固定为`content` operator | true | String | 删除配置的操作者,域账号 * **返回值** : 无 ##### 3.2.13 发布配置接口 * **URL** : http://{portal_address}/openapi/v1/envs/{env}/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/releases * **Method** : POST * **Request Params** :无 * **Request Body** : 参数名 | 必选 | 类型 | 说明 --- | --- | --- | --- releaseTitle | true | String | 此次发布的标题,长度不能超过64个字符 releaseComment | false | String | 发布的备注,长度不能超过256个字符 releasedBy | true | String | 发布人,域账号,注意:如果`ApolloConfigDB.ServerConfig`中的`namespace.lock.switch`设置为true的话(默认是false),那么该环境不允许发布人和编辑人为同一人。所以如果编辑人是zhanglea,发布人就不能再是zhanglea。 * **Request Body example** : ```json { "releaseTitle":"2016-08-11", "releaseComment":"修改timeout值", "releasedBy":"zhanglea" } ``` * **返回值Sample** : ``` json { "appId": "test-0620-01", "clusterName": "test", "namespaceName": "application", "name": "2016-08-11", "configurations": { "timeout": "3000", }, "comment": "修改timeout值", "dataChangeCreatedBy": "zhanglea", "dataChangeLastModifiedBy": "zhanglea", "dataChangeCreatedTime": "2016-08-11T14:03:46.232+0800", "dataChangeLastModifiedTime": "2016-08-11T14:03:46.235+0800" } ``` ##### 3.2.14 获取某个Namespace当前生效的已发布配置接口 * **URL** : http://{portal_address}/openapi/v1/envs/{env}/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/releases/latest * **Method** : GET * **Request Params** :无 * **返回值Sample** : ``` json { "appId": "test-0620-01", "clusterName": "test", "namespaceName": "application", "name": "2016-08-11", "configurations": { "timeout": "3000", }, "comment": "修改timeout值", "dataChangeCreatedBy": "zhanglea", "dataChangeLastModifiedBy": "zhanglea", "dataChangeCreatedTime": "2016-08-11T14:03:46.232+0800", "dataChangeLastModifiedTime": "2016-08-11T14:03:46.235+0800" } ``` ##### 3.2.15 回滚已发布配置接口 * **URL** : http://{portal_address}/openapi/v1/envs/{env}/releases/{releaseId}/rollback * **Method** : PUT * **Request Params** : 参数名 | 必选 | 类型 | 说明 --- | --- | --- | --- operator | true | String | 删除配置的操作者,域账号 * **返回值** : 无 ##### 3.2.16 分页获取配置项接口 * **URL** : http://{portal_address}/openapi/v1/envs/{env}/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/items * **Method** : GET * **Version** : >= 2.1.0 * **Request Params** : 参数名 | 必选 | 类型 | 说明 --- |-------|-----| --- page | false | int | 页码,从 0 开始,默认为 0 size | false | int | 页大小,默认为 50 * **返回值Sample** : ``` json { "content": [ { "key": "timeout", "value": "3000", "comment": "超时时间", "dataChangeCreatedBy": "mghio", "dataChangeLastModifiedBy": "mghio", "dataChangeCreatedTime": "2022-07-17T21:37:41.818+0800", "dataChangeLastModifiedTime": "2022-07-17T21:37:41.818+0800" }, { "key": "page.size", "value": "200", "comment": "页大小", "dataChangeCreatedBy": "mghio", "dataChangeLastModifiedBy": "mghio", "dataChangeCreatedTime": "2022-07-17T21:37:41.818+0800", "dataChangeLastModifiedTime": "2022-07-17T21:37:41.818+0800" } ], "page": 0, "size": 50, "total": 2 } ``` ##### 3.2.17 创建App并获取管理员权限 可以通过此接口创建App, > 注意:需要在创建第三方应用时,**勾选允许创建app,否则会产生异常,HTTP状态码401** * **URL** : http://{portal_address}/openapi/v1/apps/ * **Method** : POST * **Request Params** :无 * **请求内容(Request Body, JSON格式)** : | 参数名 | 必选 | 类型 | 说明 | | ------------------- | ----- | -------- | ------------------------------------- | | assignAppRoleToSelf | true | Boolean | true:授予自己APP的管理权限 | | admins | false | String[] | 授予这些用户APP的管理权限 | | app | true | Object | APP的信息,字段参考下方的请求值Sample | * **请求值 Sample** : ```json { "assignAppRoleToSelf": true, "admins": [ "user1", "user2" ], "app": { "name": "appName1234", "appId": "xxx-web", "orgId": "development", "orgName": "产品研发部", "ownerName": "user3", "ownerEmail": "user3@test.com" } } ``` * **返回值 Sample** : 无返回值 ### 四、错误码说明 正常情况下,接口返回的Http状态码是200,下面列举了Apollo会返回的非200错误码说明。 #### 4.1 400 - Bad Request 客户端传入参数的错误,如操作人不存在,namespace不存在等等,客户端需要根据提示信息检查对应的参数是否正确。 #### 4.2 401 - Unauthorized 接口传入的token非法或者已过期,客户端需要检查token是否传入正确。 #### 4.3 403 - Forbidden 接口要访问的资源未得到授权,比如只授权了对A应用下Namespace的管理权限,但是却尝试管理B应用下的配置。 #### 4.4 404 - Not Found 接口要访问的资源不存在,一般是URL或URL的参数错误。 #### 4.5 405 - Method Not Allowed 接口访问的Method不正确,比如应该使用POST的接口使用了GET访问等,客户端需要检查接口访问方式是否正确。 #### 4.6 500 - Internal Server Error 其它类型的错误默认都会返回500,对这类错误如果应用无法根据提示信息找到原因的话,可以找Apollo研发团队一起排查问题。 ================================================ FILE: docs/zh/portal/apollo-user-guide.md ================================================ #   # 名词解释 * 普通应用 * 普通应用指的是独立运行的程序,如 * Web应用程序 * 带有main函数的程序 * 公共组件 * 公共组件指的是发布的类库、客户端程序,不会自己独立运行,如 * Java的jar包 * .Net的dll文件 # 一、普通应用接入指南 ## 1.1 创建项目 要使用Apollo,第一步需要创建项目。 1. 打开apollo-portal主页 2. 点击“创建项目” ![create-app-entry](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/create-app-entry.png) 3. 输入项目信息 * 部门:选择应用所在的部门 * 应用AppId:用来标识应用身份的唯一id,格式为string,需要和客户端app.properties中配置的app.id对应 * 应用名称:应用名,仅用于界面展示 * 应用负责人:选择的人默认会成为该项目的管理员,具备项目权限管理、集群创建、Namespace创建等权限 ![create-app](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/create-app.png) 4. 点击提交 创建成功后,会自动跳转到项目首页 ![app-created](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/app-created.png) ## 1.2 项目权限分配 ### 1.2.1 项目管理员权限 项目管理员拥有以下权限: 1. 可以管理项目的权限分配 2. 可以创建集群 3. 可以创建Namespace 创建项目时填写的应用负责人默认会成为项目的管理员之一,如果还需要其他人也成为项目管理员,可以按照下面步骤操作: 1. 点击页面左侧的“管理项目” * ![app-permission-entry](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/app-permission-entry.png) 2. 搜索需要添加的成员并点击添加 * ![app-permission-search-user](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/app-permission-search-user.png) * ![app-permission-user-added](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/app-permission-user-added.png) ### 1.2.2 配置编辑、发布权限 配置权限分为编辑和发布: * 编辑权限允许用户在Apollo界面上创建、修改、删除配置 * 配置修改后只在Apollo界面上变化,不会影响到应用实际使用的配置 * 发布权限允许用户在Apollo界面上发布、回滚配置 * 配置只有在发布、回滚动作后才会被应用实际使用到 * Apollo在用户操作发布、回滚动作后实时通知到应用,并使最新配置生效 项目创建完,默认没有分配配置的编辑和发布权限,需要项目管理员进行授权。 1. 点击application这个namespace的授权按钮 * ![namespace-permission-entry](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/namespace-permission-entry.png) 2. 分配修改权限 * ![namespace-permission-edit](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/namespace-permission-edit.png) 3. 分配发布权限 * ![namespace-publish-permission](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/namespace-publish-permission.png) ### 1.2.3 不同维度的权限管理 对于Apollo的配置权限,在初始设计时把权限绑定在了 namespace 上,因为 apollo 的权限管理本身是比较灵活的,所以可以在这个基础之上进行扩展。 基于 Apollo 的主要实体类的设计 [E-R Diagram](/docs/zh/design/apollo-design.md?id=_14-e-r-diagram), 我们可以认为 Namespace 是一个权限的最小单元,而 App 是一个权限的最大单元。 中间依次是 Env、Cluster,所以我们可以对不同维度的权限进行管理。 | App | Env | Cluster | Namespace | Model | Impl | | --- | --- | --- | --- | --- |------| | ☑️ | | | | App → * | 未实现 | | ☑️ | | | ☑️ | App → Namespace | 已实现 | | ☑️ | ☑️ | | | App + Env → * | 未实现 | | ☑️ | ☑️ | | ☑️ | App + Env → Namespace | 已实现 | | ☑️ | ☑️ | ☑️ | | App + Env + Cluster → * | 已实现 | | ☑️ | ☑️ | ☑️ | ☑️ | App + Env + Cluster → Namespace | 未实现 | 对不同权限模型的释义: | Model | Target | PermissionType (e.g. Modify) | TargetId | | --- | --- | --- | --- | | App → * | App的所有namespace | | | | App → Namespace | App下的所有指定名字的namespace | ModifyNamespace | App+Namespace | | App + Env → * | App的env下所有namespace | | | | App + Env → Namespace | App的env下所有指定名字的namespace | ModifyNamespace | App+Namespace+Env | | App + Env + Cluster → * | App的env中cluster的所有namespace | ModifyNamespaceInCluster | App+Env+ClusterName | | App + Env + Cluster → Namespace | App的env中cluster下指定名字的namespace | | | #### 1.2.3.1 App下的所有指定名字的namespace 1. 点击application这个namespace的授权按钮 * ![ns-permission-app-allns-entry](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/ns-permission-app-allns-entry.png) 2. 选择“所有环境” * ![ns-permission-app-allns-select](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/ns-permission-app-allns-select.png) 3. 分配修改权限 * ![namespace-permission-edit](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/namespace-permission-edit.png) 4. 分配发布权限 * ![namespace-publish-permission](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/namespace-publish-permission.png) #### 1.2.3.2 App的env下所有指定名字的namespace 1. 点击application这个namespace的授权按钮 * ![ns-permission-app-allns-entry](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/ns-permission-app-allns-entry.png) 2. 选择环境 * ![ns-permission-app-env-ns-select](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/ns-permission-app-env-ns-select.png) 3. 分配修改权限 * ![namespace-permission-edit](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/namespace-permission-edit.png) 4. 分配发布权限 * ![namespace-publish-permission](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/namespace-publish-permission.png) #### 1.2.3.3 App的env中cluster的所有namespace 1. 点击“管理集群”进取管理集群页面 * ![manage-cluster-entry](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/manage-cluster-entry.png) 2. 点击想管理的Cluster的授权按钮 * ![ns-permission-app-env-cluster-entry](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/ns-permission-app-env-cluster-entry.png) 3. 编辑权限 * ![ns-permission-app-env-cluster-edit](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/ns-permission-app-env-cluster-edit.png) ## 1.3 添加配置项 编辑配置需要拥有这个Namespace的编辑权限,如果发现没有新增配置按钮,可以找项目管理员授权。 ### 1.3.1 通过表格模式添加配置 1. 点击新增配置 * ![create-item-entry](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/create-item-entry.png) 2. 输入配置项 * ![create-item-detail](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/create-item-detail.png) 3. 点击提交 * ![item-created](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/item-created.png) ### 1.3.2 通过文本模式编辑 Apollo除了支持表格模式,逐个添加、修改配置外,还提供文本模式批量添加、修改。 这个对于从已有的properties文件迁移尤其有用。 1. 切换到文本编辑模式 ![text-mode-config-overview](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/text-mode-config-overview.png) 2. 点击右侧的修改配置按钮 ![text-mode-config-entry](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/text-mode-config-entry.png) 3. 输入配置项,并点击提交修改 ![text-mode-config-submit](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/text-mode-config-submit.png) ## 1.4 发布配置 配置只有在发布后才会真的被应用使用到,所以在编辑完配置后,需要发布配置。 发布配置需要拥有这个Namespace的发布权限,如果发现没有发布按钮,可以找项目管理员授权。 1. 点击“发布按钮” ![publish-entry](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/hermes-portal-publish-entry.png) 2. 填写发布相关信息,点击发布 ![publish-detail](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/hermes-portal-publish-detail.png) ## 1.5 应用读取配置 配置发布成功后,应用就可以通过Apollo客户端读取到配置了。 Apollo目前提供Java客户端,具体信息请点击[Java客户端使用文档](zh/client/java-sdk-user-guide): 如果应用使用了其它语言,也可以通过直接访问Http接口获取配置,具体可以参考[其它语言客户端接入指南](zh/client/other-language-client-user-guide) ## 1.6 回滚已发布配置 如果发现已发布的配置有问题,可以通过点击『回滚』按钮来将客户端读取到的配置回滚到上一个发布版本。 这里的回滚机制类似于发布系统,发布系统中的回滚操作是将部署到机器上的安装包回滚到上一个部署的版本,但代码仓库中的代码是不会回滚的,从而开发可以在修复代码后重新发布。 Apollo中的回滚也是类似的机制,点击回滚后是将发布到客户端的配置回滚到上一个已发布版本,也就是说客户端读取到的配置会恢复到上一个版本,但页面上编辑状态的配置是不会回滚的,从而开发可以在修复配置后重新发布。 ## 1.7 配置查询(管理员权限) 在配置添加或修改后,管理员用户可以通过进入 `管理员工具 - Value的全局搜索` 页面,来对配置项进行所属查询以及跳转修改。 这里的查询为模糊检索,通过对配置项的key与value至少一项进行检索,找到该配置在哪个应用、环境、集群、命名空间中被使用。 - properties格式配置可以直接通过对key与value进行检索 ![Configuration query-properties](../images/Configuration query-properties.png) - xml、json、yml、yaml、txt等格式配置,由于存储时以content-value进行存储,故可以通过key=content、value=配置项内容,进行检索 ![Configuration query-Non properties](../images/Configuration query-Non properties.png) # 二、公共组件接入指南 ## 2.1 公共组件和普通应用的区别 公共组件是指那些发布给其它应用使用的客户端代码,比如CAT客户端、Hermes Producer客户端等。 虽然这类组件是由其他团队开发、维护,但是运行时是在业务实际应用内的,所以本质上可以认为是应用的一部分。 通常情况下,这类组件所用到的配置由原始开发团队维护,不过由于实际应用的运行时、环境各不一样,所以我们也允许应用在实际使用时能够覆盖公共组件的部分配置。 ## 2.2 公共组件接入步骤 公共组件的接入步骤,和普通应用几乎一致,唯一的区别是公共组件需要创建自己唯一的Namespace。 所以,首先执行普通应用接入文档中的以下几个步骤,然后再按照本章节后面的步骤操作。 1. [创建项目](#_11-%E5%88%9B%E5%BB%BA%E9%A1%B9%E7%9B%AE) 2. [项目管理员权限](#_121-%E9%A1%B9%E7%9B%AE%E7%AE%A1%E7%90%86%E5%91%98%E6%9D%83%E9%99%90) ### 2.2.1 创建Namespace 创建Namespace需要项目管理员权限,如果发现没有添加Namespace按钮,可以找项目管理员授权。 1. 点击页面左侧的添加Namespace * ![create-namespace](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/create-namespace.png) 2. 点击“创建新的Namespace” * ![create-namespace-select-type](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/create-namespace-select-type.png) 3. 输入公共组件的Namespace名称,需要注意的是Namespace名称全局唯一 * Apollo会默认把部门代号添加在最前面 * ![create-namespace-detail](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/create-namespace-detail.png) 4. 点击提交后,页面会自动跳转到关联Namespace页面 * 首先,选中所有需要有这个Namespace的环境和集群,一般建议全选 * 其次,选中刚刚创建的namespace * 最后,点击提交 * ![link-namespace-detail](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/link-namespace-detail.png) 5. 关联成功后,页面会自动跳转到Namespace权限管理页面 1. 分配修改权限 * ![namespace-permission-edit](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/namespace-permission-edit.png) 2. 分配发布权限 * ![namespace-publish-permission](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/namespace-publish-permission.png) 6. 点击“返回”回到项目页面 ### 2.2.2 添加配置项 编辑配置需要拥有这个Namespace的编辑权限,如果发现没有新增配置按钮,可以找项目管理员授权。 #### 2.2.2.1 通过表格模式添加配置 1. 点击新增配置 ![public-namespace-edit-item-entry](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/public-namespace-edit-item-entry.png) 2. 输入配置项 ![public-namespace-edit-item](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/public-namespace-edit-item.png) 3. 点击提交 ![public-namespace-item-created](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/public-namespace-item-created.png) #### 2.2.2.2 通过文本模式编辑 这部分和普通应用一致,具体步骤请参见[1.3.2 通过文本模式编辑](#_132-%E9%80%9A%E8%BF%87%E6%96%87%E6%9C%AC%E6%A8%A1%E5%BC%8F%E7%BC%96%E8%BE%91)。 ### 2.2.3 发布配置 配置只有在发布后才会真的被应用使用到,所以在编辑完配置后,需要发布配置。 发布配置需要拥有这个Namespace的发布权限,如果发现没有发布按钮,可以找项目管理员授权。 1. 点击“发布按钮” ![public-namespace-publish-items-entry](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/public-namespace-publish-items-entry.png) 2. 填写发布相关信息,点击发布 ![public-namespace-publish-items](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/public-namespace-publish-items.png) ### 2.2.4 应用读取配置 配置发布成功后,应用就可以通过Apollo客户端读取到配置了。 Apollo目前提供Java客户端,具体信息请点击[Java客户端使用文档](zh/client/java-sdk-user-guide): 如果应用使用了其它语言,也可以通过直接访问Http接口获取配置,具体可以参考[其它语言客户端接入指南](zh/client/other-language-client-user-guide) 对于公共组件的配置读取,可以参考上述文档中的“获取公共Namespace的配置”部分。 ## 2.3 应用覆盖公用组件配置步骤 前面提到,通常情况下,公共组件所用到的配置由原始开发团队维护,不过由于实际应用的运行时、环境各不一样,所以我们也允许应用在实际使用时能够覆盖公共组件的部分配置。 这里就讲一下应用如何覆盖公用组件的配置,简单起见,假设apollo-portal应用使用了hermes producer客户端,并且希望调整hermes的批量发送大小。 ### 2.3.1 关联公共组件Namespace 1. 进入使用公共组件的应用项目首页,点击左侧的添加Namespace按钮 * 所以,在这个例子中,我们需要进入apollo-portal的首页。 * (添加Namespace需要项目管理员权限,如果发现没有添加Namespace按钮,可以找项目管理员授权) * ![link-public-namespace-entry](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/link-public-namespace-entry.png) 2. 找到hermes producer的namespace,并选择需要关联到哪些环境和集群 ![link-public-namespace](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/link-public-namespace.png) 3. 关联成功后,页面会自动跳转到Namespace权限管理页面 1. 分配修改权限 ![namespace-permission-edit](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/namespace-permission-edit.png) 2. 分配发布权限 ![namespace-publish-permission](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/namespace-publish-permission.png) 4. 点击“返回”回到项目页面 ### 2.3.2 覆盖公用组件配置 1. 点击新增配置 ![override-public-namespace-entry](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/override-public-namespace-entry.png) 2. 输入要覆盖的配置项 ![override-public-namespace-item](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/override-public-namespace-item.png) 3. 点击提交 ![override-public-namespace-item-done](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/override-public-namespace-item-done.png) ### 2.3.3 发布配置 配置只有在发布后才会真的被应用使用到,所以在编辑完配置后,需要发布配置。 发布配置需要拥有这个Namespace的发布权限,如果发现没有发布按钮,可以找项目管理员授权。 1. 点击“发布按钮” ![override-public-namespace-item-publish-entry](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/override-public-namespace-item-publish-entry.png) 2. 填写发布相关信息,点击发布 ![override-public-namespace-item-publish](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/override-public-namespace-item-publish.png) 3. 配置发布成功后,hermes producer客户端在apollo-portal应用里面运行时读取到的sender.batchSize的值就是1000。 # 三、集群独立配置说明 在有些特殊情况下,应用有需求对不同的集群做不同的配置,比如部署在A机房的应用连接的es服务器地址和部署在B机房的应用连接的es服务器地址不一样。 在这种情况下,可以通过在Apollo创建不同的集群来解决。 ## 3.1 创建集群 创建集群需要项目管理员权限,如果发现没有添加集群按钮,可以找项目管理员授权。 1. 点击页面左侧的“添加集群”按钮 * ![create-cluster](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/create-cluster.png) 2. 输入集群名称,选择环境并提交 * ![create-cluster-detail](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/create-cluster-detail.png) 3. 切换到对应的集群,修改配置并发布即可 * ![config-in-cluster-created](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/cluster-created.png) 4. 通过上述配置,部署在SHAJQ机房的应用就会读到SHAJQ集群下的配置 5. 如果应用还在其它机房部署了应用,那么在上述的配置下,会读到default集群下的配置。 # 四、多个AppId使用同一份配置 在一些情况下,尽管应用本身不是公共组件,但还是需要在多个AppId之间共用同一份配置,比如同一个产品的不同项目:XX-Web, XX-Service, XX-Job等。 这种情况下如果希望实现多个AppId使用同一份配置的话,基本概念和公共组件的配置是一致的。 具体来说,就是在其中一个AppId下创建一个namespace,写入公共的配置信息,然后在各个项目中读取该namespace的配置即可。 如果某个AppId需要覆盖公共的配置信息,那么在该AppId下关联公共的namespace并写入需要覆盖的配置即可。 具体步骤可以参考[公共组件接入指南](#%e4%ba%8c%e3%80%81%e5%85%ac%e5%85%b1%e7%bb%84%e4%bb%b6%e6%8e%a5%e5%85%a5%e6%8c%87%e5%8d%97)。 # 五、灰度发布使用指南 通过灰度发布功能,可以实现: 1. 对于一些对程序有比较大影响的配置,可以先在一个或者多个实例生效,观察一段时间没问题后再全量发布配置。 2. 对于一些需要调优的配置参数,可以通过灰度发布功能来实现A/B测试。可以在不同的机器上应用不同的配置,不断调整、测评一段时间后找出较优的配置再全量发布配置。 下面将结合一个实际例子来描述如何使用灰度发布功能。 ## 5.1 场景介绍 100004458(apollo-demo)项目有两个客户端: 1. 10.32.21.19 2. 10.32.21.22 ![initial-instance-list](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/gray-release/initial-instance-list.png) **灰度目标:** * 当前有一个配置timeout=2000,我们希望对10.32.21.22灰度发布timeout=3000,对10.32.21.19仍然是timeout=2000。 ![initial-config](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/gray-release/initial-config.png) ## 5.2 创建灰度 首先点击application namespace右上角的`创建灰度`按钮。 ![create-gray-release](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/gray-release/create-gray-release.png) 点击确定后,灰度版本就创建成功了,页面会自动切换到`灰度版本`Tab。 ![initial-gray-release-tab](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/gray-release/initial-gray-release-tab.png) ## 5.3 灰度配置 点击`主版本的配置`中,timeout配置最右侧的`对此配置灰度`按钮 ![initial-gray-release-tab](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/gray-release/edit-gray-release-config.png) 在弹出框中填入要灰度的值:3000,点击提交。 ![submit-gray-release-config](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/gray-release/submit-gray-release-config.png) ![gray-release-config-submitted](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/gray-release/gray-release-config-submitted.png) 灰度配置完成后,确认灰度的配置和主版本配置 点击`灰度发布`按钮 ![click-gray-release](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/gray-release/click-gray-release.png) 与主版本的值和灰度版本已发布的值对比,确认将要发布的灰度配置 ![gray-release-diff-items](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/gray-release/gray-release-diff-items.png) ## 5.4 配置灰度规则 切换到`灰度规则`Tab,点击`新增规则`按钮 ![new-gray-release-rule](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/gray-release/new-gray-release-rule.png) 在弹出框中`灰度的IP`下拉框会默认展示当前使用配置的机器列表,选择我们要灰度的IP。 ![select-gray-release-ip](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/gray-release/select-gray-release-ip.png) ![gray-release-ip-selected](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/gray-release/gray-release-ip-selected.png) 除了IP维度以外,从2.0.0版本开始还支持通过label来标识灰度的实例列表,适用于IP不固定的场景如`Kubernetes`。 手动输入想要设置的label标签,输入完成后点击点击添加按钮。 ![manual-input-gray-release-label](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/gray-release/manual-input-gray-release-label.png) ![manual-input-gray-release-label-2](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/gray-release/manual-input-gray-release-label2.png) ![gray-release-rule-saved](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/gray-release/gray-release-rule-saved.png) 上述规则配置后,灰度配置会对AppId为`100004458`,IP为`10.32.21.22`或者`Label`标记为`myLabel`或`appLabel`的实例生效。 > 关于`Label`如何标记,可以参考[ApolloLabel](zh/client/java-sdk-user-guide?id=_1247-apollolabel)的配置说明。 如果下拉框中没找到需要的IP,说明机器还没从Apollo取过配置,可以点击手动输入IP来输入,输入完后点击添加按钮 ![manual-input-gray-release-ip](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/gray-release/manual-input-gray-release-ip.png) ![manual-input-gray-release-ip-2](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/gray-release/manual-input-gray-release-ip-2.png) >注:对于公共Namespace的灰度规则,需要先指定要灰度的appId,然后再选择IP和Label。 ## 5.5 灰度发布 配置规则已经生效,不过灰度配置还没有发布。切换到`配置`Tab。 再次检查灰度的配置部分,如果没有问题,点击`灰度发布`。 ![prepare-to-do-gray-release](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/gray-release/prepare-to-do-gray-release.png) 在弹出框中可以看到主版本的值是2000,灰度版本即将发布的值是3000。填入其它信息后,点击发布。 ![gray-release-confirm-dialog](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/gray-release/gray-release-confirm-dialog.png) 发布后,切换到`灰度实例列表`Tab,就能看到10.32.21.22已经使用了灰度发布的值。 ![gray-release-instance-list](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/gray-release/gray-release-instance-list.png) 切换到`主版本`的`实例列表`,会看到主版本配置只有10.32.21.19在使用了。 ![master-branch-instance-list](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/gray-release/master-branch-instance-list.png) 后面可以继续配置的修改或规则的更改。配置的修改需要点击灰度发布后才会生效,规则的修改在规则点击完成后就会实时生效。 ## 5.6 全量发布 如果灰度的配置测试下来比较理想,符合预期,那么就可以操作`全量发布`。 全量发布的效果是: 1. 灰度版本的配置会合并回主版本,在这个例子中,就是主版本的timeout会被更新成3000 2. 主版本的配置会自动进行一次发布 3. 在全量发布页面,可以选择是否保留当前灰度版本,默认为不保留。 ![prepare-to-full-release](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/gray-release/prepare-to-full-release.png) ![full-release-confirm-dialog](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/gray-release/full-release-confirm-dialog.png) ![full-release-confirm-dialog-2](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/gray-release/full-release-confirm-dialog-2.png) 我选择了不保留灰度版本,所以发布完的效果就是主版本的配置更新、灰度版本删除。点击主版本的实例列表,可以看到10.32.21.22和10.32.21.19都使用了主版本最新的配置。 ![master-branch-instance-list-after-full-release](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/gray-release/master-branch-instance-list-after-full-release.png) ## 5.7 放弃灰度 如果灰度版本不理想或者不需要了,可以点击`放弃灰度`。 ![abandon-gray-release](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/gray-release/abandon-gray-release.png) ## 5.8 发布历史 点击主版本的`发布历史`按钮,可以看到当前namespace的主版本以及灰度版本的发布历史。 ![view-release-history](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/gray-release/view-release-history.png) ![view-release-history-detail](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/gray-release/view-release-history-detail.png) # 六、其它功能配置 ## 6.1 配置查看权限 从1.1.0版本开始,apollo-portal增加了查看权限的支持,可以支持配置某个环境只允许项目成员查看私有Namespace的配置。 这里的项目成员是指: 1. 项目的管理员 2. 具备该私有Namespace在该环境下的修改或发布权限 配置方式很简单,用超级管理员账号登录后,进入`管理员工具 - 系统参数`页面新增或修改`configView.memberOnly.envs`配置项即可。 ![configView.memberOnly.envs](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/configure-view-permissions.png) ## 6.2 配置访问密钥 Apollo从1.6.0版本开始增加访问密钥机制,从而只有经过身份验证的客户端才能访问敏感配置。如果应用开启了访问密钥,客户端需要配置密钥,否则无法获取配置。 1. 项目管理员打开管理密钥页面 ![管理密钥入口](https://user-images.githubusercontent.com/837658/94990081-f4d3cd80-05ab-11eb-9470-fed5ec6de92e.png) 2. 为项目的每个环境生成访问密钥,注意默认是禁用的,建议在客户端都配置完成后再开启 ![密钥配置页面](https://user-images.githubusercontent.com/837658/94990150-788dba00-05ac-11eb-9a12-727fdb872e42.png) 3. 客户端侧[配置访问密钥](zh/client/java-sdk-user-guide#_1244-配置访问密钥) ## 6.3 全局搜索配置项的系统参数设置 从2.4.0版本开始,apollo-portal增加了全局搜索配置项的功能,通过对配置项的key与value进行的模糊检索,找到拥有对应值的配置项在哪个应用、环境、集群、命名空间中被使用。为了防止在进行配置项的全局视角搜索时出现内存溢出(OOM)的问题,我们引入了一个系统参数`apollo.portal.search.perEnvMaxResults`。这个参数用于限制每个环境配置项单次最大搜索结果的数量。默认情况下,这个值被设置为`200`,但管理员可以根据实际需求进行调整。 **设置方法:** 1. 用超级管理员账号登录到Apollo配置中心的界面 2. 进入`管理员工具 - 系统参数`页面新增或修改`apollo.portal.search.perEnvMaxResults`配置项即可 请注意,修改系统参数可能会影响搜索功能的性能,因此在修改之前应该进行充分的测试,并确保理解参数的具体作用。 ![System-parameterization-of-global-search-configuration-items](../images/System-parameterization-of-global-search-configuration-items.png) ## 6.4 appld+cluster维度下命名空间数量限制功能参数设置 从2.4.0版本开始,apollo-portal提供了appld+cluster维度下可以创建的命名空间数量上限校验的功能,此功能默认关闭,需要配置系统 `namespace.num.limit.enabled` 开启,同时提供了系统参数`namespace.num.limit`来动态配置appld+cluster维度下的Namespace数量上限值,默认为200个,考虑到一些基础组件如网关、消息队列、Redis、数据库等需要特殊处理,新增了系统参数`namespace.num.limit.white` 来配置校验白名单,不受Namespace数量上限的影响 **设置方法:** 1. 用超级管理员账号登录到Apollo配置中心的界面 2. 进入 `管理员工具 - 系统参数 - ConfigDB 配置管理` 页面新增或修改 `namespace.num.limit.enabled` 配置项为true/false 即可开启/关闭此功能,默认关闭 ![item-num-limit-enabled](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/namespace-num-limit-enabled.png) 3. 进入 `管理员工具 - 系统参数 - ConfigDB 配置管理` 页面新增或修改 `namespace.num.limit` 配置项来配置单个appld+cluster下的namespace数量上限值,默认为200 ![item-num-limit](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/namespace-num-limit.png) 4. 进入 `管理员工具 - 系统参数 - ConfigDB 配置管理` 页面新增或修改 `namespace.num.limit.white` 配置项来配置namespace数量上限校验的白名单,多个AppId使用英文逗号分隔 ![item-num-limit](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/namespace-num-limit-white.png) ## 6.5 单个命名空间下的配置项数量限制 从2.4.0版本开始,apollo-portal提供了限制单个命名空间下的配置项数量的功能,此功能默认关闭,需要配置系统 `item.num.limit.enabled` 开启,同时提供了系统参数`item.num.limit`来动态配置单个Namespace下的item数量上限值 **设置方法:** 1. 用超级管理员账号登录到Apollo配置中心的界面 2. 进入`管理员工具 - 系统参数 - ConfigDB 配置管理`页面新增或修改`item.num.limit.enabled`配置项为true/false 即可开启/关闭此功能 ![item-num-limit-enabled](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/item-num-limit-enabled.png) 3. 进入`管理员工具 - 系统参数 - ConfigDB 配置管理`页面新增或修改`item.num.limit`配置项来配置单个Namespace下的item数量上限值 ![item-num-limit](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/item-num-limit.png) # 七、最佳实践 ## 7.1 安全相关 配置中心作为基础服务,存储着公司非常重要的配置信息,所以安全因素需要大家重点关注,下面列举了一些注意事项供大家参考,也欢迎大家分享自己的实践案例。 ### 7.1.1 认证 建议接入公司统一的身份认证系统,如 SSO、LDAP 等,接入方式可以参考[Portal 实现用户登录功能](zh/extension/portal-how-to-implement-user-login-function) > 如果使用Apollo提供的Spring Security简单认证,务必记得要修改超级管理员apollo的密码 ### 7.1.2 授权 Apollo 支持细粒度的权限控制,请务必根据实际情况做好权限控制: 1. [项目管理员权限](#_121-项目管理员权限) * Apollo 默认允许所有登录用户创建项目,如果只允许部分用户创建项目,可以开启[创建项目权限控制](zh/deployment/distributed-deployment-guide?id=_3110-rolecreate-applicationenabled-是否开启创建项目权限控制) 2. [配置编辑、发布权限](#_122-配置编辑、发布权限) * 配置编辑、发布权限支持按环境配置,比如开发环境开发人员可以自行完成配置编辑和发布的过程,但是生产环境发布权限交由测试或运维人员 * 生产环境建议同时开启[发布审核](zh/deployment/distributed-deployment-guide?id=_322-namespacelockswitch-一次发布只能有一个人修改开关,用于发布审核),从而控制一次配置发布只能由一个人修改,另一个人发布,确保配置修改得到充分检查 3. [配置查看权限](#_61-配置查看权限) * 可以指定某个环境只允许项目成员查看私有Namespace的配置,从而避免敏感配置泄露,如生产环境 ### 7.1.3 系统访问 除了用户权限,在系统访问上也需要加以考虑: 1. `apollo-configservice`和`apollo-adminservice`是基于内网可信网络设计的,所以出于安全考虑,禁止`apollo-configservice`和`apollo-adminservice`直接暴露在公网 2. 对敏感配置可以考虑开启[访问秘钥](#_62-%e9%85%8d%e7%bd%ae%e8%ae%bf%e9%97%ae%e5%af%86%e9%92%a5),从而只有经过身份验证的客户端才能访问敏感配置 3. 1.7.1及以上版本可以考虑为`apollo-adminservice`开启[访问控制](zh/deployment/distributed-deployment-guide?id=_326-admin-serviceaccesscontrolenabled-配置apollo-adminservice是否开启访问控制),从而只有[受控的](zh/deployment/distributed-deployment-guide?id=_3112-admin-serviceaccesstokens-设置apollo-portal访问各环境apollo-adminservice所需的access-token)`apollo-portal`才能访问对应接口,增强安全性 4. 2.1.0及以上版本可以考虑为`eureka`开启[访问控制](zh/deployment/distributed-deployment-guide?id=_329-apolloeurekaserversecurityenabled-配置是否开启eureka-server的登录认证),从而只有受控的`apollo-configservice`和`apollo-adminservice`可以注册到`eureka`,增强安全性 ================================================ FILE: docs/zh/portal/apollo-user-practices.md ================================================ Apollo 配置中心的实践案例,供大家参考: * [Apollo+ES源码改造,构建民生银行的ELK日志平台配置管理中心](https://mp.weixin.qq.com/s/VHugn0vgNu4m56V49geC4w) * [Apollo在有赞的实践](https://mp.weixin.qq.com/s/Ge14UeY9Gm2Hrk--E47eJQ) * [微服务版本切换初始设计思路](https://blog.llyweb.com/articles/2020/08/11/1597149013480.html) * [Alibaba Sentinel Push模式 规则推送至Apollo配置中心](https://anilople.github.io/Sentinel) ================================================ FILE: e2e/README.md ================================================ # Apollo E2E Tests This directory contains end-to-end (E2E) UI tests and related CI entrypoints. ## Test Suites ### Portal UI E2E - Location: `e2e/portal-e2e` - Runtime: Playwright + Chromium - Tags: - `@smoke`: core user journeys - `@regression`: extended scenarios - CI runs both tags together. Current cases: 1. `login flow works @smoke` 2. `create app flow works @smoke` 3. `create item and first release works @smoke` 4. `update item and second release works @smoke` 5. `rollback latest release works @smoke` 6. `release history contains publish and rollback records @smoke` 7. `duplicate app creation is rejected @regression` 8. `cluster and namespace pages support creation flow @regression` 9. `config export and instance view paths are reachable @regression` 10. `published, gray published and rolled back configs are readable from config service @regression` 11. `properties, yaml and json namespaces are readable from config service @regression` 12. `namespace role page supports grant and revoke operations @regression` 13. `text mode edit and publish are readable from config service @regression` 14. `linked public namespace supports association and override @regression` 15. `grayscale ui supports create rule publish merge and discard @regression` High-priority user-guide coverage (via `portal-priority.spec.js`): 1. Namespace permission management (grant/revoke role in namespace role page). 2. Text-mode editing path (switch to text, submit edits, publish, verify from Config Service). 3. Public namespace association and override in linked namespace. 4. Grayscale UI chain: create branch, maintain rules, gray publish, merge-to-master publish, and discard branch. Config Service full-chain coverage (via `portal-configservice.spec.js`): Covered controllers: 1. `ConfigController` (`/configs/**`) 2. `ConfigFileController` (`/configfiles/**`) 3. `NotificationControllerV2` (`/notifications/v2`) Covered behaviors: 1. Normal publish result is readable from Config Service. 2. Gray release result is readable and isolated by client IP. 3. Rollback result is readable from Config Service after rollback. 4. Namespace formats `properties`, `yaml`, and `json` are all verifiable via Config Service APIs. 5. Notification polling returns namespace updates with increasing notification IDs after publish/gray/rollback. ### Portal Auth Matrix E2E - Location: `e2e/portal-e2e` - Runtime: Playwright + Chromium + Dockerized auth providers - Tag: - `@auth-matrix`: auth matrix scenarios (runs separately from `@smoke|@regression`) - Modes: - `ldap`: OpenLDAP + group filter (`memberUid`) login checks - `oidc`: Keycloak OIDC interactive login checks Covered behaviors: 1. Login success in the target auth mode. 2. Login failures: - non-existent user - wrong password - LDAP-only: blocked user rejected by group filter 3. Post-login user search for app creation: - user can be searched from auth provider-backed user source - selected option contains user id/name/email information 4. Namespace role page grant/revoke: - assign role after real Select2 user search - revoke assigned role successfully Notes: - LDAP e2e config uses `camelCase` keys under `ldap.mapping` and `ldap.group` (for example `objectClass`, `loginId`, `groupSearch`) to match the runtime placeholders. - OIDC e2e fixture pre-creates users with `firstName`, `lastName`, and `emailVerified=true` to avoid Keycloak `VERIFY_PROFILE` redirect during first login. - OIDC secondary user is warmed up once before assertions so Apollo local user search can find it (`OidcLocalUserService` behavior). ## Local Run ```bash cd e2e/portal-e2e npm ci npx playwright install --with-deps chromium BASE_URL=http://127.0.0.1:8070 npm run test:e2e ``` CI mode command: ```bash cd e2e/portal-e2e BASE_URL=http://127.0.0.1:8070 npm run test:e2e:ci ``` Run only Config Service full-chain regression: ```bash cd e2e/portal-e2e BASE_URL=http://127.0.0.1:8070 npm run test:e2e:ci -- tests/portal-configservice.spec.js ``` Run Portal auth matrix in LDAP mode: ```bash cd /path/to/apollo ./e2e/portal-e2e/scripts/auth/setup-ldap.sh SPRING_PROFILES_ACTIVE=github,database-discovery,ldap \ SPRING_SQL_CONFIG_INIT_MODE=always \ SPRING_SQL_PORTAL_INIT_MODE=always \ SPRING_CONFIG_DATASOURCE_URL="jdbc:h2:mem:apollo-config-db;mode=mysql;DB_CLOSE_ON_EXIT=FALSE;DB_CLOSE_DELAY=-1;BUILTIN_ALIAS_OVERRIDE=TRUE;DATABASE_TO_UPPER=FALSE" \ SPRING_PORTAL_DATASOURCE_URL="jdbc:h2:mem:apollo-portal-db;mode=mysql;DB_CLOSE_ON_EXIT=FALSE;DB_CLOSE_DELAY=-1;BUILTIN_ALIAS_OVERRIDE=TRUE;DATABASE_TO_UPPER=FALSE" \ SPRING_CONFIG_ADDITIONAL_LOCATION="file:/path/to/apollo/e2e/portal-e2e/config/application-ldap-e2e.yml" \ java -jar /path/to/apollo/apollo-assembly/target/apollo-assembly-*.jar cd e2e/portal-e2e npm ci npx playwright install --with-deps chromium PORTAL_AUTH_MODE=ldap BASE_URL=http://127.0.0.1:8070 npm run test:e2e:auth-matrix cd /path/to/apollo ./e2e/portal-e2e/scripts/auth/teardown-auth.sh ``` Run Portal auth matrix in OIDC mode: ```bash cd /path/to/apollo ./e2e/portal-e2e/scripts/auth/setup-oidc.sh SPRING_PROFILES_ACTIVE=github,database-discovery,oidc \ SPRING_SQL_CONFIG_INIT_MODE=always \ SPRING_SQL_PORTAL_INIT_MODE=always \ SPRING_CONFIG_DATASOURCE_URL="jdbc:h2:mem:apollo-config-db;mode=mysql;DB_CLOSE_ON_EXIT=FALSE;DB_CLOSE_DELAY=-1;BUILTIN_ALIAS_OVERRIDE=TRUE;DATABASE_TO_UPPER=FALSE" \ SPRING_PORTAL_DATASOURCE_URL="jdbc:h2:mem:apollo-portal-db;mode=mysql;DB_CLOSE_ON_EXIT=FALSE;DB_CLOSE_DELAY=-1;BUILTIN_ALIAS_OVERRIDE=TRUE;DATABASE_TO_UPPER=FALSE" \ SPRING_CONFIG_ADDITIONAL_LOCATION="file:/path/to/apollo/e2e/portal-e2e/config/application-oidc-e2e.yml" \ java -jar /path/to/apollo/apollo-assembly/target/apollo-assembly-*.jar cd e2e/portal-e2e npm ci npx playwright install --with-deps chromium PORTAL_AUTH_MODE=oidc BASE_URL=http://127.0.0.1:8070 npm run test:e2e:auth-matrix cd /path/to/apollo ./e2e/portal-e2e/scripts/auth/teardown-auth.sh ``` ## CI Workflow - Workflow file: `.github/workflows/portal-ui-e2e.yml` - Job/check name: `portal-ui-e2e` - PR trigger paths: - `apollo-portal/**` - `apollo-assembly/**` - `e2e/portal-e2e/**` - `scripts/sql/**` - `.github/workflows/portal-ui-e2e.yml` Portal auth matrix workflow: - Workflow file: `.github/workflows/portal-login-e2e.yml` - Job/check name: `portal-login-e2e (ldap|oidc)` - Matrix: - `ldap` - `oidc` - PR trigger paths: - `apollo-portal/**` - `apollo-assembly/**` - `e2e/portal-e2e/**` - `.github/workflows/portal-login-e2e.yml` ## Maintenance Notes - Prefer stable selectors (`id`, stable attributes, deterministic CSS) over UI text. - Test data should use unique app ids to avoid collisions. - Keep assertions focused on behavior: URL transition, API response status, and visible success/failure signals. ================================================ FILE: e2e/portal-e2e/.gitignore ================================================ node_modules/ playwright-report/ test-results/ ================================================ FILE: e2e/portal-e2e/config/application-ldap-e2e.yml ================================================ # # Copyright 2026 Apollo Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # spring: ldap: base: "dc=example,dc=org" username: "cn=admin,dc=example,dc=org" password: "admin" searchFilter: "(uid={0})" urls: - "${LDAP_URL:ldap://127.0.0.1:3389}" ldap: mapping: objectClass: "inetOrgPerson" loginId: "uid" rdnKey: "uid" userDisplayName: "cn" email: "mail" group: groupBase: "ou=group" groupSearch: "(&(cn=dev))" groupMembership: "memberUid" ================================================ FILE: e2e/portal-e2e/config/application-oidc-e2e.yml ================================================ # # Copyright 2026 Apollo Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # server: forward-headers-strategy: framework spring: security: oauth2: client: provider: keycloak: issuer-uri: "${OIDC_ISSUER_URI:http://127.0.0.1:9080/realms/apollo}" registration: keycloak: authorization-grant-type: authorization_code client-authentication-method: client_secret_basic client-id: "${OIDC_CLIENT_ID:apollo-portal}" provider: keycloak scope: - openid - profile - email client-secret: "${OIDC_CLIENT_SECRET:apollo-secret}" resourceserver: jwt: issuer-uri: "${OIDC_ISSUER_URI:http://127.0.0.1:9080/realms/apollo}" ================================================ FILE: e2e/portal-e2e/package.json ================================================ { "name": "apollo-portal-e2e", "private": true, "version": "1.0.0", "scripts": { "test:e2e": "playwright test --project=chromium --grep '@smoke|@regression'", "test:e2e:ci": "playwright test --project=chromium --grep '@smoke|@regression' --reporter=line,html", "test:e2e:auth-matrix": "playwright test --project=chromium --grep '@auth-matrix'", "test:e2e:auth-matrix:ci": "playwright test --project=chromium --grep '@auth-matrix' --reporter=line,html" }, "devDependencies": { "@playwright/test": "^1.54.2" } } ================================================ FILE: e2e/portal-e2e/playwright.config.js ================================================ /* * Copyright 2026 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ const { defineConfig, devices } = require('@playwright/test'); const retries = Number(process.env.PLAYWRIGHT_RETRIES || '0'); const workers = Number(process.env.PLAYWRIGHT_WORKERS || (process.env.CI ? '2' : '1')); module.exports = defineConfig({ testDir: './tests', workers, retries, timeout: 180000, expect: { timeout: 20000, }, reporter: process.env.CI ? [['line'], ['html', { open: 'never' }]] : [['list']], use: { baseURL: process.env.BASE_URL || 'http://127.0.0.1:8070', headless: true, trace: 'retain-on-failure', screenshot: 'only-on-failure', video: 'retain-on-failure', }, projects: [ { name: 'chromium', use: { ...devices['Desktop Chrome'], }, }, ], }); ================================================ FILE: e2e/portal-e2e/scripts/auth/setup-ldap.sh ================================================ #!/usr/bin/env bash # # Copyright 2026 Apollo Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # set -euo pipefail LDAP_CONTAINER_NAME="${LDAP_CONTAINER_NAME:-apollo-e2e-ldap}" LDAP_PORT="${LDAP_PORT:-3389}" LDAP_DOMAIN="${LDAP_DOMAIN:-example.org}" LDAP_ORGANISATION="${LDAP_ORGANISATION:-Apollo E2E Org}" LDAP_ADMIN_PASSWORD="${LDAP_ADMIN_PASSWORD:-admin}" LDAP_ALLOWED_USER="${LDAP_ALLOWED_USER:-apollo}" LDAP_ALLOWED_USER_PASSWORD="${LDAP_ALLOWED_USER_PASSWORD:-admin}" LDAP_ALLOWED_USER_DISPLAY_NAME="${LDAP_ALLOWED_USER_DISPLAY_NAME:-Apollo Admin}" LDAP_ALLOWED_USER_EMAIL="${LDAP_ALLOWED_USER_EMAIL:-apollo@example.org}" LDAP_ALLOWED_USER_SECONDARY="${LDAP_ALLOWED_USER_SECONDARY:-devops1}" LDAP_ALLOWED_USER_SECONDARY_PASSWORD="${LDAP_ALLOWED_USER_SECONDARY_PASSWORD:-admin}" LDAP_ALLOWED_USER_SECONDARY_DISPLAY_NAME="${LDAP_ALLOWED_USER_SECONDARY_DISPLAY_NAME:-Dev Ops One}" LDAP_ALLOWED_USER_SECONDARY_EMAIL="${LDAP_ALLOWED_USER_SECONDARY_EMAIL:-devops1@example.org}" LDAP_BLOCKED_USER="${LDAP_BLOCKED_USER:-blocked1}" LDAP_BLOCKED_USER_PASSWORD="${LDAP_BLOCKED_USER_PASSWORD:-admin}" LDAP_BLOCKED_USER_DISPLAY_NAME="${LDAP_BLOCKED_USER_DISPLAY_NAME:-Blocked User}" LDAP_BLOCKED_USER_EMAIL="${LDAP_BLOCKED_USER_EMAIL:-blocked1@example.org}" LDAP_BASE_DN="dc=example,dc=org" echo "[setup-ldap] Preparing container ${LDAP_CONTAINER_NAME}" docker rm -f "${LDAP_CONTAINER_NAME}" >/dev/null 2>&1 || true docker run --name "${LDAP_CONTAINER_NAME}" \ --detach \ --publish "${LDAP_PORT}:389" \ --env "LDAP_ORGANISATION=${LDAP_ORGANISATION}" \ --env "LDAP_DOMAIN=${LDAP_DOMAIN}" \ --env "LDAP_ADMIN_PASSWORD=${LDAP_ADMIN_PASSWORD}" \ osixia/openldap:1.5.0 >/dev/null echo "[setup-ldap] Waiting for OpenLDAP to be ready" for _ in $(seq 1 60); do if docker exec "${LDAP_CONTAINER_NAME}" ldapsearch \ -x \ -H ldap://localhost:389 \ -D "cn=admin,${LDAP_BASE_DN}" \ -w "${LDAP_ADMIN_PASSWORD}" \ -b "${LDAP_BASE_DN}" \ "(objectClass=*)" dn >/dev/null 2>&1; then break fi sleep 2 done tmp_ldif="$(mktemp)" cleanup() { rm -f "${tmp_ldif}" } trap cleanup EXIT cat > "${tmp_ldif}" </dev/null echo "[setup-ldap] LDAP fixtures imported" docker exec "${LDAP_CONTAINER_NAME}" ldapsearch \ -x \ -H ldap://localhost:389 \ -D "cn=admin,${LDAP_BASE_DN}" \ -w "${LDAP_ADMIN_PASSWORD}" \ -b "ou=group,${LDAP_BASE_DN}" \ "(cn=dev)" memberUid >/dev/null echo "[setup-ldap] Done" ================================================ FILE: e2e/portal-e2e/scripts/auth/setup-oidc.sh ================================================ #!/usr/bin/env bash # # Copyright 2026 Apollo Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # set -euo pipefail KEYCLOAK_CONTAINER_NAME="${KEYCLOAK_CONTAINER_NAME:-apollo-e2e-keycloak}" KEYCLOAK_PORT="${KEYCLOAK_PORT:-9080}" KEYCLOAK_IMAGE="${KEYCLOAK_IMAGE:-quay.io/keycloak/keycloak:26.1.0}" KEYCLOAK_ADMIN_USER="${KEYCLOAK_ADMIN_USER:-admin}" KEYCLOAK_ADMIN_PASSWORD="${KEYCLOAK_ADMIN_PASSWORD:-admin}" OIDC_REALM="${OIDC_REALM:-apollo}" OIDC_USERNAME="${OIDC_USERNAME:-apollo}" OIDC_PASSWORD="${OIDC_PASSWORD:-admin}" OIDC_PRIMARY_EMAIL="${OIDC_PRIMARY_EMAIL:-apollo@example.org}" OIDC_PRIMARY_FIRST_NAME="${OIDC_PRIMARY_FIRST_NAME:-Apollo}" OIDC_PRIMARY_LAST_NAME="${OIDC_PRIMARY_LAST_NAME:-Admin}" OIDC_SECONDARY_USERNAME="${OIDC_SECONDARY_USERNAME:-oidcdev1}" OIDC_SECONDARY_PASSWORD="${OIDC_SECONDARY_PASSWORD:-admin}" OIDC_SECONDARY_EMAIL="${OIDC_SECONDARY_EMAIL:-oidcdev1@example.org}" OIDC_SECONDARY_FIRST_NAME="${OIDC_SECONDARY_FIRST_NAME:-Oidc}" OIDC_SECONDARY_LAST_NAME="${OIDC_SECONDARY_LAST_NAME:-DevOne}" OIDC_CLIENT_ID="${OIDC_CLIENT_ID:-apollo-portal}" OIDC_CLIENT_SECRET="${OIDC_CLIENT_SECRET:-apollo-secret}" PORTAL_BASE_URL="${PORTAL_BASE_URL:-http://127.0.0.1:8070}" ALT_PORTAL_BASE_URL="${ALT_PORTAL_BASE_URL:-http://localhost:8070}" KEYCLOAK_URL="http://127.0.0.1:${KEYCLOAK_PORT}" echo "[setup-oidc] Preparing container ${KEYCLOAK_CONTAINER_NAME}" docker rm -f "${KEYCLOAK_CONTAINER_NAME}" >/dev/null 2>&1 || true docker run --name "${KEYCLOAK_CONTAINER_NAME}" \ --detach \ --publish "${KEYCLOAK_PORT}:8080" \ --env "KEYCLOAK_ADMIN=${KEYCLOAK_ADMIN_USER}" \ --env "KEYCLOAK_ADMIN_PASSWORD=${KEYCLOAK_ADMIN_PASSWORD}" \ "${KEYCLOAK_IMAGE}" \ start-dev >/dev/null echo "[setup-oidc] Waiting for Keycloak master realm" for _ in $(seq 1 120); do if curl -fsS "${KEYCLOAK_URL}/realms/master/.well-known/openid-configuration" >/dev/null 2>&1; then break fi sleep 2 done docker exec "${KEYCLOAK_CONTAINER_NAME}" /opt/keycloak/bin/kcadm.sh config credentials \ --server http://localhost:8080 \ --realm master \ --user "${KEYCLOAK_ADMIN_USER}" \ --password "${KEYCLOAK_ADMIN_PASSWORD}" >/dev/null docker exec "${KEYCLOAK_CONTAINER_NAME}" /opt/keycloak/bin/kcadm.sh create realms \ -s "realm=${OIDC_REALM}" \ -s enabled=true >/dev/null create_user() { local username="$1" local password="$2" local email="$3" local first_name="$4" local last_name="$5" local user_id user_id="$(docker exec "${KEYCLOAK_CONTAINER_NAME}" /opt/keycloak/bin/kcadm.sh create users \ -r "${OIDC_REALM}" \ -s "username=${username}" \ -s enabled=true \ -s "email=${email}" \ -s "firstName=${first_name}" \ -s "lastName=${last_name}" \ -s emailVerified=true \ -i | tr -d '\r\n')" docker exec "${KEYCLOAK_CONTAINER_NAME}" /opt/keycloak/bin/kcadm.sh set-password \ -r "${OIDC_REALM}" \ --userid "${user_id}" \ --new-password "${password}" >/dev/null docker exec "${KEYCLOAK_CONTAINER_NAME}" /opt/keycloak/bin/kcadm.sh update "users/${user_id}" \ -r "${OIDC_REALM}" \ -s 'requiredActions=[]' >/dev/null } create_user "${OIDC_USERNAME}" "${OIDC_PASSWORD}" "${OIDC_PRIMARY_EMAIL}" \ "${OIDC_PRIMARY_FIRST_NAME}" "${OIDC_PRIMARY_LAST_NAME}" create_user "${OIDC_SECONDARY_USERNAME}" "${OIDC_SECONDARY_PASSWORD}" "${OIDC_SECONDARY_EMAIL}" \ "${OIDC_SECONDARY_FIRST_NAME}" "${OIDC_SECONDARY_LAST_NAME}" client_id="$(docker exec "${KEYCLOAK_CONTAINER_NAME}" /opt/keycloak/bin/kcadm.sh create clients \ -r "${OIDC_REALM}" \ -s "clientId=${OIDC_CLIENT_ID}" \ -s enabled=true \ -s protocol=openid-connect \ -s publicClient=false \ -s "secret=${OIDC_CLIENT_SECRET}" \ -s standardFlowEnabled=true \ -s directAccessGrantsEnabled=true \ -i | tr -d '\r\n')" primary_redirect="${PORTAL_BASE_URL%/}/login/oauth2/code/*" secondary_redirect="${ALT_PORTAL_BASE_URL%/}/login/oauth2/code/*" primary_origin="${PORTAL_BASE_URL%/}" secondary_origin="${ALT_PORTAL_BASE_URL%/}" docker exec "${KEYCLOAK_CONTAINER_NAME}" /opt/keycloak/bin/kcadm.sh update "clients/${client_id}" \ -r "${OIDC_REALM}" \ -s "redirectUris=[\"${primary_redirect}\",\"${secondary_redirect}\"]" \ -s "webOrigins=[\"${primary_origin}\",\"${secondary_origin}\"]" >/dev/null echo "[setup-oidc] Keycloak fixtures imported" curl -fsS "${KEYCLOAK_URL}/realms/${OIDC_REALM}/.well-known/openid-configuration" >/dev/null echo "[setup-oidc] Done" ================================================ FILE: e2e/portal-e2e/scripts/auth/teardown-auth.sh ================================================ #!/usr/bin/env bash # # Copyright 2026 Apollo Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # set -euo pipefail LDAP_CONTAINER_NAME="${LDAP_CONTAINER_NAME:-apollo-e2e-ldap}" KEYCLOAK_CONTAINER_NAME="${KEYCLOAK_CONTAINER_NAME:-apollo-e2e-keycloak}" docker rm -f "${LDAP_CONTAINER_NAME}" >/dev/null 2>&1 || true docker rm -f "${KEYCLOAK_CONTAINER_NAME}" >/dev/null 2>&1 || true echo "[teardown-auth] Containers cleaned" ================================================ FILE: e2e/portal-e2e/scripts/wait-for-ready.sh ================================================ #!/usr/bin/env bash # # Copyright 2026 Apollo Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # set -euo pipefail BASE_HOST="${BASE_HOST:-127.0.0.1}" PORTAL_URL="${PORTAL_URL:-http://${BASE_HOST}:8070}" CONFIG_URL="${CONFIG_URL:-http://${BASE_HOST}:8080}" ADMIN_URL="${ADMIN_URL:-http://${BASE_HOST}:8090}" WAIT_TIMEOUT_SECONDS="${WAIT_TIMEOUT_SECONDS:-300}" PORTAL_USERNAME="${PORTAL_USERNAME:-apollo}" PORTAL_PASSWORD="${PORTAL_PASSWORD:-admin}" PORTAL_AUTH_MODE="${PORTAL_AUTH_MODE:-auth}" probe() { local url="$1" curl -fsS "$url" >/dev/null 2>&1 } portal_http_status() { local url="$1" curl -sS -o /dev/null -w '%{http_code}' "$url" 2>/dev/null || echo "000" } portal_ready_for_oidc() { local code code="$(portal_http_status "${PORTAL_URL}/")" [[ "$code" == "200" || "$code" == "302" || "$code" == "401" ]] } has_admin_service_registration() { local services services="$(curl -fsS "${CONFIG_URL}/services/admin" 2>/dev/null || true)" [[ -n "$services" ]] && [[ "$services" != "[]" ]] && [[ "$services" == *"apollo-adminservice"* ]] } warm_up_portal_admin_path() { local app_id cookie_file app_payload item_payload release_payload app_status item_status release_status cookie_file="$(mktemp)" app_id="warmup$(date +%s)$RANDOM" curl -fsS -c "$cookie_file" -b "$cookie_file" \ -H 'Content-Type: application/x-www-form-urlencoded' \ -X POST "${PORTAL_URL}/signin" \ --data-urlencode "username=${PORTAL_USERNAME}" \ --data-urlencode "password=${PORTAL_PASSWORD}" >/dev/null 2>&1 || { rm -f "$cookie_file" return 1 } app_payload=$(cat < { const value = url.toString(); return !value.includes('/signin') && !value.includes('login.html'); }, { timeout: 60000 } ), page.click('#login-submit'), ]); const cookies = await page.context().cookies(); expect(cookies.some((cookie) => cookie.name === SESSION_COOKIE_NAME)).toBeTruthy(); } async function expectFormLoginFailure(page, options = {}) { const users = getAuthUsers(); const username = options.username || users.invalidUser; const password = options.password || users.invalidPassword; await page.goto('/login.html', { waitUntil: 'domcontentloaded' }); await page.fill('input[name="username"]', username); await page.fill('input[name="password"]', password); await Promise.all([ page.waitForURL( (url) => { const value = url.toString(); return value.includes('/signin') || value.includes('login.html'); }, { timeout: 60000 } ), page.click('#login-submit'), ]); await expect(page).toHaveURL(/(signin|login\.html)/); await expect(page).toHaveURL(/#\/error/); } async function waitForOidcLoginPage(page) { await page.waitForURL( (url) => { const value = url.toString(); return value.includes('/realms/') && ( value.includes('/protocol/openid-connect/') || value.includes('/login-actions/') ); }, { timeout: 90000 } ); await page.locator('#username').waitFor({ state: 'visible', timeout: 30000 }); await page.locator('#password').waitFor({ state: 'visible', timeout: 30000 }); } async function loginByOidc(page, options = {}) { const users = getAuthUsers(); const username = options.username || users.successUser; const password = options.password || users.successPassword; await page.goto('/', { waitUntil: 'domcontentloaded' }); await waitForOidcLoginPage(page); await page.fill('#username', username); await page.fill('#password', password); const baseUrl = options.baseUrl || getBaseUrl(); await Promise.all([ page.waitForURL( (url) => { const value = url.toString(); return value.startsWith(baseUrl) && !value.includes('/login/oauth2/code/'); }, { timeout: 90000 } ), page.click('#kc-login'), ]); const cookies = await page.context().cookies(); expect(cookies.some((cookie) => cookie.name === SESSION_COOKIE_NAME)).toBeTruthy(); } async function expectOidcLoginFailure(page, options = {}) { const users = getAuthUsers(); const username = options.username || users.invalidUser; const password = options.password || users.invalidPassword; await page.goto('/', { waitUntil: 'domcontentloaded' }); await waitForOidcLoginPage(page); await page.fill('#username', username); await page.fill('#password', password); await page.click('#kc-login'); await waitForOidcLoginPage(page); const errorSelectors = ['#input-error', '.alert-error', '#kc-error-message', '.kc-feedback-text']; const visibleError = page.locator(errorSelectors.join(',')); await expect(visibleError.first()).toBeVisible({ timeout: 30000 }); } async function warmUpOidcSecondaryUser(browser, options = {}) { if (resolveAuthMode() !== MODE_OIDC) { return; } const users = getAuthUsers(); const context = await browser.newContext({ baseURL: options.baseUrl || getBaseUrl(), }); try { const page = await context.newPage(); await loginByOidc(page, { username: options.username || users.secondaryUser, password: options.password || users.secondaryPassword, baseUrl: options.baseUrl, }); } finally { await context.close(); } } module.exports = { MODE_LDAP, MODE_OIDC, resolveAuthMode, getBaseUrl, getAuthUsers, loginByMode, expectLoginFailureByMode, warmUpOidcSecondaryUser, }; ================================================ FILE: e2e/portal-e2e/tests/helpers/portal-helpers.js ================================================ /* * Copyright 2026 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ const { expect } = require('@playwright/test'); const USERNAME = process.env.PORTAL_USERNAME || 'apollo'; const PASSWORD = process.env.PORTAL_PASSWORD || 'admin'; const DEFAULT_SUCCESS_STATUSES = [200, 201, 202, 204]; const DEFAULT_ENV = 'LOCAL'; const DEFAULT_CLUSTER = 'default'; const DEFAULT_NAMESPACE = 'application'; const DEFAULT_CONFIG_BASE_URL = 'http://127.0.0.1:8080'; function generateUniqueId(prefix) { const randomSuffix = Math.floor(Math.random() * 10000) .toString() .padStart(4, '0'); return `${prefix}${Date.now()}${randomSuffix}`; } function isExpectedStatus(actualStatus, expectedStatus) { if (Array.isArray(expectedStatus)) { return expectedStatus.includes(actualStatus); } if (expectedStatus === null || expectedStatus === undefined) { return actualStatus >= 200 && actualStatus < 400; } return actualStatus === expectedStatus; } function resolveConfigServiceBaseUrl() { const explicitConfigUrl = process.env.CONFIG_URL; if (explicitConfigUrl) { return explicitConfigUrl.replace(/\/$/, ''); } const baseUrl = process.env.BASE_URL || 'http://127.0.0.1:8070'; try { const parsed = new URL(baseUrl); if (!parsed.port || parsed.port === '8070') { parsed.port = '8080'; } return parsed.origin.replace(/\/$/, ''); } catch (error) { return DEFAULT_CONFIG_BASE_URL; } } function encodePathSegment(value) { return encodeURIComponent(value); } function toPropertiesText(properties) { const lines = Object.entries(properties).map(([key, value]) => `${key}=${value}`); return `${lines.join('\n')}\n`; } function normalizeNotificationNamespace(namespaceName) { return `${namespaceName || ''}`.replace(/\.properties$/i, '').toLowerCase(); } function escapeRegExp(value) { return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } function resolveNamespaceDisplayNames(namespaceName) { if (!namespaceName) { return [DEFAULT_NAMESPACE]; } const stripped = namespaceName.replace(/\.(properties|yaml|yml|json)$/i, ''); return Array.from(new Set([namespaceName, stripped].filter(Boolean))); } async function sleep(milliseconds) { return new Promise((resolve) => setTimeout(resolve, milliseconds)); } async function locateNamespacePanel(page, namespaceName) { await page.locator('.panel.namespace-panel').first().waitFor({ state: 'visible', timeout: 90000 }); const candidates = resolveNamespaceDisplayNames(namespaceName); for (const candidate of candidates) { const panel = page.locator('.panel.namespace-panel:not(.hidden)').filter({ has: page.locator('b.namespace-name', { hasText: new RegExp(`^\\s*${escapeRegExp(candidate)}\\s*$`, 'i'), }), }).first(); if (await panel.count()) { await panel.waitFor({ state: 'visible', timeout: 30000 }); return panel; } } const fallbackPanel = page.locator('.panel.namespace-panel:not(.hidden)').filter({ has: page.locator('b.namespace-name', { hasText: candidates[0] || DEFAULT_NAMESPACE }), }).first(); await fallbackPanel.waitFor({ state: 'visible', timeout: 30000 }); return fallbackPanel; } async function waitForApiResponse(page, method, urlFragment, status = DEFAULT_SUCCESS_STATUSES) { return page.waitForResponse( (response) => response.request().method() === method && response.url().includes(urlFragment) && isExpectedStatus(response.status(), status), { timeout: 90000 } ); } async function waitForApiCall(page, method, urlFragment) { return page.waitForResponse( (response) => response.request().method() === method && response.url().includes(urlFragment), { timeout: 90000 } ); } async function waitForApiResponseByFragments( page, method, requiredFragments, status = DEFAULT_SUCCESS_STATUSES ) { return page.waitForResponse( (response) => { if (response.request().method() !== method) { return false; } if (!isExpectedStatus(response.status(), status)) { return false; } const url = response.url(); return requiredFragments.every((fragment) => url.includes(fragment)); }, { timeout: 90000 } ); } async function login(page) { await page.goto('/login.html', { waitUntil: 'domcontentloaded' }); await page.fill('input[name="username"]', USERNAME); await page.fill('input[name="password"]', PASSWORD); await Promise.all([ page.waitForURL((url) => !url.toString().includes('login.html'), { timeout: 60000, }), page.click('#login-submit'), ]); const cookies = await page.context().cookies(); expect(cookies.some((cookie) => cookie.name === 'SESSION')).toBeTruthy(); } async function selectOrganization(page) { await page.waitForFunction(() => { return ( typeof window.$ === 'function' && window.$('#organization').length > 0 && window.$('#organization').data('select2') ); }); await page.evaluate(() => { const select = window.$('#organization'); const internal = select.data('select2'); const data = internal?.options?.options?.data || []; const first = data.find((item) => item && item.id); if (!first) { throw new Error('No organization options are available'); } if (select.find(`option[value="${first.id}"]`).length === 0) { const option = new Option(first.text, first.id, true, true); select.append(option); } select.val(first.id).trigger('change'); }); } async function selectUserByKeyword(page, panelSelector, keyword, options = {}) { const { exact = false, timeoutMs = 20000, } = options; await page.locator(`${panelSelector} .select2-selection`).first().click(); const searchInput = page.locator('body .select2-container--open .select2-search__field'); await searchInput.fill(keyword); const optionMatcher = exact ? new RegExp(`^\\s*${escapeRegExp(keyword)}\\s*$`, 'i') : keyword; const matchedOption = page .locator('body .select2-container--open .select2-results__option') .filter({ hasText: optionMatcher }) .first(); await matchedOption.waitFor({ state: 'visible', timeout: timeoutMs }); const selectedText = (await matchedOption.innerText()).trim(); await matchedOption.click(); return selectedText; } async function createAppViaUiWithUserSelection(page, appId, options = {}) { const { ownerKeyword = USERNAME, adminKeyword = USERNAME, } = options; await page.goto('/app.html', { waitUntil: 'domcontentloaded' }); await selectOrganization(page); await page.fill('input[name="appId"]', appId); await page.fill('input[name="appName"]', appId); const ownerSelectionText = await selectUserByKeyword( page, '.J_ownerSelectorPanel', ownerKeyword ); const adminSelectionText = await selectUserByKeyword( page, '.J_adminSelectorPanel', adminKeyword ); await Promise.all([ page.waitForURL( (url) => url.toString().includes('config.html') && url.toString().includes(`appid=${appId}`), { timeout: 90000 } ), page.click('button[type="submit"]'), ]); await expect(page).toHaveURL(/config\.html/); return { ownerSelectionText, adminSelectionText, }; } async function createAppViaUi(page, appId) { await createAppViaUiWithUserSelection(page, appId); } async function submitAppCreation(page, appId) { await page.goto('/app.html', { waitUntil: 'domcontentloaded' }); await selectOrganization(page); await page.fill('input[name="appId"]', appId); await page.fill('input[name="appName"]', appId); await selectUserByKeyword(page, '.J_ownerSelectorPanel', USERNAME); await selectUserByKeyword(page, '.J_adminSelectorPanel', USERNAME); const createAppResponse = waitForApiCall(page, 'POST', '/apps'); await page.click('button[type="submit"]'); return createAppResponse; } async function submitClusterCreation(page, appId, clusterName) { await page.goto(`/cluster.html?e2e=${Date.now()}#/appid=${appId}`, { waitUntil: 'domcontentloaded' }); const clusterNameInput = page.locator('.apollo-container:not(.hidden) input[name="clusterName"]').first(); const clusterCommentInput = page.locator('.apollo-container:not(.hidden) textarea[name="clusterComment"]').first(); await clusterNameInput.waitFor({ state: 'visible', timeout: 60000 }); await clusterNameInput.fill(clusterName); await clusterCommentInput.fill('portal regression cluster creation'); await page.locator('.apollo-container:not(.hidden) tr:has-text("LOCAL") input[type="checkbox"]').first().click(); const createClusterResponse = waitForApiCall(page, 'POST', `/apps/${appId}/clusters`); await page.click('.apollo-container:not(.hidden) form[name="clusterForm"] button[type="submit"]'); return createClusterResponse; } async function createClusterViaUi(page, appId, clusterName) { const createClusterResponse = await submitClusterCreation(page, appId, clusterName); expect([200, 201, 204, 302]).toContain(createClusterResponse.status()); await expect(page.locator('div.row.text-center h3')).toBeVisible({ timeout: 30000 }); } async function submitNamespaceCreation(page, appId, namespaceName) { return submitNamespaceCreationWithOptions(page, appId, namespaceName, {}); } async function submitNamespaceCreationWithOptions(page, appId, namespaceName, options) { const { format = 'properties', isPublic, comment = 'portal regression namespace creation' } = options; await page.goto(`/namespace.html?#/appid=${appId}`, { waitUntil: 'domcontentloaded' }); const namespaceNameInput = page.locator('.apollo-container:not(.hidden) input[name="namespaceName"]').first(); const namespaceCommentInput = page.locator('.apollo-container:not(.hidden) textarea[name="comment"]').first(); await namespaceNameInput.waitFor({ state: 'visible', timeout: 60000 }); if (typeof isPublic === 'boolean') { const visibilitySelector = isPublic ? '.apollo-container:not(.hidden) input[name="namespaceType"][value="true"]' : '.apollo-container:not(.hidden) input[name="namespaceType"][value="false"]'; const namespaceTypeRadio = page.locator(visibilitySelector).first(); if (await namespaceTypeRadio.isVisible().catch(() => false)) { await namespaceTypeRadio.check(); } } if (format) { await page.locator('.apollo-container:not(.hidden) select[name="format"]').first().selectOption(format); } await namespaceNameInput.fill(namespaceName); await namespaceCommentInput.fill(comment); const createNamespaceResponse = waitForApiCall(page, 'POST', `/apps/${appId}/appnamespaces`); await page.click('.apollo-container:not(.hidden) form[name="namespaceForm"] button[type="submit"]'); return createNamespaceResponse; } async function createNamespaceViaUi(page, appId, namespaceName, options = {}) { const createNamespaceResponse = await submitNamespaceCreationWithOptions(page, appId, namespaceName, options); expect([200, 201, 202, 204, 302]).toContain(createNamespaceResponse.status()); let createdNamespaceName = namespaceName; const createNamespaceBody = await createNamespaceResponse.json().catch(() => null); if (createNamespaceBody && createNamespaceBody.name) { createdNamespaceName = createNamespaceBody.name; } else if (options.format && options.format !== 'properties') { createdNamespaceName = `${namespaceName}.${options.format}`; } await page.waitForURL((url) => url.toString().includes('/namespace/role.html'), { timeout: 30000 }); return createdNamespaceName; } async function clearNamespaceRoleViaPortalApi(page, appId, namespaceName, options = {}) { const { roleType = 'ModifyNamespace', userId = USERNAME, env, } = options; const namespacePath = encodePathSegment(namespaceName); const appPath = encodePathSegment(appId); const envPath = env ? encodePathSegment(env) : ''; const userQuery = encodePathSegment(userId); const path = env ? `/apps/${appPath}/envs/${envPath}/namespaces/${namespacePath}/roles/${roleType}?user=${userQuery}` : `/apps/${appPath}/namespaces/${namespacePath}/roles/${roleType}?user=${userQuery}`; const response = await page.context().request.delete(path); expect([200, 204, 400, 404]).toContain(response.status()); } async function assignNamespaceRoleViaUi(page, appId, namespaceName, options = {}) { const { roleType = 'ModifyNamespace', userId = USERNAME, env = '', } = options; const namespaceParam = encodePathSegment(namespaceName); await page.goto(`/namespace/role.html?#/appid=${appId}&namespaceName=${namespaceParam}`, { waitUntil: 'domcontentloaded', }); await page.locator('section.panel[ng-controller="NamespaceRoleController"]').waitFor({ state: 'visible', timeout: 60000, }); const isReleaseRole = roleType === 'ReleaseNamespace'; const widgetClass = isReleaseRole ? 'releaseRoleWidgetId' : 'modifyRoleWidgetId'; const formSelector = isReleaseRole ? 'form[ng-submit="assignRoleToUser(\'ReleaseNamespace\')"]' : 'form[ng-submit="assignRoleToUser(\'ModifyNamespace\')"]'; await page.waitForFunction( ({ widgetClass: targetWidgetClass, userId: targetUserId }) => { if (typeof window.$ !== 'function') { return false; } const selector = window.$(`.${targetWidgetClass}`); if (!selector.length || !selector.data('select2')) { return false; } if (selector.find(`option[value="${targetUserId}"]`).length === 0) { selector.append(new Option(targetUserId, targetUserId, true, true)); } selector.val(targetUserId).trigger('change'); return true; }, { widgetClass, userId } ); let targetEnv = env; if (env) { const envSelector = isReleaseRole ? `${formSelector} select[ng-model="releaseRoleSelectedEnv"]` : `${formSelector} select[ng-model="modifyRoleSelectedEnv"]`; const roleEnvSelector = page.locator(envSelector).first(); await roleEnvSelector.waitFor({ state: 'visible', timeout: 30000 }); targetEnv = await roleEnvSelector.evaluate((select, preferredEnv) => { const envOptions = Array.from(select.options) .map((option) => option.value) .filter(Boolean); if (envOptions.length === 0) { return ''; } const selectedEnv = preferredEnv && envOptions.includes(preferredEnv) ? preferredEnv : envOptions[0]; select.value = selectedEnv; select.dispatchEvent(new Event('change', { bubbles: true })); return selectedEnv; }, env); } await clearNamespaceRoleViaPortalApi(page, appId, namespaceName, { roleType, userId, ...(targetEnv ? { env: targetEnv } : {}), }); const grantRoleResponse = waitForApiResponseByFragments( page, 'POST', [ `/apps/${appId}/`, '/namespaces/', `/roles/${roleType}`, ...(targetEnv ? [`/envs/${targetEnv}/`] : []), ] ); await Promise.all([ grantRoleResponse, page.locator(`${formSelector} button[type="submit"]`).first().click(), ]); await expect(page.locator('.toast-success').first()).toBeVisible({ timeout: 30000 }); return targetEnv; } function parseUserIdFromSelect2Text(selectionText) { if (!selectionText) { return ''; } const [firstPart] = selectionText.split('|'); return `${firstPart || ''}`.trim(); } async function assignNamespaceRoleViaUiBySearch(page, appId, namespaceName, options = {}) { const { roleType = 'ModifyNamespace', userKeyword = USERNAME, env = '', } = options; const namespaceParam = encodePathSegment(namespaceName); await page.goto(`/namespace/role.html?#/appid=${appId}&namespaceName=${namespaceParam}`, { waitUntil: 'domcontentloaded', }); await page.locator('section.panel[ng-controller="NamespaceRoleController"]').waitFor({ state: 'visible', timeout: 60000, }); const isReleaseRole = roleType === 'ReleaseNamespace'; const formSelector = isReleaseRole ? 'form[ng-submit="assignRoleToUser(\'ReleaseNamespace\')"]' : 'form[ng-submit="assignRoleToUser(\'ModifyNamespace\')"]'; const selectedUserText = await selectUserByKeyword(page, formSelector, userKeyword); const selectedUserId = parseUserIdFromSelect2Text(selectedUserText); expect(selectedUserId).toBeTruthy(); let targetEnv = env; if (env) { const envSelector = isReleaseRole ? `${formSelector} select[ng-model="releaseRoleSelectedEnv"]` : `${formSelector} select[ng-model="modifyRoleSelectedEnv"]`; const roleEnvSelector = page.locator(envSelector).first(); await roleEnvSelector.waitFor({ state: 'visible', timeout: 30000 }); targetEnv = await roleEnvSelector.evaluate((select, preferredEnv) => { const envOptions = Array.from(select.options) .map((option) => option.value) .filter(Boolean); if (envOptions.length === 0) { return ''; } const selectedEnv = preferredEnv && envOptions.includes(preferredEnv) ? preferredEnv : envOptions[0]; select.value = selectedEnv; select.dispatchEvent(new Event('change', { bubbles: true })); return selectedEnv; }, env); } await clearNamespaceRoleViaPortalApi(page, appId, namespaceName, { roleType, userId: selectedUserId, ...(targetEnv ? { env: targetEnv } : {}), }); const grantRoleResponse = waitForApiResponseByFragments( page, 'POST', [ `/apps/${appId}/`, '/namespaces/', `/roles/${roleType}`, ...(targetEnv ? [`/envs/${targetEnv}/`] : []), ] ); await Promise.all([ grantRoleResponse, page.locator(`${formSelector} button[type="submit"]`).first().click(), ]); await expect(page.locator('.toast-success').first()).toBeVisible({ timeout: 30000 }); return { targetEnv, selectedUserId, selectedUserText, }; } async function revokeNamespaceRoleViaUi(page, appId, namespaceName, options = {}) { const { roleType = 'ModifyNamespace', userId = USERNAME, env = '', } = options; const namespaceParam = encodePathSegment(namespaceName); await page.goto(`/namespace/role.html?#/appid=${appId}&namespaceName=${namespaceParam}`, { waitUntil: 'domcontentloaded', }); await page.locator('section.panel[ng-controller="NamespaceRoleController"]').waitFor({ state: 'visible', timeout: 60000, }); const isReleaseRole = roleType === 'ReleaseNamespace'; const formSelector = isReleaseRole ? 'form[ng-submit="assignRoleToUser(\'ReleaseNamespace\')"]' : 'form[ng-submit="assignRoleToUser(\'ModifyNamespace\')"]'; const roleSection = page.locator(formSelector) .locator('xpath=ancestor::div[contains(@class,"col-sm-8")]') .first(); const envContainer = env ? roleSection.locator('.item-container').filter({ has: roleSection.locator('h5', { hasText: new RegExp(`^\\s*${escapeRegExp(env)}\\s*$`), }), }).first() : roleSection; const userRole = envContainer.locator('.btn-group.item-info').filter({ hasText: userId }).first(); await userRole.waitFor({ state: 'visible', timeout: 30000 }); const revokeRoleResponse = waitForApiResponseByFragments( page, 'DELETE', [ `/apps/${appId}/`, '/namespaces/', `/roles/${roleType}`, ...(env ? [`/envs/${env}/`] : []), ], [200, 204, 400, 404] ); await Promise.all([ revokeRoleResponse, userRole.locator('button.dropdown-toggle').first().click(), ]); await expect(page.locator('.toast-success').first()).toBeVisible({ timeout: 30000 }); } async function openConfigPage(page, appId, options = {}) { const { env = DEFAULT_ENV, clusterName = DEFAULT_CLUSTER, namespaceName = DEFAULT_NAMESPACE, allowBranchView = false, } = options; const encodedNamespaceName = encodePathSegment(namespaceName); await page.evaluate(() => window.localStorage.removeItem('OperateBranch')).catch(() => {}); await page.goto( `/config.html?#/appid=${appId}&env=${env}&cluster=${clusterName}&namespace=${encodedNamespaceName}`, { waitUntil: 'domcontentloaded' } ); const publishButton = page.locator('[ng-click="publish(namespace)"]:visible').first(); const createMissingEnvButton = page.locator('a.list-group-item[ng-click="createAppInMissEnv()"]').first(); const publishVisible = await publishButton.isVisible().catch(() => false); if (!publishVisible) { const createMissingEnvVisible = await createMissingEnvButton.isVisible().catch(() => false); if (createMissingEnvVisible) { const createInMissEnvResponse = waitForApiCall(page, 'POST', `/apps/${appId}/envs/`); await createMissingEnvButton.click(); await createInMissEnvResponse; } } const namespacePanel = await locateNamespacePanel(page, namespaceName); const masterPublishButton = namespacePanel.locator('[ng-click="publish(namespace)"]:visible').first(); const branchPublishButton = namespacePanel.locator('[ng-click="publish(namespace.branch)"]:visible').first(); const isMasterVisible = await masterPublishButton.isVisible().catch(() => false); if (isMasterVisible) { return; } const isBranchVisible = await branchPublishButton.isVisible().catch(() => false); if (isBranchVisible && !allowBranchView) { await namespacePanel.locator('a[ng-click="switchBranch(\'master\', true)"]').first().click(); } await masterPublishButton.waitFor({ state: 'visible', timeout: 90000, }); } async function switchNamespaceView(page, namespaceName, viewType) { const namespacePanel = await locateNamespacePanel(page, namespaceName); await namespacePanel.locator(`li[ng-click="switchView(namespace, '${viewType}')"]`).first().click(); } async function editNamespaceTextViaUi(page, appId, configText, options = {}) { const { env = DEFAULT_ENV, clusterName = DEFAULT_CLUSTER, namespaceName = DEFAULT_NAMESPACE, } = options; await openConfigPage(page, appId, { env, clusterName, namespaceName, }); await switchNamespaceView(page, namespaceName, 'text'); const namespacePanel = await locateNamespacePanel(page, namespaceName); const toggleEditButton = namespacePanel.locator('[ng-click="toggleTextEditStatus(namespace)"]:visible').first(); await toggleEditButton.waitFor({ state: 'visible', timeout: 30000 }); await toggleEditButton.click(); await namespacePanel.locator('[ng-click="modifyByText(namespace)"]:visible').first().waitFor({ state: 'visible', timeout: 30000, }); await page.evaluate( ({ targetNamespaceName, text }) => { if (!window.angular) { throw new Error('angular is not available on config page'); } const escapeRegExpInBrowser = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const stripped = targetNamespaceName.replace(/\.(properties|yaml|yml|json)$/i, ''); const candidates = [targetNamespaceName, stripped].filter(Boolean); const panels = Array.from(document.querySelectorAll('.panel.namespace-panel')); const targetPanel = candidates .map((candidate) => { const matcher = new RegExp(`^\\s*${escapeRegExpInBrowser(candidate)}\\s*$`, 'i'); return panels.find((panel) => { const namespaceLabel = panel.querySelector('b.namespace-name'); return namespaceLabel && matcher.test(namespaceLabel.textContent || ''); }); }) .find(Boolean); if (!targetPanel) { throw new Error(`Unable to locate namespace panel by namespaceName=${targetNamespaceName}`); } const targetScope = window.angular.element(targetPanel).scope(); if (!targetScope || !targetScope.namespace) { throw new Error('Unable to locate namespace scope for text editing'); } targetScope.namespace.editText = text; targetScope.$applyAsync(); }, { targetNamespaceName: namespaceName, text: configText } ); const updateItemsResponse = waitForApiResponse( page, 'PUT', `/apps/${appId}/envs/${env}/clusters/${clusterName}/namespaces/${namespaceName}/items` ); await Promise.all([ updateItemsResponse, namespacePanel.locator('[ng-click="modifyByText(namespace)"]:visible').first().click(), ]); await expect(page.locator('.toast-success').first()).toBeVisible({ timeout: 30000 }); } async function linkPublicNamespacesViaUi(page, appId, namespaceNames) { await page.goto(`/namespace.html?#/appid=${appId}`, { waitUntil: 'domcontentloaded' }); const namespaceForm = page.locator('.apollo-container:not(.hidden) form[name="namespaceForm"]'); await namespaceForm.waitFor({ state: 'visible', timeout: 60000 }); await page.locator('.apollo-container:not(.hidden) button[ng-click="switchType(\'link\')"]').first().click(); await page.waitForFunction(() => { return typeof window.$ === 'function' && window.$('#namespaces').length > 0 && window.$('#namespaces').data('select2'); }); await page.waitForFunction((targetNamespaces) => { const select = window.$('#namespaces'); if (!select.length || !select.data('select2')) { return false; } targetNamespaces.forEach((targetNamespace) => { if (select.find(`option[value="${targetNamespace}"]`).length === 0) { select.append(new Option(targetNamespace, targetNamespace, true, true)); } }); select.val(targetNamespaces).trigger('change'); return true; }, namespaceNames); const linkNamespaceResponse = waitForApiResponseByFragments( page, 'POST', [`/apps/${appId}/namespaces`] ); await Promise.all([ linkNamespaceResponse, namespaceForm.locator('button[type="submit"]').click(), ]); await page.locator('div.row.text-center h3').waitFor({ state: 'visible', timeout: 30000 }); await page.waitForURL((url) => { const href = url.toString(); return href.includes('/namespace/role.html') || href.includes('/config.html'); }, { timeout: 30000 }); } async function switchNamespaceBranch(page, namespaceName, targetBranchType = 'master') { const namespacePanel = await locateNamespacePanel(page, namespaceName); if (targetBranchType === 'master') { await namespacePanel.locator('a[ng-click="switchBranch(\'master\', true)"]').first().click(); await namespacePanel.locator('[ng-click="publish(namespace)"]:visible').first().waitFor({ state: 'visible', timeout: 30000, }); return; } await namespacePanel.locator('a[ng-click="switchBranch(namespace.branchName, true)"]').first().click(); await namespacePanel.locator('[ng-click="publish(namespace.branch)"]:visible').first().waitFor({ state: 'visible', timeout: 30000, }); } async function createBranchViaUi(page, appId, options = {}) { const { env = DEFAULT_ENV, clusterName = DEFAULT_CLUSTER, namespaceName = DEFAULT_NAMESPACE, } = options; const namespacePanel = await locateNamespacePanel(page, namespaceName); await namespacePanel.locator('[ng-click="preCreateBranch(namespace)"]:visible').first().click(); await expect(page.locator('#createBranchTips')).toBeVisible({ timeout: 30000 }); const createBranchResponse = waitForApiResponseByFragments( page, 'POST', [ `/apps/${appId}/envs/${env}/clusters/${clusterName}/namespaces/${namespaceName}/branches`, ] ); await Promise.all([ createBranchResponse, page.locator('#createBranchTips .modal-footer button.btn-primary').first().click(), ]); const response = await createBranchResponse; const body = await response.json().catch(() => null); await expect(page.locator('.toast-success').first()).toBeVisible({ timeout: 30000 }); await namespacePanel.locator('a[ng-click="switchBranch(namespace.branchName, true)"]').first().waitFor({ state: 'visible', timeout: 30000, }); return body?.clusterName; } async function addGrayRuleViaUi(page, appId, options = {}) { const { env = DEFAULT_ENV, clusterName = DEFAULT_CLUSTER, namespaceName = DEFAULT_NAMESPACE, clientAppId = appId, clientIpList = ['1.1.1.1'], clientLabelList = [], } = options; await switchNamespaceBranch(page, namespaceName, 'gray'); const namespacePanel = await locateNamespacePanel(page, namespaceName); await namespacePanel.locator('li[ng-click="switchView(namespace.branch, \'rule\')"]').first().click(); await namespacePanel.locator('[ng-click="addRuleItem(namespace.branch)"]:visible').first().click(); await expect(page.locator('#rulesModal')).toBeVisible({ timeout: 30000 }); const clientAppInput = page.locator('#rulesModal input[type="text"]').first(); if (await clientAppInput.isVisible().catch(() => false)) { await clientAppInput.fill(clientAppId); } if (clientIpList.length > 0) { const manualInputToggle = page.locator('#rulesModal a[ng-click*="manual"]').first(); if (await manualInputToggle.isVisible().catch(() => false)) { await manualInputToggle.click(); } const ipTextarea = page.locator('#rulesModal textarea[rows="3"]').first(); await ipTextarea.waitFor({ state: 'visible', timeout: 30000 }); await ipTextarea.fill(clientIpList.join(',')); await page.locator('#rulesModal button.add-rule').first().click(); } if (clientLabelList.length > 0) { await page.locator('#rulesModal textarea[rows="1"]').fill(clientLabelList.join(',')); await page.locator('#rulesModal button.add-rule').nth(1).click(); } const updateGrayRulesResponse = waitForApiResponseByFragments( page, 'PUT', [ `/apps/${appId}/envs/${env}/clusters/${clusterName}/namespaces/${namespaceName}/branches/`, '/rules', ] ); await Promise.all([ updateGrayRulesResponse, page.locator('#rulesModal .modal-footer button.btn-primary').click(), ]); await expect(page.locator('#rulesModal')).toBeHidden({ timeout: 30000 }); } async function grayPublishNamespaceViaUi(page, appId, releaseName, comment, options = {}) { const { env = DEFAULT_ENV, clusterName = DEFAULT_CLUSTER, namespaceName = DEFAULT_NAMESPACE, } = options; await switchNamespaceBranch(page, namespaceName, 'gray'); const namespacePanel = await locateNamespacePanel(page, namespaceName); await namespacePanel.locator('[ng-click="publish(namespace.branch)"]:visible').first().click(); await expect(page.locator('#releaseModal')).toBeVisible({ timeout: 30000 }); await page.fill('#releaseModal input[name="releaseName"]', releaseName); await page.fill('#releaseModal textarea[name="comment"]', comment); const grayReleaseResponse = waitForApiResponseByFragments( page, 'POST', [ `/apps/${appId}/envs/${env}/clusters/${clusterName}/namespaces/${namespaceName}/branches/`, '/releases', ] ); await Promise.all([ grayReleaseResponse, page.click('#releaseModal button.btn-primary[type="submit"]'), ]); await expect(page.locator('.toast-success').first()).toBeVisible({ timeout: 30000 }); } async function mergeAndPublishNamespaceViaUi(page, appId, releaseName, comment, options = {}) { const { env = DEFAULT_ENV, clusterName = DEFAULT_CLUSTER, namespaceName = DEFAULT_NAMESPACE, deleteBranch = true, } = options; await switchNamespaceBranch(page, namespaceName, 'gray'); const namespacePanel = await locateNamespacePanel(page, namespaceName); await namespacePanel.locator('[ng-click="mergeAndPublish(namespace.branch)"]:visible').first().click(); await expect(page.locator('#mergeAndPublishModal')).toBeVisible({ timeout: 30000 }); if (!deleteBranch) { await page.locator('#mergeAndPublishModal input[name="deleteBranch"]').nth(1).check(); } await page.locator('#mergeAndPublishModal .modal-footer button.btn-primary').click(); await expect(page.locator('#releaseModal')).toBeVisible({ timeout: 30000 }); await page.fill('#releaseModal input[name="releaseName"]', releaseName); await page.fill('#releaseModal textarea[name="comment"]', comment); const mergeReleaseResponse = waitForApiResponseByFragments( page, 'POST', [ `/apps/${appId}/envs/${env}/clusters/${clusterName}/namespaces/${namespaceName}/branches/`, '/merge', ] ); await Promise.all([ mergeReleaseResponse, page.click('#releaseModal button.btn-primary[type="submit"]'), ]); await expect(page.locator('.toast-success').first()).toBeVisible({ timeout: 30000 }); } async function discardGrayBranchViaUi(page, appId, options = {}) { const { env = DEFAULT_ENV, clusterName = DEFAULT_CLUSTER, namespaceName = DEFAULT_NAMESPACE, } = options; await switchNamespaceBranch(page, namespaceName, 'gray'); const namespacePanel = await locateNamespacePanel(page, namespaceName); await namespacePanel.locator('[ng-click="preDeleteBranch(namespace.branch)"]:visible').first().click(); await expect(page.locator('#deleteBranchDialog')).toBeVisible({ timeout: 30000 }); const deleteBranchResponse = waitForApiResponseByFragments( page, 'DELETE', [ `/apps/${appId}/envs/${env}/clusters/${clusterName}/namespaces/${namespaceName}/branches/`, ] ); await Promise.all([ deleteBranchResponse, page.click('#deleteBranchDialog button.btn-danger'), ]); await expect(page.locator('.toast-success').first()).toBeVisible({ timeout: 30000 }); } async function createNamespaceItem(page, appId, itemKey, value, comment, options = {}) { const { env = DEFAULT_ENV, clusterName = DEFAULT_CLUSTER, namespaceName = DEFAULT_NAMESPACE, } = options; const namespacePanel = await locateNamespacePanel(page, namespaceName); const createItemButton = namespacePanel.locator('[ng-click="createItem(namespace)"]:visible').first(); await createItemButton.waitFor({ state: 'visible', timeout: 30000 }); await createItemButton.click(); await expect(page.locator('#itemModal')).toBeVisible(); await page.fill('#itemModal input[name="key"]', itemKey); await page.fill('#itemModal textarea[name="value"]', value); await page.fill('#itemModal textarea[name="comment"]', comment); const createItemResponse = waitForApiResponse( page, 'POST', `/apps/${appId}/envs/${env}/clusters/${clusterName}/namespaces/${namespaceName}/item` ); await Promise.all([ createItemResponse, page.click('#itemModal button[type="submit"]'), ]); await expect(page.locator('#itemModal')).toBeHidden(); } async function createBranchNamespaceItem(page, appId, itemKey, value, comment, options = {}) { const { env = DEFAULT_ENV, namespaceName = DEFAULT_NAMESPACE, } = options; await switchNamespaceBranch(page, namespaceName, 'gray'); const namespacePanel = await locateNamespacePanel(page, namespaceName); const createItemButton = namespacePanel.locator('[ng-click="createItem(namespace.branch)"]:visible').first(); await createItemButton.waitFor({ state: 'visible', timeout: 30000 }); await createItemButton.click(); await expect(page.locator('#itemModal')).toBeVisible(); await page.fill('#itemModal input[name="key"]', itemKey); await page.fill('#itemModal textarea[name="value"]', value); await page.fill('#itemModal textarea[name="comment"]', comment); const createBranchItemResponse = page.waitForResponse( (response) => response.request().method() === 'POST' && response.url().includes(`/apps/${appId}/envs/${env}/clusters/`) && response.url().includes(`/namespaces/${namespaceName}/item`) && isExpectedStatus(response.status(), DEFAULT_SUCCESS_STATUSES), { timeout: 90000 } ); await Promise.all([ createBranchItemResponse, page.click('#itemModal button[type="submit"]'), ]); await expect(page.locator('#itemModal')).toBeHidden(); } async function updateNamespaceItem(page, appId, itemKey, value, comment, options = {}) { const { env = DEFAULT_ENV, clusterName = DEFAULT_CLUSTER, namespaceName = DEFAULT_NAMESPACE, } = options; const namespacePanel = await locateNamespacePanel(page, namespaceName); const itemRow = namespacePanel.locator('tr').filter({ hasText: itemKey }).first(); await itemRow.waitFor({ state: 'visible', timeout: 30000 }); await itemRow.locator('[ng-click="editItem(namespace, config.item)"]:visible').first().click(); await expect(page.locator('#itemModal')).toBeVisible(); await page.fill('#itemModal textarea[name="value"]', value); await page.fill('#itemModal textarea[name="comment"]', comment); const updateResponse = page.waitForResponse( (response) => { const method = response.request().method(); return ['PUT', 'POST'].includes(method) && response.url().includes(`/apps/${appId}/envs/${env}/clusters/${clusterName}/namespaces/${namespaceName}/item`) && isExpectedStatus(response.status(), DEFAULT_SUCCESS_STATUSES); }, { timeout: 90000 } ); await Promise.all([ updateResponse, page.click('#itemModal button[type="submit"]'), ]); await expect(page.locator('#itemModal')).toBeHidden(); } async function publishNamespace(page, appId, releaseName, comment, options = {}) { const { env = DEFAULT_ENV, clusterName = DEFAULT_CLUSTER, namespaceName = DEFAULT_NAMESPACE, } = options; const namespacePanel = await locateNamespacePanel(page, namespaceName); await namespacePanel.locator('[ng-click="publish(namespace)"]:visible').first().click(); await expect(page.locator('#releaseModal')).toBeVisible(); await page.fill('#releaseModal input[name="releaseName"]', releaseName); await page.fill('#releaseModal textarea[name="comment"]', comment); const releaseResponse = waitForApiResponse( page, 'POST', `/apps/${appId}/envs/${env}/clusters/${clusterName}/namespaces/${namespaceName}/releases` ); await Promise.all([ releaseResponse, page.click('#releaseModal button.btn-primary[type="submit"]'), ]); await expect(page.locator('.toast-success').first()).toBeVisible({ timeout: 30000 }); } async function loadNamespaceViaPortalApi(page, appId, options = {}) { const { env = DEFAULT_ENV, clusterName = DEFAULT_CLUSTER, namespaceName = DEFAULT_NAMESPACE, } = options; const response = await page.context().request.get( `/apps/${encodePathSegment(appId)}/envs/${encodePathSegment(env)}/clusters/${encodePathSegment(clusterName)}/namespaces/${encodePathSegment(namespaceName)}` ); expect(response.status()).toBe(200); return response.json(); } async function modifyNamespaceTextViaPortalApi(page, appId, configText, options = {}) { const { env = DEFAULT_ENV, clusterName = DEFAULT_CLUSTER, namespaceName = DEFAULT_NAMESPACE, format = 'properties', } = options; const namespace = await loadNamespaceViaPortalApi(page, appId, { env, clusterName, namespaceName }); const namespaceId = namespace?.baseInfo?.id; expect(namespaceId).toBeTruthy(); const response = await page.context().request.put( `/apps/${encodePathSegment(appId)}/envs/${encodePathSegment(env)}/clusters/${encodePathSegment(clusterName)}/namespaces/${encodePathSegment(namespaceName)}/items`, { data: { namespaceId, format, configText, }, } ); expect([200, 204]).toContain(response.status()); } async function createBranchViaPortalApi(page, appId, options = {}) { const { env = DEFAULT_ENV, clusterName = DEFAULT_CLUSTER, namespaceName = DEFAULT_NAMESPACE, } = options; const response = await page.context().request.post( `/apps/${encodePathSegment(appId)}/envs/${encodePathSegment(env)}/clusters/${encodePathSegment(clusterName)}/namespaces/${encodePathSegment(namespaceName)}/branches`, { data: {} } ); expect(response.status()).toBe(200); const body = await response.json(); expect(body?.clusterName).toBeTruthy(); return body.clusterName; } async function updateGrayRulesViaPortalApi(page, appId, branchName, options = {}) { const { env = DEFAULT_ENV, clusterName = DEFAULT_CLUSTER, namespaceName = DEFAULT_NAMESPACE, clientAppId = appId, clientIpList = ['1.1.1.1'], clientLabelList = [], } = options; const response = await page.context().request.put( `/apps/${encodePathSegment(appId)}/envs/${encodePathSegment(env)}/clusters/${encodePathSegment(clusterName)}/namespaces/${encodePathSegment(namespaceName)}/branches/${encodePathSegment(branchName)}/rules`, { data: { appId, clusterName, namespaceName, branchName, ruleItems: [ { clientAppId, clientIpList, clientLabelList, }, ], }, } ); expect([200, 204]).toContain(response.status()); } async function publishGrayReleaseViaPortalApi(page, appId, branchName, releaseTitle, releaseComment, options = {}) { const { env = DEFAULT_ENV, clusterName = DEFAULT_CLUSTER, namespaceName = DEFAULT_NAMESPACE, } = options; const response = await page.context().request.post( `/apps/${encodePathSegment(appId)}/envs/${encodePathSegment(env)}/clusters/${encodePathSegment(clusterName)}/namespaces/${encodePathSegment(namespaceName)}/branches/${encodePathSegment(branchName)}/releases`, { data: { releaseTitle, releaseComment, isEmergencyPublish: false, }, } ); expect([200, 201]).toContain(response.status()); } async function fetchApolloConfigFromConfigService(request, appId, namespaceName, options = {}) { const { clusterName = DEFAULT_CLUSTER, dataCenter, ip = '127.0.0.1', label, } = options; const configBaseUrl = resolveConfigServiceBaseUrl(); const response = await request.get( `${configBaseUrl}/configs/${encodePathSegment(appId)}/${encodePathSegment(clusterName)}/${encodePathSegment(namespaceName)}`, { params: { ...(dataCenter ? { dataCenter } : {}), ...(ip ? { ip } : {}), ...(label ? { label } : {}), }, } ); let body = null; if (response.ok()) { body = await response.json(); } return { response, body }; } async function waitForApolloConfigValue(request, appId, namespaceName, key, expectedValue, options = {}) { const timeoutMs = options.timeoutMs || 60000; const intervalMs = options.intervalMs || 1500; const deadline = Date.now() + timeoutMs; let lastStatus = 0; let lastValue; while (Date.now() < deadline) { const { response, body } = await fetchApolloConfigFromConfigService(request, appId, namespaceName, options); lastStatus = response.status(); if (response.ok() && body?.configurations) { lastValue = body.configurations[key]; if (`${lastValue}` === `${expectedValue}`) { return body; } } await sleep(intervalMs); } throw new Error( `Timed out waiting for config value. appId=${appId}, namespace=${namespaceName}, key=${key}, expected=${expectedValue}, actual=${lastValue}, status=${lastStatus}` ); } async function fetchRawConfigFromConfigService(request, appId, namespaceName, options = {}) { const { clusterName = DEFAULT_CLUSTER, dataCenter, ip = '127.0.0.1', label, } = options; const configBaseUrl = resolveConfigServiceBaseUrl(); const response = await request.get( `${configBaseUrl}/configfiles/raw/${encodePathSegment(appId)}/${encodePathSegment(clusterName)}/${encodePathSegment(namespaceName)}`, { params: { ...(dataCenter ? { dataCenter } : {}), ...(ip ? { ip } : {}), ...(label ? { label } : {}), }, } ); const text = await response.text(); return { response, text }; } async function fetchNotificationsV2FromConfigService(request, appId, notifications, options = {}) { const { clusterName = DEFAULT_CLUSTER, dataCenter, ip, requestTimeoutMs = 10000, } = options; const configBaseUrl = resolveConfigServiceBaseUrl(); const response = await request.get( `${configBaseUrl}/notifications/v2`, { params: { appId, cluster: clusterName, notifications: JSON.stringify(notifications), ...(dataCenter ? { dataCenter } : {}), ...(ip ? { ip } : {}), }, timeout: requestTimeoutMs, } ); const body = await response.json().catch(() => null); return { response, body }; } async function waitForNotificationV2Update(request, appId, namespaceName, previousNotificationId, options = {}) { const timeoutMs = options.timeoutMs || 60000; const intervalMs = options.intervalMs || 1000; const requestTimeoutMs = options.requestTimeoutMs || 10000; const deadline = Date.now() + timeoutMs; let lastStatus = 0; let lastBody = null; while (Date.now() < deadline) { try { const { response, body } = await fetchNotificationsV2FromConfigService( request, appId, [ { namespaceName, notificationId: previousNotificationId, }, ], { ...options, requestTimeoutMs, } ); lastStatus = response.status(); lastBody = body; if (response.status() === 200 && Array.isArray(body)) { const matched = body.find((item) => { if (!item || item.notificationId === undefined || item.notificationId === null) { return false; } return normalizeNotificationNamespace(item.namespaceName) === normalizeNotificationNamespace(namespaceName); }); if (matched && Number(matched.notificationId) > Number(previousNotificationId)) { return matched; } } } catch (error) { lastBody = error?.message || `${error}`; } await sleep(intervalMs); } throw new Error( `Timed out waiting for notifications/v2 update. appId=${appId}, namespace=${namespaceName}, previousNotificationId=${previousNotificationId}, lastStatus=${lastStatus}, lastBody=${JSON.stringify(lastBody)}` ); } async function waitForRawConfig(request, appId, namespaceName, predicate, options = {}) { const timeoutMs = options.timeoutMs || 60000; const intervalMs = options.intervalMs || 1500; const deadline = Date.now() + timeoutMs; let lastStatus = 0; let lastText = ''; while (Date.now() < deadline) { const { response, text } = await fetchRawConfigFromConfigService(request, appId, namespaceName, options); lastStatus = response.status(); lastText = text; if (response.ok() && predicate(text)) { return text; } await sleep(intervalMs); } throw new Error( `Timed out waiting for raw config. appId=${appId}, namespace=${namespaceName}, status=${lastStatus}, body=${lastText.slice(0, 200)}` ); } async function rollbackLatestRelease(page, options = {}) { const { namespaceName = DEFAULT_NAMESPACE, env = DEFAULT_ENV } = options; const namespacePanel = await locateNamespacePanel(page, namespaceName); await namespacePanel.locator('[ng-click="rollback(namespace)"]:visible').first().click(); await expect(page.locator('#rollbackModal')).toBeVisible({ timeout: 30000 }); await page.click('#rollbackModal button[type="submit"]'); await expect(page.locator('#rollbackAlertDialog')).toBeVisible({ timeout: 30000 }); const rollbackResponse = waitForApiResponse(page, 'PUT', `/envs/${env}/releases/`); await Promise.all([ rollbackResponse, page.click('#rollbackAlertDialog button.btn-danger'), ]); await expect(page.locator('.toast-success').first()).toBeVisible({ timeout: 30000 }); } module.exports = { USERNAME, generateUniqueId, login, selectOrganization, selectUserByKeyword, createAppViaUiWithUserSelection, waitForApiResponse, waitForApiCall, waitForApiResponseByFragments, createAppViaUi, submitAppCreation, submitClusterCreation, createClusterViaUi, submitNamespaceCreation, createNamespaceViaUi, clearNamespaceRoleViaPortalApi, assignNamespaceRoleViaUi, assignNamespaceRoleViaUiBySearch, revokeNamespaceRoleViaUi, openConfigPage, switchNamespaceView, editNamespaceTextViaUi, linkPublicNamespacesViaUi, switchNamespaceBranch, createBranchViaUi, addGrayRuleViaUi, grayPublishNamespaceViaUi, mergeAndPublishNamespaceViaUi, discardGrayBranchViaUi, createNamespaceItem, createBranchNamespaceItem, updateNamespaceItem, publishNamespace, loadNamespaceViaPortalApi, modifyNamespaceTextViaPortalApi, createBranchViaPortalApi, updateGrayRulesViaPortalApi, publishGrayReleaseViaPortalApi, fetchApolloConfigFromConfigService, waitForApolloConfigValue, fetchRawConfigFromConfigService, fetchNotificationsV2FromConfigService, waitForNotificationV2Update, waitForRawConfig, toPropertiesText, rollbackLatestRelease, }; ================================================ FILE: e2e/portal-e2e/tests/portal-auth-matrix.spec.js ================================================ /* * Copyright 2026 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ const { test, expect } = require('@playwright/test'); const { generateUniqueId, createAppViaUiWithUserSelection, assignNamespaceRoleViaUiBySearch, revokeNamespaceRoleViaUi, } = require('./helpers/portal-helpers'); const { MODE_LDAP, MODE_OIDC, resolveAuthMode, getAuthUsers, loginByMode, expectLoginFailureByMode, warmUpOidcSecondaryUser, } = require('./helpers/auth-helpers'); const mode = resolveAuthMode(); const users = getAuthUsers(); function assertSelectionText(selectionText) { expect(selectionText).toContain(users.secondaryUser); expect(selectionText).toContain(users.secondaryEmail); if (mode === MODE_LDAP && users.secondaryDisplayName) { expect(selectionText).toContain(users.secondaryDisplayName); } } test.describe.serial(`@auth-matrix portal login matrix (${mode})`, () => { let createdAppId = ''; test.beforeAll(async ({ browser }) => { if (mode === MODE_OIDC) { await warmUpOidcSecondaryUser(browser); } }); test('login succeeds for allowed user @auth-matrix', async ({ page }) => { await loginByMode(page, { username: users.successUser, password: users.successPassword, }); await expect(page).toHaveURL(/127\.0\.0\.1:8070|localhost:8070/); }); test('login fails for non-existent user @auth-matrix', async ({ page }) => { await expectLoginFailureByMode(page, { username: users.invalidUser, password: users.invalidPassword, }); }); test('login fails for wrong password @auth-matrix', async ({ page }) => { await expectLoginFailureByMode(page, { username: users.successUser, password: users.wrongPassword, }); }); test('ldap blocked user is rejected by group filter @auth-matrix', async ({ page }) => { test.skip(mode !== MODE_LDAP, 'LDAP-only scenario'); await expectLoginFailureByMode(page, { username: users.blockedUser, password: users.blockedPassword, }); }); test('create app can search and select secondary user with display fields @auth-matrix', async ({ page, }) => { createdAppId = generateUniqueId('auth'); await loginByMode(page, { username: users.successUser, password: users.successPassword, }); const { ownerSelectionText, adminSelectionText } = await createAppViaUiWithUserSelection( page, createdAppId, { ownerKeyword: users.secondaryUser, adminKeyword: users.secondaryUser, } ); assertSelectionText(ownerSelectionText); assertSelectionText(adminSelectionText); }); test('namespace role assignment supports searching secondary user @auth-matrix', async ({ page }) => { expect(createdAppId).toBeTruthy(); await loginByMode(page, { username: users.secondaryUser, password: users.secondaryPassword || users.successPassword, }); const modifyRoleResult = await assignNamespaceRoleViaUiBySearch( page, createdAppId, 'application', { roleType: 'ModifyNamespace', userKeyword: users.secondaryUser, } ); assertSelectionText(modifyRoleResult.selectedUserText); await revokeNamespaceRoleViaUi(page, createdAppId, 'application', { roleType: 'ModifyNamespace', userId: modifyRoleResult.selectedUserId, ...(modifyRoleResult.targetEnv ? { env: modifyRoleResult.targetEnv } : {}), }); const releaseRoleResult = await assignNamespaceRoleViaUiBySearch( page, createdAppId, 'application', { roleType: 'ReleaseNamespace', userKeyword: users.secondaryUser, } ); assertSelectionText(releaseRoleResult.selectedUserText); await revokeNamespaceRoleViaUi(page, createdAppId, 'application', { roleType: 'ReleaseNamespace', userId: releaseRoleResult.selectedUserId, ...(releaseRoleResult.targetEnv ? { env: releaseRoleResult.targetEnv } : {}), }); }); }); ================================================ FILE: e2e/portal-e2e/tests/portal-configservice.spec.js ================================================ /* * Copyright 2026 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ const { test, expect } = require('@playwright/test'); const { generateUniqueId, login, createAppViaUi, createNamespaceViaUi, openConfigPage, createNamespaceItem, updateNamespaceItem, publishNamespace, rollbackLatestRelease, createBranchViaPortalApi, modifyNamespaceTextViaPortalApi, updateGrayRulesViaPortalApi, publishGrayReleaseViaPortalApi, waitForApolloConfigValue, waitForRawConfig, waitForNotificationV2Update, toPropertiesText, } = require('./helpers/portal-helpers'); test.describe.serial('@regression Apollo Portal to ConfigService full chain', () => { test('published, gray published and rolled back configs are readable from config service @regression', async ({ page, request }) => { const appId = generateUniqueId('e2e-cfg-'); const configKey = generateUniqueId('timeout_'); const nonGrayClientIp = '2.2.2.2'; const grayClientIp = '1.1.1.1'; let notificationId = -1; await login(page); await createAppViaUi(page, appId); await openConfigPage(page, appId); await createNamespaceItem(page, appId, configKey, '100', 'e2e config service first value'); await publishNamespace(page, appId, generateUniqueId('release_'), 'e2e config service first publish'); await waitForApolloConfigValue(request, appId, 'application', configKey, '100', { ip: nonGrayClientIp, }); const firstNotification = await waitForNotificationV2Update( request, appId, 'application', notificationId, { ip: nonGrayClientIp } ); notificationId = Number(firstNotification.notificationId); await openConfigPage(page, appId); await updateNamespaceItem(page, appId, configKey, '200', 'e2e config service second value'); await publishNamespace(page, appId, generateUniqueId('release_'), 'e2e config service second publish'); await waitForApolloConfigValue(request, appId, 'application', configKey, '200', { ip: nonGrayClientIp, }); const secondNotification = await waitForNotificationV2Update( request, appId, 'application', notificationId, { ip: nonGrayClientIp } ); expect(Number(secondNotification.notificationId)).toBeGreaterThan(notificationId); notificationId = Number(secondNotification.notificationId); const branchName = await createBranchViaPortalApi(page, appId, { namespaceName: 'application', }); await modifyNamespaceTextViaPortalApi( page, appId, toPropertiesText({ [configKey]: '300' }), { clusterName: branchName, namespaceName: 'application', format: 'properties', } ); await updateGrayRulesViaPortalApi(page, appId, branchName, { namespaceName: 'application', clientAppId: appId, clientIpList: [grayClientIp], }); await publishGrayReleaseViaPortalApi( page, appId, branchName, generateUniqueId('gray_'), 'e2e gray publish', { namespaceName: 'application' } ); await waitForApolloConfigValue(request, appId, 'application', configKey, '300', { ip: grayClientIp, }); await waitForApolloConfigValue(request, appId, 'application', configKey, '200', { ip: nonGrayClientIp, }); const grayNotification = await waitForNotificationV2Update( request, appId, 'application', notificationId, { ip: grayClientIp } ); expect(Number(grayNotification.notificationId)).toBeGreaterThan(notificationId); notificationId = Number(grayNotification.notificationId); await openConfigPage(page, appId); await rollbackLatestRelease(page); await waitForApolloConfigValue(request, appId, 'application', configKey, '100', { ip: nonGrayClientIp, }); await waitForApolloConfigValue(request, appId, 'application', configKey, '300', { ip: grayClientIp, }); const rollbackNotification = await waitForNotificationV2Update( request, appId, 'application', notificationId, { ip: nonGrayClientIp } ); expect(Number(rollbackNotification.notificationId)).toBeGreaterThan(notificationId); }); test('properties, yaml and json namespaces are readable from config service @regression', async ({ page, request }) => { const appId = generateUniqueId('e2e-fmt-'); const propertiesNamespaceSeed = generateUniqueId('props_'); const yamlNamespaceSeed = generateUniqueId('yaml_'); const jsonNamespaceSeed = generateUniqueId('json_'); const propertiesKey = generateUniqueId('rate_'); const notificationIds = {}; await login(page); await createAppViaUi(page, appId); const propertiesNamespace = await createNamespaceViaUi(page, appId, propertiesNamespaceSeed, { format: 'properties', }); notificationIds[propertiesNamespace] = -1; const yamlNamespace = await createNamespaceViaUi(page, appId, yamlNamespaceSeed, { format: 'yaml', }); notificationIds[yamlNamespace] = -1; const jsonNamespace = await createNamespaceViaUi(page, appId, jsonNamespaceSeed, { format: 'json', }); notificationIds[jsonNamespace] = -1; await openConfigPage(page, appId, { namespaceName: propertiesNamespace }); await createNamespaceItem( page, appId, propertiesKey, '101', 'e2e properties namespace value', { namespaceName: propertiesNamespace } ); await publishNamespace( page, appId, generateUniqueId('release_'), 'e2e properties namespace publish', { namespaceName: propertiesNamespace } ); await waitForApolloConfigValue(request, appId, propertiesNamespace, propertiesKey, '101'); const propertiesNotification = await waitForNotificationV2Update( request, appId, propertiesNamespace, notificationIds[propertiesNamespace] ); notificationIds[propertiesNamespace] = Number(propertiesNotification.notificationId); const yamlText = 'limits:\n qps: 300\nenabled: true\n'; await modifyNamespaceTextViaPortalApi(page, appId, yamlText, { namespaceName: yamlNamespace, format: 'yaml', }); await openConfigPage(page, appId, { namespaceName: yamlNamespace }); await publishNamespace( page, appId, generateUniqueId('release_'), 'e2e yaml namespace publish', { namespaceName: yamlNamespace } ); const yamlRaw = await waitForRawConfig( request, appId, yamlNamespace, (raw) => raw.includes('qps: 300') && raw.includes('enabled: true') ); expect(yamlRaw).toContain('limits:'); const yamlNotification = await waitForNotificationV2Update( request, appId, yamlNamespace, notificationIds[yamlNamespace] ); notificationIds[yamlNamespace] = Number(yamlNotification.notificationId); const jsonText = '{"limits":{"qps":500},"enabled":true}'; await modifyNamespaceTextViaPortalApi(page, appId, jsonText, { namespaceName: jsonNamespace, format: 'json', }); await openConfigPage(page, appId, { namespaceName: jsonNamespace }); await publishNamespace( page, appId, generateUniqueId('release_'), 'e2e json namespace publish', { namespaceName: jsonNamespace } ); const jsonRaw = await waitForRawConfig( request, appId, jsonNamespace, (raw) => { try { const parsed = JSON.parse(raw); return parsed?.limits?.qps === 500 && parsed.enabled === true; } catch (error) { return false; } } ); const parsedJson = JSON.parse(jsonRaw); expect(parsedJson.limits.qps).toBe(500); expect(parsedJson.enabled).toBeTruthy(); const jsonNotification = await waitForNotificationV2Update( request, appId, jsonNamespace, notificationIds[jsonNamespace] ); notificationIds[jsonNamespace] = Number(jsonNotification.notificationId); expect(notificationIds[jsonNamespace]).toBeGreaterThan(-1); }); }); ================================================ FILE: e2e/portal-e2e/tests/portal-core.spec.js ================================================ /* * Copyright 2026 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ const { test, expect } = require('@playwright/test'); const { generateUniqueId, login, waitForApiResponse, createAppViaUi, openConfigPage, createNamespaceItem, updateNamespaceItem, publishNamespace, rollbackLatestRelease, } = require('./helpers/portal-helpers'); test.describe.serial('@smoke Apollo Portal config lifecycle', () => { let createdAppId = ''; let itemKey = ''; let firstReleaseName = ''; let secondReleaseName = ''; test('login flow works @smoke', async ({ page }) => { await login(page); await expect(page).not.toHaveURL(/login\.html/); }); test('create app flow works @smoke', async ({ page }) => { createdAppId = generateUniqueId('e2e'); await login(page); await createAppViaUi(page, createdAppId); }); test('create item and first release works @smoke', async ({ page }) => { expect(createdAppId).toBeTruthy(); itemKey = generateUniqueId('timeout_'); firstReleaseName = generateUniqueId('release_'); await login(page); await openConfigPage(page, createdAppId); await createNamespaceItem(page, createdAppId, itemKey, '100', 'portal smoke create item'); await publishNamespace(page, createdAppId, firstReleaseName, 'portal smoke first release'); }); test('update item and second release works @smoke', async ({ page }) => { expect(createdAppId).toBeTruthy(); expect(itemKey).toBeTruthy(); secondReleaseName = generateUniqueId('release_'); await login(page); await openConfigPage(page, createdAppId); await updateNamespaceItem(page, createdAppId, itemKey, '200', 'portal smoke update item'); await publishNamespace(page, createdAppId, secondReleaseName, 'portal smoke second release'); }); test('rollback latest release works @smoke', async ({ page }) => { expect(createdAppId).toBeTruthy(); await login(page); await openConfigPage(page, createdAppId); await rollbackLatestRelease(page); }); test('release history contains publish and rollback records @smoke', async ({ page }) => { expect(createdAppId).toBeTruthy(); await login(page); const historyResponsePromise = waitForApiResponse( page, 'GET', `/apps/${createdAppId}/envs/LOCAL/clusters/default/namespaces/application/releases/histories`, 200 ); await page.goto( `/config/history.html?#/appid=${createdAppId}&env=LOCAL&clusterName=default&namespaceName=application`, { waitUntil: 'domcontentloaded' } ); const historyResponse = await historyResponsePromise; const histories = await historyResponse.json(); expect(Array.isArray(histories)).toBeTruthy(); expect(histories.length).toBeGreaterThanOrEqual(3); expect(histories.some((history) => history.operation === 0 || history.operation === 5)).toBeTruthy(); expect(histories.some((history) => history.operation === 1 || history.operation === 6)).toBeTruthy(); expect(histories.some((history) => history.releaseTitle === firstReleaseName)).toBeTruthy(); expect(histories.some((history) => history.releaseTitle === secondReleaseName)).toBeTruthy(); await expect(page.locator('.release-history-list .media').first()).toBeVisible({ timeout: 30000 }); }); }); ================================================ FILE: e2e/portal-e2e/tests/portal-priority.spec.js ================================================ /* * Copyright 2026 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ const { test, expect } = require('@playwright/test'); const { USERNAME, generateUniqueId, login, createAppViaUi, createNamespaceViaUi, clearNamespaceRoleViaPortalApi, assignNamespaceRoleViaUi, revokeNamespaceRoleViaUi, openConfigPage, createNamespaceItem, createBranchNamespaceItem, updateNamespaceItem, publishNamespace, editNamespaceTextViaUi, linkPublicNamespacesViaUi, createBranchViaUi, addGrayRuleViaUi, grayPublishNamespaceViaUi, mergeAndPublishNamespaceViaUi, discardGrayBranchViaUi, modifyNamespaceTextViaPortalApi, waitForApolloConfigValue, toPropertiesText, } = require('./helpers/portal-helpers'); test.describe.serial('@regression Apollo Portal high-priority user-guide scenarios', () => { test('super admin can edit and release namespace without explicit namespace roles @regression', async ({ page, request, }) => { const appId = generateUniqueId('e2e-super-admin-'); const configKey = generateUniqueId('super_admin_key_'); const initialValue = '100'; const updatedValue = '200'; await login(page); await createAppViaUi(page, appId); await openConfigPage(page, appId); await createNamespaceItem(page, appId, configKey, initialValue, 'super admin baseline'); await publishNamespace(page, appId, generateUniqueId('release_'), 'super admin baseline release'); await waitForApolloConfigValue(request, appId, 'application', configKey, initialValue, { ip: '2.2.2.2', }); await clearNamespaceRoleViaPortalApi(page, appId, 'application', { roleType: 'ModifyNamespace', userId: USERNAME, }); await clearNamespaceRoleViaPortalApi(page, appId, 'application', { roleType: 'ReleaseNamespace', userId: USERNAME, }); await clearNamespaceRoleViaPortalApi(page, appId, 'application', { roleType: 'ModifyNamespace', userId: USERNAME, env: 'LOCAL', }); await clearNamespaceRoleViaPortalApi(page, appId, 'application', { roleType: 'ReleaseNamespace', userId: USERNAME, env: 'LOCAL', }); // Without super admin support in unified permission checks, the edit/publish operations // below return 403 after namespace roles are removed. await editNamespaceTextViaUi(page, appId, toPropertiesText({ [configKey]: updatedValue, })); await publishNamespace(page, appId, generateUniqueId('release_'), 'super admin release after role revoke'); await waitForApolloConfigValue(request, appId, 'application', configKey, updatedValue, { ip: '2.2.2.2', }); }); test('namespace role page supports grant and revoke operations @regression', async ({ page }) => { const appId = generateUniqueId('e2e-role-'); const namespaceSeed = generateUniqueId('role_ns_'); await login(page); await createAppViaUi(page, appId); const namespaceName = await createNamespaceViaUi(page, appId, namespaceSeed); const modifyRoleEnv = await assignNamespaceRoleViaUi(page, appId, namespaceName, { roleType: 'ModifyNamespace', userId: USERNAME, }); const releaseRoleEnv = await assignNamespaceRoleViaUi(page, appId, namespaceName, { roleType: 'ReleaseNamespace', userId: USERNAME, }); await revokeNamespaceRoleViaUi(page, appId, namespaceName, { roleType: 'ReleaseNamespace', userId: USERNAME, env: releaseRoleEnv, }); await revokeNamespaceRoleViaUi(page, appId, namespaceName, { roleType: 'ModifyNamespace', userId: USERNAME, env: modifyRoleEnv, }); }); test('text mode edit and publish are readable from config service @regression', async ({ page, request }) => { const appId = generateUniqueId('e2e-text-'); const configKey = generateUniqueId('text_key_'); await login(page); await createAppViaUi(page, appId); await openConfigPage(page, appId); await createNamespaceItem(page, appId, configKey, '100', 'priority text mode baseline'); await publishNamespace(page, appId, generateUniqueId('release_'), 'priority text mode baseline release'); await waitForApolloConfigValue(request, appId, 'application', configKey, '100', { ip: '2.2.2.2', }); await editNamespaceTextViaUi(page, appId, toPropertiesText({ [configKey]: '300', })); await publishNamespace(page, appId, generateUniqueId('release_'), 'priority text mode publish'); await waitForApolloConfigValue(request, appId, 'application', configKey, '300', { ip: '2.2.2.2', }); }); test('linked public namespace supports association and override @regression', async ({ page, request }) => { const providerAppId = generateUniqueId('e2e-pub-'); const consumerAppId = generateUniqueId('e2e-link-'); const publicNamespaceSeed = `pub${Date.now().toString().slice(-6)}`; const sharedKey = generateUniqueId('shared_'); const overrideKey = generateUniqueId('override_'); await login(page); await createAppViaUi(page, providerAppId); const publicNamespaceName = await createNamespaceViaUi(page, providerAppId, publicNamespaceSeed, { isPublic: true, format: 'properties', }); await openConfigPage(page, providerAppId, { namespaceName: publicNamespaceName }); await createNamespaceItem( page, providerAppId, sharedKey, 'provider-default', 'priority linked namespace shared value', { namespaceName: publicNamespaceName } ); await createNamespaceItem( page, providerAppId, overrideKey, 'provider-v1', 'priority linked namespace override baseline', { namespaceName: publicNamespaceName } ); await publishNamespace( page, providerAppId, generateUniqueId('release_'), 'priority public namespace release', { namespaceName: publicNamespaceName } ); await createAppViaUi(page, consumerAppId); await linkPublicNamespacesViaUi(page, consumerAppId, [publicNamespaceName]); await openConfigPage(page, consumerAppId, { namespaceName: publicNamespaceName }); await updateNamespaceItem( page, consumerAppId, overrideKey, 'consumer-v2', 'priority linked namespace override', { namespaceName: publicNamespaceName } ); await publishNamespace( page, consumerAppId, generateUniqueId('release_'), 'priority linked namespace release', { namespaceName: publicNamespaceName } ); await waitForApolloConfigValue(request, consumerAppId, publicNamespaceName, sharedKey, 'provider-default'); await waitForApolloConfigValue(request, consumerAppId, publicNamespaceName, overrideKey, 'consumer-v2'); }); test('grayscale ui supports create rule publish merge and discard @regression', async ({ page, request }) => { const appId = generateUniqueId('e2e-gray-'); const configKey = generateUniqueId('gray_key_'); const mergeKey = generateUniqueId('merge_key_'); const firstGrayValue = '300'; const mergePublishValue = '350'; const grayClientIp = '1.1.1.1'; const normalClientIp = '2.2.2.2'; await login(page); await createAppViaUi(page, appId); await openConfigPage(page, appId); await createNamespaceItem(page, appId, configKey, '100', 'priority grayscale baseline'); await publishNamespace(page, appId, generateUniqueId('release_'), 'priority grayscale baseline release'); const branchName = await createBranchViaUi(page, appId); expect(branchName).toBeTruthy(); await modifyNamespaceTextViaPortalApi( page, appId, toPropertiesText({ [configKey]: firstGrayValue }), { clusterName: branchName, namespaceName: 'application', format: 'properties', } ); await openConfigPage(page, appId); await addGrayRuleViaUi(page, appId, { namespaceName: 'application', clientAppId: appId, clientIpList: [grayClientIp], }); await grayPublishNamespaceViaUi( page, appId, generateUniqueId('gray_'), 'priority grayscale publish', { namespaceName: 'application' } ); await waitForApolloConfigValue(request, appId, 'application', configKey, firstGrayValue, { ip: grayClientIp, }); await waitForApolloConfigValue(request, appId, 'application', configKey, '100', { ip: normalClientIp, }); await openConfigPage(page, appId); await discardGrayBranchViaUi(page, appId, { namespaceName: 'application' }); await openConfigPage(page, appId); const mergeBranchName = await createBranchViaUi(page, appId, { namespaceName: 'application' }); expect(mergeBranchName).toBeTruthy(); await createBranchNamespaceItem( page, appId, mergeKey, mergePublishValue, 'priority grayscale merge branch value', { namespaceName: 'application' } ); await openConfigPage(page, appId); await mergeAndPublishNamespaceViaUi( page, appId, generateUniqueId('merge_'), 'priority grayscale merge and publish', { namespaceName: 'application', deleteBranch: true, } ); await waitForApolloConfigValue(request, appId, 'application', mergeKey, mergePublishValue, { ip: normalClientIp, }); await openConfigPage(page, appId); const createBranchButton = page.locator('.panel.namespace-panel:not(.hidden)').filter({ has: page.locator('b.namespace-name', { hasText: /^application$/i }), }).first().locator('[ng-click="preCreateBranch(namespace)"]:visible').first(); await expect(createBranchButton).toBeVisible({ timeout: 30000 }); }); }); ================================================ FILE: e2e/portal-e2e/tests/portal-regression.spec.js ================================================ /* * Copyright 2026 Apollo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ const { test, expect } = require('@playwright/test'); const { generateUniqueId, login, waitForApiResponse, createAppViaUi, submitAppCreation, submitClusterCreation, createClusterViaUi, submitNamespaceCreation, createNamespaceViaUi, openConfigPage, createNamespaceItem, publishNamespace, } = require('./helpers/portal-helpers'); test.describe.serial('@regression Apollo Portal extended scenarios', () => { test('duplicate app creation is rejected @regression', async ({ page }) => { const appId = generateUniqueId('e2e-dup-'); await login(page); await createAppViaUi(page, appId); const duplicateCreateResponse = await submitAppCreation(page, appId); expect(duplicateCreateResponse.status()).toBeGreaterThanOrEqual(400); await expect(page).toHaveURL(/app\.html/, { timeout: 30000 }); }); test('cluster and namespace pages support creation flow @regression', async ({ page }) => { const appId = generateUniqueId('e2e-reg-'); const clusterName = generateUniqueId('cluster_'); const namespaceName = generateUniqueId('ns_'); await login(page); await createAppViaUi(page, appId); await createClusterViaUi(page, appId, clusterName); const duplicateClusterResponse = await submitClusterCreation(page, appId, clusterName); expect(duplicateClusterResponse.status()).toBeGreaterThanOrEqual(400); await expect(page).toHaveURL(/cluster\.html/, { timeout: 30000 }); await createNamespaceViaUi(page, appId, namespaceName); const duplicateNamespaceResponse = await submitNamespaceCreation(page, appId, namespaceName); expect(duplicateNamespaceResponse.status()).toBeGreaterThanOrEqual(400); await expect(page).toHaveURL(/namespace\.html/, { timeout: 30000 }); }); test('config export and instance view paths are reachable @regression', async ({ page }) => { const appId = generateUniqueId('e2e-exp-'); const itemKey = generateUniqueId('qps_'); const releaseName = generateUniqueId('release_'); await login(page); await createAppViaUi(page, appId); await openConfigPage(page, appId); await createNamespaceItem(page, appId, itemKey, '100', 'portal regression item'); await publishNamespace(page, appId, releaseName, 'portal regression release'); await page.goto('/config_export.html', { waitUntil: 'domcontentloaded' }); await page.click('a[href="#app_config"]'); await page.fill('#app_config input[ng-model="cluster.appId"]', appId); await page.fill('#app_config input[ng-model="cluster.env"]', 'LOCAL'); await page.fill('#app_config input[ng-model="cluster.name"]', 'default'); await page.click('#app_config button.btn-info'); await expect(page.locator('#app_config h5').first()).toContainText(appId, { timeout: 30000 }); const downloadPromise = page.waitForEvent('download'); await page.click('#app_config a.btn.btn-primary[ng-click="exportAppConfig()"]'); const download = await downloadPromise; expect(download.suggestedFilename()).toContain(appId); await openConfigPage(page, appId); const byNamespaceResponse = waitForApiResponse(page, 'GET', '/envs/LOCAL/instances/by-namespace', 200); await page.locator('[ng-click="switchView(namespace, \'instance\')"]').first().click(); const response = await byNamespaceResponse; expect(response.status()).toBe(200); }); }); ================================================ FILE: mvnw ================================================ #!/bin/sh # ---------------------------------------------------------------------------- # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- # Maven2 Start Up Batch script # # Required ENV vars: # ------------------ # JAVA_HOME - location of a JDK home dir # # Optional ENV vars # ----------------- # M2_HOME - location of maven2's installed home dir # MAVEN_OPTS - parameters passed to the Java VM when running Maven # e.g. to debug Maven itself, use # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 # MAVEN_SKIP_RC - flag to disable loading of mavenrc files # ---------------------------------------------------------------------------- if [ -z "$MAVEN_SKIP_RC" ] ; then if [ -f /etc/mavenrc ] ; then . /etc/mavenrc fi if [ -f "$HOME/.mavenrc" ] ; then . "$HOME/.mavenrc" fi fi # OS specific support. $var _must_ be set to either true or false. cygwin=false; darwin=false; mingw=false case "`uname`" in CYGWIN*) cygwin=true ;; MINGW*) mingw=true;; Darwin*) darwin=true # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home # See https://developer.apple.com/library/mac/qa/qa1170/_index.html if [ -z "$JAVA_HOME" ]; then if [ -x "/usr/libexec/java_home" ]; then export JAVA_HOME="`/usr/libexec/java_home`" else export JAVA_HOME="/Library/Java/Home" fi fi ;; esac if [ -z "$JAVA_HOME" ] ; then if [ -r /etc/gentoo-release ] ; then JAVA_HOME=`java-config --jre-home` fi fi if [ -z "$M2_HOME" ] ; then ## resolve links - $0 may be a link to maven's home PRG="$0" # need this for relative symlinks while [ -h "$PRG" ] ; do ls=`ls -ld "$PRG"` link=`expr "$ls" : '.*-> \(.*\)$'` if expr "$link" : '/.*' > /dev/null; then PRG="$link" else PRG="`dirname "$PRG"`/$link" fi done saveddir=`pwd` M2_HOME=`dirname "$PRG"`/.. # make it fully qualified M2_HOME=`cd "$M2_HOME" && pwd` cd "$saveddir" # echo Using m2 at $M2_HOME fi # For Cygwin, ensure paths are in UNIX format before anything is touched if $cygwin ; then [ -n "$M2_HOME" ] && M2_HOME=`cygpath --unix "$M2_HOME"` [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` [ -n "$CLASSPATH" ] && CLASSPATH=`cygpath --path --unix "$CLASSPATH"` fi # For Mingw, ensure paths are in UNIX format before anything is touched if $mingw ; then [ -n "$M2_HOME" ] && M2_HOME="`(cd "$M2_HOME"; pwd)`" [ -n "$JAVA_HOME" ] && JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" # TODO classpath? fi if [ -z "$JAVA_HOME" ]; then javaExecutable="`which javac`" if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then # readlink(1) is not available as standard on Solaris 10. readLink=`which readlink` if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then if $darwin ; then javaHome="`dirname \"$javaExecutable\"`" javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" else javaExecutable="`readlink -f \"$javaExecutable\"`" fi javaHome="`dirname \"$javaExecutable\"`" javaHome=`expr "$javaHome" : '\(.*\)/bin'` JAVA_HOME="$javaHome" export JAVA_HOME fi fi fi if [ -z "$JAVACMD" ] ; then if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD="$JAVA_HOME/jre/sh/java" else JAVACMD="$JAVA_HOME/bin/java" fi else JAVACMD="`which java`" fi fi if [ ! -x "$JAVACMD" ] ; then echo "Error: JAVA_HOME is not defined correctly." >&2 echo " We cannot execute $JAVACMD" >&2 exit 1 fi if [ -z "$JAVA_HOME" ] ; then echo "Warning: JAVA_HOME environment variable is not set." fi CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher # traverses directory structure from process work directory to filesystem root # first directory with .mvn subdirectory is considered project base directory find_maven_basedir() { if [ -z "$1" ] then echo "Path not specified to find_maven_basedir" return 1 fi basedir="$1" wdir="$1" while [ "$wdir" != '/' ] ; do if [ -d "$wdir"/.mvn ] ; then basedir=$wdir break fi # workaround for JBEAP-8937 (on Solaris 10/Sparc) if [ -d "${wdir}" ]; then wdir=`cd "$wdir/.."; pwd` fi # end of workaround done echo "${basedir}" } # concatenates all lines of a file concat_lines() { if [ -f "$1" ]; then echo "$(tr -s '\n' ' ' < "$1")" fi } BASE_DIR=`find_maven_basedir "$(pwd)"` if [ -z "$BASE_DIR" ]; then exit 1; fi ########################################################################################## # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central # This allows using the maven wrapper in projects that prohibit checking in binary data. ########################################################################################## if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then if [ "$MVNW_VERBOSE" = true ]; then echo "Found .mvn/wrapper/maven-wrapper.jar" fi else if [ "$MVNW_VERBOSE" = true ]; then echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." fi jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" while IFS="=" read key value; do case "$key" in (wrapperUrl) jarUrl="$value"; break ;; esac done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" if [ "$MVNW_VERBOSE" = true ]; then echo "Downloading from: $jarUrl" fi wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" if command -v wget > /dev/null; then if [ "$MVNW_VERBOSE" = true ]; then echo "Found wget ... using wget" fi wget "$jarUrl" -O "$wrapperJarPath" elif command -v curl > /dev/null; then if [ "$MVNW_VERBOSE" = true ]; then echo "Found curl ... using curl" fi curl -o "$wrapperJarPath" "$jarUrl" else if [ "$MVNW_VERBOSE" = true ]; then echo "Falling back to using Java to download" fi javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" if [ -e "$javaClass" ]; then if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then if [ "$MVNW_VERBOSE" = true ]; then echo " - Compiling MavenWrapperDownloader.java ..." fi # Compiling the Java class ("$JAVA_HOME/bin/javac" "$javaClass") fi if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then # Running the downloader if [ "$MVNW_VERBOSE" = true ]; then echo " - Running MavenWrapperDownloader.java ..." fi ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") fi fi fi fi ########################################################################################## # End of extension ########################################################################################## export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} if [ "$MVNW_VERBOSE" = true ]; then echo $MAVEN_PROJECTBASEDIR fi MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" # For Cygwin, switch paths to Windows format before running java if $cygwin; then [ -n "$M2_HOME" ] && M2_HOME=`cygpath --path --windows "$M2_HOME"` [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` [ -n "$CLASSPATH" ] && CLASSPATH=`cygpath --path --windows "$CLASSPATH"` [ -n "$MAVEN_PROJECTBASEDIR" ] && MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` fi WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain exec "$JAVACMD" \ $MAVEN_OPTS \ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" ================================================ FILE: mvnw.cmd ================================================ @REM ---------------------------------------------------------------------------- @REM Licensed to the Apache Software Foundation (ASF) under one @REM or more contributor license agreements. See the NOTICE file @REM distributed with this work for additional information @REM regarding copyright ownership. The ASF licenses this file @REM to you under the Apache License, Version 2.0 (the @REM "License"); you may not use this file except in compliance @REM with the License. You may obtain a copy of the License at @REM @REM http://www.apache.org/licenses/LICENSE-2.0 @REM @REM Unless required by applicable law or agreed to in writing, @REM software distributed under the License is distributed on an @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY @REM KIND, either express or implied. See the License for the @REM specific language governing permissions and limitations @REM under the License. @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- @REM Maven2 Start Up Batch script @REM @REM Required ENV vars: @REM JAVA_HOME - location of a JDK home dir @REM @REM Optional ENV vars @REM M2_HOME - location of maven2's installed home dir @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven @REM e.g. to debug Maven itself, use @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files @REM ---------------------------------------------------------------------------- @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' @echo off @REM set title of command window title %0 @REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% @REM set %HOME% to equivalent of $HOME if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") @REM Execute a user defined script before this one if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre @REM check for pre script, once with legacy .bat ending and once with .cmd ending if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" :skipRcPre @setlocal set ERROR_CODE=0 @REM To isolate internal variables from possible post scripts, we use another setlocal @setlocal @REM ==== START VALIDATION ==== if not "%JAVA_HOME%" == "" goto OkJHome echo. echo Error: JAVA_HOME not found in your environment. >&2 echo Please set the JAVA_HOME variable in your environment to match the >&2 echo location of your Java installation. >&2 echo. goto error :OkJHome if exist "%JAVA_HOME%\bin\java.exe" goto init echo. echo Error: JAVA_HOME is set to an invalid directory. >&2 echo JAVA_HOME = "%JAVA_HOME%" >&2 echo Please set the JAVA_HOME variable in your environment to match the >&2 echo location of your Java installation. >&2 echo. goto error @REM ==== END VALIDATION ==== :init @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". @REM Fallback to current working directory if not found. set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir set EXEC_DIR=%CD% set WDIR=%EXEC_DIR% :findBaseDir IF EXIST "%WDIR%"\.mvn goto baseDirFound cd .. IF "%WDIR%"=="%CD%" goto baseDirNotFound set WDIR=%CD% goto findBaseDir :baseDirFound set MAVEN_PROJECTBASEDIR=%WDIR% cd "%EXEC_DIR%" goto endDetectBaseDir :baseDirNotFound set MAVEN_PROJECTBASEDIR=%EXEC_DIR% cd "%EXEC_DIR%" :endDetectBaseDir IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig @setlocal EnableExtensions EnableDelayedExpansion for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% :endReadAdditionalConfig SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" FOR /F "tokens=1,2 delims==" %%A IN (%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties) DO ( IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B ) @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central @REM This allows using the maven wrapper in projects that prohibit checking in binary data. if exist %WRAPPER_JAR% ( echo Found %WRAPPER_JAR% ) else ( echo Couldn't find %WRAPPER_JAR%, downloading it ... echo Downloading from: %DOWNLOAD_URL% powershell -Command "(New-Object Net.WebClient).DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')" echo Finished downloading %WRAPPER_JAR% ) @REM End of extension %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* if ERRORLEVEL 1 goto error goto end :error set ERROR_CODE=1 :end @endlocal & set ERROR_CODE=%ERROR_CODE% if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost @REM check for post script, once with legacy .bat ending and once with .cmd ending if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" :skipRcPost @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' if "%MAVEN_BATCH_PAUSE%" == "on" pause if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% exit /B %ERROR_CODE% ================================================ FILE: pom.xml ================================================ 4.0.0 com.ctrip.framework.apollo apollo ${revision} Apollo pom Configuration Center https://github.com/apolloconfig/apollo Apache License, Version 2.0 http://www.apache.org/licenses/LICENSE-2.0 https://github.com/apolloconfig/apollo scm:git:git@github.com:apolloconfig/apollo.git scm:git:ssh://git@github.com:apolloconfig/apollo.git GitHub Actions https://github.com/apolloconfig/apollo/actions github https://github.com/apolloconfig/apollo/issues apollo The Apollo Project Contributors apollo-config@googlegroups.com https://www.apolloconfig.com/ 3.0.0-SNAPSHOT 17 UTF-8 2.5.0 3.5.10 2025.0.1 3.18.0 32.0.0-jre 4.5.14 2.0.1 0.2.7 3.23.1-GA 1.4.0 2.2.37 2.2.37 3.3.0 3.10.1 2.8.2 3.0.1 2.5.2 0.8.8 3.2.2 3.4.0 3.3.2 3.2.1 3.2.5 apollo-build-sql-converter apollo-buildtools apollo-common apollo-biz apollo-configservice apollo-adminservice apollo-portal apollo-assembly apollo-audit com.ctrip.framework.apollo apollo-core ${apollo-java.version} com.ctrip.framework.apollo apollo-common ${project.version} com.ctrip.framework.apollo apollo-biz ${project.version} com.ctrip.framework.apollo apollo-buildtools ${project.version} com.ctrip.framework.apollo apollo-configservice ${project.version} com.ctrip.framework.apollo apollo-adminservice ${project.version} com.ctrip.framework.apollo apollo-portal ${project.version} com.ctrip.framework.apollo apollo-openapi ${apollo-java.version} com.ctrip.framework.apollo apollo-audit-annotation ${project.version} com.ctrip.framework.apollo apollo-audit-impl ${project.version} com.ctrip.framework.apollo apollo-audit-api ${project.version} com.ctrip.framework.apollo apollo-audit-spring-boot-starter ${project.version} com.google.guava guava ${guava.version} com.google.inject guice 5.0.1 org.apache.commons commons-lang3 ${common-lang3.version} org.openapitools jackson-databind-nullable ${jackson-databind-nullable.version} org.apache.httpcomponents httpclient ${httpclient4.version} io.swagger.core.v3 swagger-annotations ${swagger-annotations.version} io.swagger.core.v3 swagger-models ${swagger-models.version} com.github.stefanbirkner system-lambda 1.2.0 test org.springframework.boot spring-boot-dependencies ${spring-boot.version} pom import org.springframework.cloud spring-cloud-dependencies ${spring-cloud.version} pom import com.sun.jersey.contribs jersey-apache-client4 1.19.4 com.sun.mail jakarta.mail ${jakarta.mail.version} org.javassist javassist ${javassist.version} org.springframework.boot spring-boot-starter-test test spring-boot-starter org.springframework.boot org.awaitility awaitility test org.junit.vintage junit-vintage-engine test org.hamcrest hamcrest-core org.apache.maven.plugins maven-compiler-plugin ${maven-compiler-plugin.version} ${java.version} ${project.build.sourceEncoding} true org.apache.maven.plugins maven-surefire-plugin ${maven-surefire-plugin.version} org.apache.maven.plugins maven-source-plugin ${maven-source-plugin.version} attach-sources jar-no-fork org.apache.maven.plugins maven-jar-plugin ${maven-jar-plugin.version} maven-javadoc-plugin ${maven-javadoc-plugin.version} attach-javadoc jar none public UTF-8 UTF-8 UTF-8 http://docs.oracle.com/javase/7/docs/api org.apache.maven.plugins maven-war-plugin ${maven-war-plugin.version} org.apache.maven.plugins maven-install-plugin ${maven-install-plugin.version} org.apache.maven.plugins maven-deploy-plugin ${maven-deploy-plugin.version} org.apache.maven.plugins maven-gpg-plugin ${maven-gpg-plugin.version} --pinentry-mode loopback verify sign org.springframework.boot spring-boot-maven-plugin ${spring-boot.version} true false repackage org.codehaus.mojo findbugs-maven-plugin 3.0.3 true Max Low false org.codehaus.mojo cobertura-maven-plugin 2.7 ch.qos.logback logback-classic 1.3.12 org.apache.maven.plugins maven-assembly-plugin ${maven-assembly-plugin.version} org.codehaus.mojo versions-maven-plugin 2.2 pl.project13.maven git-commit-id-plugin 2.2.6 revision true yyyy-MM-dd'T'HH:mm:ssZ true ${project.build.outputDirectory}/apollo-git.properties false org.codehaus.mojo flatten-maven-plugin 1.1.0 true resolveCiFriendliesOnly flatten process-resources flatten flatten.clean clean clean org.apache.maven.plugins maven-compiler-plugin org.apache.maven.plugins maven-surefire-plugin false org.apache.maven.plugins maven-source-plugin org.apache.maven.plugins maven-war-plugin org.apache.maven.plugins maven-install-plugin org.apache.maven.plugins maven-deploy-plugin org.codehaus.mojo findbugs-maven-plugin org.codehaus.mojo versions-maven-plugin org.apache.maven.plugins maven-jar-plugin true true pl.project13.maven git-commit-id-plugin org.codehaus.mojo flatten-maven-plugin org.jacoco jacoco-maven-plugin ${maven-jacoco-plugin.version} prepare-agent prepare-agent com.diffplug.spotless spotless-maven-plugin 2.43.0 apollo-buildtools/style/eclipse-java-google-style.xml apollo-buildtools/style/license/apollo-license src/main/resources true **/*.yml **/*.yaml **/*.properties **/*.xml src/main/resources false **/*.yml **/*.yaml **/*.properties **/*.xml github github true nacos-discovery 0.2.12 1.2.83 com.alibaba.boot nacos-discovery-spring-boot-starter ${nacos.discovery.version} com.alibaba fastjson ${fastjson.version} Central Portal Snapshots central-portal-snapshots https://central.sonatype.com/repository/maven-snapshots/ false true always ================================================ FILE: scripts/build.bat ================================================ rem rem Copyright 2024 Apollo Authors rem rem Licensed under the Apache License, Version 2.0 (the "License"); rem you may not use this file except in compliance with the License. rem You may obtain a copy of the License at rem rem http://www.apache.org/licenses/LICENSE-2.0 rem rem Unless required by applicable law or agreed to in writing, software rem distributed under the License is distributed on an "AS IS" BASIS, rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. rem See the License for the specific language governing permissions and rem limitations under the License. rem @echo off rem apollo config db info set apollo_config_db_url="jdbc:mysql://localhost:3306/ApolloConfigDB?characterEncoding=utf8" set apollo_config_db_username="root" set apollo_config_db_password="" rem apollo portal db info set apollo_portal_db_url="jdbc:mysql://localhost:3306/ApolloPortalDB?characterEncoding=utf8" set apollo_portal_db_username="root" set apollo_portal_db_password="" rem meta server url, different environments should have different meta server addresses set dev_meta="http://localhost:8080" set fat_meta="http://someIp:8080" set uat_meta="http://anotherIp:8080" set pro_meta="http://yetAnotherIp:8080" set META_SERVERS_OPTS=-Ddev_meta=%dev_meta% -Dfat_meta=%fat_meta% -Duat_meta=%uat_meta% -Dpro_meta=%pro_meta% rem =============== Please do not modify the following content =============== rem go to script directory cd "%~dp0" cd .. rem package config-service and admin-service echo "==== starting to build config-service and admin-service ====" call mvn clean package -DskipTests -pl apollo-configservice,apollo-adminservice -am -Dapollo_profile=github -Dspring_datasource_url=%apollo_config_db_url% -Dspring_datasource_username=%apollo_config_db_username% -Dspring_datasource_password=%apollo_config_db_password% echo "==== building config-service and admin-service finished ====" echo "==== starting to build portal ====" call mvn clean package -DskipTests -pl apollo-portal -am -Dapollo_profile=github,auth -Dspring_datasource_url=%apollo_portal_db_url% -Dspring_datasource_username=%apollo_portal_db_username% -Dspring_datasource_password=%apollo_portal_db_password% %META_SERVERS_OPTS% echo "==== building portal finished ====" pause ================================================ FILE: scripts/build.sh ================================================ #!/bin/sh # # Copyright 2024 Apollo Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # apollo config db info apollo_config_db_url='jdbc:mysql://fill-in-the-correct-server:3306/ApolloConfigDB?characterEncoding=utf8' apollo_config_db_username='FillInCorrectUser' apollo_config_db_password='FillInCorrectPassword' # apollo portal db info apollo_portal_db_url='jdbc:mysql://fill-in-the-correct-server:3306/ApolloPortalDB?characterEncoding=utf8' apollo_portal_db_username='FillInCorrectUser' apollo_portal_db_password='FillInCorrectPassword' # meta server url, different environments should have different meta server addresses dev_meta=http://fill-in-dev-meta-server:8080 fat_meta=http://fill-in-fat-meta-server:8080 uat_meta=http://fill-in-uat-meta-server:8080 pro_meta=http://fill-in-pro-meta-server:8080 META_SERVERS_OPTS="-Ddev_meta=$dev_meta -Dfat_meta=$fat_meta -Duat_meta=$uat_meta -Dpro_meta=$pro_meta" # =============== Please do not modify the following content =============== # # go to script directory cd "${0%/*}" || exit cd .. # package config-service and admin-service echo "==== starting to build config-service and admin-service ====" mvn clean package -DskipTests -pl apollo-configservice,apollo-adminservice -am -Dapollo_profile=github -Dspring_datasource_url=$apollo_config_db_url -Dspring_datasource_username=$apollo_config_db_username -Dspring_datasource_password=$apollo_config_db_password echo "==== building config-service and admin-service finished ====" echo "==== starting to build portal ====" mvn clean package -DskipTests -pl apollo-portal -am -Dapollo_profile=github,auth -Dspring_datasource_url=$apollo_portal_db_url -Dspring_datasource_username=$apollo_portal_db_username -Dspring_datasource_password=$apollo_portal_db_password $META_SERVERS_OPTS echo "==== building portal finished ====" ================================================ FILE: scripts/openapi/bash/openapi-usage-example.sh ================================================ #!/bin/bash # # Copyright 2024 Apollo Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # title openapi-usage-example.sh # description show how to use openapi.sh # author wxq # date 2021-09-12 # Chinese reference website https://www.apolloconfig.com/#/zh/portal/apollo-open-api-platform # English reference website https://www.apolloconfig.com/#/en/portal/apollo-open-api-platform # export global variables export APOLLO_PORTAL_ADDRESS=http://106.54.227.205 export APOLLO_OPENAPI_TOKEN=284fe833cbaeecf2764801aa73965080b184fc88 export CURL_OPTIONS="" # load functions source openapi.sh # set up global environment variable APOLLO_APP_ID=openapi APOLLO_ENV=DEV APOLLO_CLUSTER=default APOLLO_USER=apollo ####################################### cluster ####################################### # get cluster printf "get cluster: env = '%s', app id = '%s', cluster = '%s'\n" ${APOLLO_ENV} ${APOLLO_APP_ID} ${APOLLO_CLUSTER} cluster_get ${APOLLO_ENV} ${APOLLO_APP_ID} ${APOLLO_CLUSTER} printf "\n\n" # create cluster. To forbid cluster xxx already exists, add timestamp to suffix temp_apollo_cluster="cluster-$(date +%s)" printf "create cluster: env = '%s', app id = '%s', cluster = '%s'\n" ${APOLLO_ENV} ${APOLLO_APP_ID} ${temp_apollo_cluster} cluster_create ${APOLLO_ENV} ${APOLLO_APP_ID} ${temp_apollo_cluster} ${APOLLO_USER} printf "\n\n" ####################################### end of cluster ####################################### ####################################### namespace ####################################### # create namespace temp_namespace_name="application-123" temp_namespace_format=yaml echo "create namespace: namespace name = '${temp_namespace_name}', app id = '${APOLLO_APP_ID}', format = '${temp_namespace_format}'" namespace_create ${APOLLO_APP_ID} ${temp_namespace_name} ${temp_format} false 'create by openapi, bash scripts' ${APOLLO_USER} printf "\n\n" ####################################### end of namespace ####################################### ####################################### item ####################################### # create an item, i.e a key value pair temp_item_key="openapi-usage-create-item-key-$(date +%s)" temp_item_value="openapi-usage-create-item-value-$(date +%s)" echo -e "create item: app id = ${APOLLO_APP_ID} env = ${APOLLO_ENV} key = ${temp_item_key} value = ${temp_item_value}" item_create ${APOLLO_ENV} ${APOLLO_APP_ID} default application ${temp_item_key} ${temp_item_value} "openapi-create-item" ${APOLLO_USER} printf "\n\n" # update an item echo "show update failed when item key not exists" sleep 1 temp_item_key="openapi-usage-update-item-key-$(date +%s)" temp_item_value="openapi-usage-update-item-value-$(date +%s)" item_update ${APOLLO_ENV} ${APOLLO_APP_ID} default application ${temp_item_key} ${temp_item_value} "openapi-update-item" ${APOLLO_USER} printf "\n\n" echo "show after created, update success" item_create ${APOLLO_ENV} ${APOLLO_APP_ID} default application ${temp_item_key} ${temp_item_value} "openapi-create-item" ${APOLLO_USER} temp_item_value="item-update-success" printf "\n" item_update ${APOLLO_ENV} ${APOLLO_APP_ID} default application ${temp_item_key} ${temp_item_value} "openapi-update-item" ${APOLLO_USER} printf "\n\n" echo "show Update an item of a namespace, if item doesn't exist, create it" sleep 1 temp_item_key="openapi-usage-item_update_create_if_not_exists-key-$(date +%s)" temp_item_value="openapi-usage-item_update_create_if_not_exists-value-$(date +%s)" echo "create it, key = '${temp_item_key}' value = '${temp_item_value}'" item_update_create_if_not_exists ${APOLLO_ENV} ${APOLLO_APP_ID} default application ${temp_item_key} ${temp_item_value} "openapi-update-item" ${APOLLO_USER} ${APOLLO_USER} temp_item_value="openapi-value-of-item_update_create_if_not_exists" echo "update it, key = '${temp_item_key}' value = '${temp_item_value}'" item_update_create_if_not_exists ${APOLLO_ENV} ${APOLLO_APP_ID} default application ${temp_item_key} ${temp_item_value} "openapi-update-item" ${APOLLO_USER} ${APOLLO_USER} printf "\n\n" echo "show delete item failed" item_delete ${APOLLO_ENV} ${APOLLO_APP_ID} default application "key-be-deleted" ${APOLLO_USER} printf "\nshow delete item success\n" item_delete ${APOLLO_ENV} ${APOLLO_APP_ID} default application ${temp_item_key} ${APOLLO_USER} printf "\n\n" ####################################### end of item ####################################### ####################################### namespace release ####################################### temp_namespace_name="application-$(date +%s)" temp_namespace_format=properties echo -e "create namespace: namespace name = '${temp_namespace_name}', app id = '${APOLLO_APP_ID}', format = '${temp_namespace_format}'" namespace_create ${APOLLO_APP_ID} ${temp_namespace_name} ${temp_namespace_format} false 'create by openapi, bash scripts for release' ${APOLLO_USER} echo -e "\ncreate or update an item '${temp_item_key}'='${temp_item_value}'" item_update_create_if_not_exists ${APOLLO_ENV} ${APOLLO_APP_ID} default ${temp_namespace_name} ${temp_item_key} ${temp_item_value} "openapi-update-item" ${APOLLO_USER} ${APOLLO_USER} echo -e "\nrelease namespace: '${temp_namespace_name}'" namespace_release ${APOLLO_ENV} ${APOLLO_APP_ID} ${APOLLO_CLUSTER} ${temp_namespace_name} 'releaseTitle-openapi-2021-01-01' 'releaseComment-openapi' ${APOLLO_USER} printf "\n\n" ####################################### end of namespace release ####################################### ================================================ FILE: scripts/openapi/bash/openapi.sh ================================================ #!/bin/bash # # Copyright 2024 Apollo Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # title openapi.sh # description functions to call openapi through http # author wxq # date 2021-09-12 # Chinese reference website https://www.apolloconfig.com/#/zh/portal/apollo-open-api-platform # English reference website https://www.apolloconfig.com/#/en/portal/apollo-open-api-platform ####################################### Global variables ####################################### # portal's address, just support 1 address without suffix '/' # Don't use http://ip:port/ with suffix '/' or multiple address http://ip1:port1,http://ip2:port2 APOLLO_PORTAL_ADDRESS=${APOLLO_PORTAL_ADDRESS:-http://ip:port} APOLLO_OPENAPI_TOKEN=${APOLLO_OPENAPI_TOKEN:-please_change_me_by_environment_variable} CURL_OPTIONS=${CURL_OPTIONS:-} echo "apollo portal address: ${APOLLO_PORTAL_ADDRESS}" echo "curl options: ${CURL_OPTIONS}" ####################################### end of Global variables ####################################### ####################################### basic http call ####################################### ####################################### # Http get by curl. # Globals: # APOLLO_PORTAL_ADDRESS: portal's address # APOLLO_OPENAPI_TOKEN: openapi's token # CURL_OPTIONS: options in curl # Arguments: # url_suffix ####################################### function openapi_get() { local url_suffix=$1 local url="${APOLLO_PORTAL_ADDRESS}/${url_suffix}" curl ${CURL_OPTIONS} --header "Authorization: ${APOLLO_OPENAPI_TOKEN}" --header "Content-Type: application/json;charset=UTF-8" "${url}" } ####################################### # Http post by curl. # Globals: # APOLLO_PORTAL_ADDRESS: portal's address # APOLLO_OPENAPI_TOKEN: openapi's token # CURL_OPTIONS: options in curl # Arguments: # url_suffix # body ####################################### function openapi_post() { local url_suffix=$1 local body=$2 local url="${APOLLO_PORTAL_ADDRESS}/${url_suffix}" curl ${CURL_OPTIONS} --header "Authorization: ${APOLLO_OPENAPI_TOKEN}" --header "Content-Type: application/json;charset=UTF-8" --data "${body}" "${url}" } ####################################### # Http put by curl. # Globals: # APOLLO_PORTAL_ADDRESS: portal's address # APOLLO_OPENAPI_TOKEN: openapi's token # CURL_OPTIONS: options in curl # Arguments: # url_suffix # body ####################################### function openapi_put() { local url_suffix=$1 local body=$2 local url="${APOLLO_PORTAL_ADDRESS}/${url_suffix}" curl ${CURL_OPTIONS} --header "Authorization: ${APOLLO_OPENAPI_TOKEN}" --header "Content-Type: application/json;charset=UTF-8" -X PUT --data "${body}" "${url}" } ####################################### # Http delete by curl. # Globals: # APOLLO_PORTAL_ADDRESS: portal's address # APOLLO_OPENAPI_TOKEN: openapi's token # CURL_OPTIONS: options in curl # Arguments: # url_suffix # body ####################################### function openapi_delete() { local url_suffix=$1 local body=$2 local url="${APOLLO_PORTAL_ADDRESS}/${url_suffix}" curl ${CURL_OPTIONS} --header "Authorization: ${APOLLO_OPENAPI_TOKEN}" --header "Content-Type: application/json;charset=UTF-8" -X DELETE --data "${body}" "${url}" } ####################################### end of basic http call ####################################### ####################################### cluster ####################################### ####################################### # Get cluster. # 获取集群 # Arguments: # env # appId # clusterName ####################################### function cluster_get() { local env=$1 local appId=$2 local clusterName=$3 openapi_get "openapi/v1/envs/${env}/apps/${appId}/clusters/${clusterName}" } ####################################### # Create cluster in app's environment. # 创建集群 # Arguments: # env # appId # clusterName # dataChangeCreatedBy ####################################### function cluster_create() { local env=$1 local appId=$2 local clusterName=$3 local dataChangeCreatedBy=$4 openapi_post "openapi/v1/envs/${env}/apps/${appId}/clusters" "$(cat <