Commit 0900d8df authored by Jason Song's avatar Jason Song Committed by GitHub

Merge branch 'master' into master

parents 29ed1dd2 7c435bd5
......@@ -106,7 +106,18 @@ Java客户端不依赖任何框架,能够运行于所有Java运行时环境,
* [开源配置中心Apollo的设计与实现](http://www.infoq.com/cn/articles/open-source-configuration-center-apollo)
# Support
![tech-support-qq](https://raw.githubusercontent.com/ctripcorp/apollo/master/doc/images/tech-support-qq.png)
<table>
<thead>
<th>Apollo配置中心技术支持②群<br />群号:904287263</th>
<th>Apollo配置中心技术支持①群<br />群号:375526581(已满)</th>
</thead>
<tbody>
<tr>
<td><img src="https://raw.githubusercontent.com/ctripcorp/apollo/master/doc/images/tech-support-qq-2.png" alt="tech-support-qq-2"></td>
<td><img src="https://raw.githubusercontent.com/ctripcorp/apollo/master/doc/images/tech-support-qq-1.png" alt="tech-support-qq-1"></td>
</tr>
</tbody>
</table>
# Contribution
* Source Code: https://github.com/ctripcorp/apollo
......@@ -206,3 +217,13 @@ The project is licensed under the [Apache 2 license](https://github.com/ctripcor
![万谷盛世](https://raw.githubusercontent.com/ctripcorp/apollo/master/doc/images/known-users/wgss.png)
![铂涛旅行](https://raw.githubusercontent.com/ctripcorp/apollo/master/doc/images/known-users/plateno.png)
![乐心](https://raw.githubusercontent.com/ctripcorp/apollo/master/doc/images/known-users/lifesense.png)
![亿投传媒](https://raw.githubusercontent.com/ctripcorp/apollo/master/doc/images/known-users/reachmedia.png)
![股先生](https://raw.githubusercontent.com/ctripcorp/apollo/master/doc/images/known-users/guxiansheng.png)
![财学堂](https://raw.githubusercontent.com/ctripcorp/apollo/master/doc/images/known-users/caixuetang.png)
![4399](https://raw.githubusercontent.com/ctripcorp/apollo/master/doc/images/known-users/4399.png)
![汽车之家](https://raw.githubusercontent.com/ctripcorp/apollo/master/doc/images/known-users/autohome.png)
![面包财经](https://raw.githubusercontent.com/ctripcorp/apollo/master/doc/images/known-users/mbcaijing.png)
![虎扑](https://raw.githubusercontent.com/ctripcorp/apollo/master/doc/images/known-users/hoopchina.png)
![搜狐汽车](https://raw.githubusercontent.com/ctripcorp/apollo/master/doc/images/known-users/sohu-auto.png)
![量富征信](https://raw.githubusercontent.com/ctripcorp/apollo/master/doc/images/known-users/liangfuzhengxin.png)
![卖好车](https://raw.githubusercontent.com/ctripcorp/apollo/master/doc/images/known-users/maihaoche.png)
......@@ -9,8 +9,8 @@ MAINTAINER ameizi <sxyx2008@163.com>
ENV VERSION 1.1.0-SNAPSHOT
RUN echo "http://mirrors.aliyun.com/alpine/v3.6/main" > /etc/apk/repositories \
&& echo "http://mirrors.aliyun.com/alpine/v3.6/community" >> /etc/apk/repositories \
RUN echo "http://mirrors.aliyun.com/alpine/v3.8/main" > /etc/apk/repositories \
&& echo "http://mirrors.aliyun.com/alpine/v3.8/community" >> /etc/apk/repositories \
&& apk update upgrade \
&& apk add --no-cache procps unzip curl bash tzdata \
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
......
......@@ -3,12 +3,9 @@ package com.ctrip.framework.apollo.adminservice;
import com.ctrip.framework.apollo.biz.ApolloBizConfig;
import com.ctrip.framework.apollo.common.ApolloCommonConfig;
import org.springframework.boot.actuate.system.ApplicationPidFileWriter;
import org.springframework.boot.actuate.system.EmbeddedServerPortFileWriter;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
......@@ -25,9 +22,6 @@ import org.springframework.transaction.annotation.EnableTransactionManagement;
AdminServiceApplication.class})
public class AdminServiceApplication {
public static void main(String[] args) {
ConfigurableApplicationContext context =
new SpringApplicationBuilder(AdminServiceApplication.class).run(args);
context.addApplicationListener(new ApplicationPidFileWriter());
context.addApplicationListener(new EmbeddedServerPortFileWriter());
SpringApplication.run(AdminServiceApplication.class, args);
}
}
......@@ -43,7 +43,7 @@ public class ItemController {
ConfigChangeContentBuilder builder = new ConfigChangeContentBuilder();
Item managedEntity = itemService.findOne(appId, clusterName, namespaceName, entity.getKey());
if (managedEntity != null) {
throw new BadRequestException("item already exist");
throw new BadRequestException("item already exists");
} else {
entity = itemService.save(entity);
builder.createItem(entity);
......
......@@ -176,4 +176,35 @@ public class ReleaseController {
Topics.APOLLO_RELEASE_TOPIC);
}
@Transactional
@RequestMapping(path = "/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/gray-del-releases", method = RequestMethod.POST)
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<String> grayDelKeys){
Namespace namespace = namespaceService.findOne(appId, clusterName, namespaceName);
if (namespace == null) {
throw new NotFoundException(String.format("Could not find namespace for %s %s %s", 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.transfrom(ReleaseDTO.class, release);
}
}
......@@ -11,4 +11,5 @@ server:
port: 8090
logging:
file: /opt/logs/100003172/apollo-adminservice.log
path: /opt/logs/100003172
file: ${logging.path}/apollo-adminservice.log
......@@ -3,10 +3,22 @@
<include resource="org/springframework/boot/logging/logback/defaults.xml" />
<property name="LOG_FILE"
value="${LOG_FILE:-${LOG_PATH:-${LOG_TEMP:-${java.io.tmpdir:-/tmp}}/}apollo-adminservice.log}" />
<property name="LOG_PATH" value="${LOG_PATH:-${LOG_TEMP:-${java.io.tmpdir:-/tmp}}}" />
<include resource="org/springframework/boot/logging/logback/file-appender.xml" />
<include resource="org/springframework/boot/logging/logback/console-appender.xml" />
<root level="INFO">
<appender-ref ref="FILE" />
<appender-ref ref="CONSOLE" />
</root>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/apollo-adminservice.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxFileSize>50MB</maxFileSize>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
</encoder>
</appender>
</configuration>
......@@ -6,7 +6,6 @@ import com.ctrip.framework.apollo.portal.PortalApplication;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.actuate.system.ApplicationPidFileWriter;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration;
......@@ -26,7 +25,6 @@ public class ApolloApplication {
*/
ConfigurableApplicationContext commonContext =
new SpringApplicationBuilder(ApolloApplication.class).web(false).run(args);
commonContext.addApplicationListener(new ApplicationPidFileWriter());
logger.info(commonContext.getId() + " isActive: " + commonContext.isActive());
/**
......
......@@ -6,7 +6,6 @@ import com.ctrip.framework.apollo.portal.PortalApplication;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.actuate.system.ApplicationPidFileWriter;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration;
......@@ -26,7 +25,6 @@ public class LocalApolloApplication {
*/
ConfigurableApplicationContext commonContext =
new SpringApplicationBuilder(ApolloApplication.class).web(false).run(args);
commonContext.addApplicationListener(new ApplicationPidFileWriter());
logger.info(commonContext.getId() + " isActive: " + commonContext.isActive());
/**
......
......@@ -185,6 +185,49 @@ public class ReleaseService {
return release;
}
private Release publishBranchNamespace(Namespace parentNamespace, Namespace childNamespace,
Map<String, String> childNamespaceItems,
String releaseName, String releaseComment,
String operator, boolean isEmergencyPublish, Set<String> grayDelKeys) {
Release parentLatestRelease = findLatestActiveRelease(parentNamespace);
Map<String, String> parentConfigurations = parentLatestRelease != null ?
gson.fromJson(parentLatestRelease.getConfigurations(),
GsonType.CONFIG) : new HashMap<>();
long baseReleaseId = parentLatestRelease == null ? 0 : parentLatestRelease.getId();
Map<String, String> configsToPublish = mergeConfiguration(parentConfigurations, childNamespaceItems);
if(!(grayDelKeys == null || grayDelKeys.size()==0)){
for (String key : grayDelKeys){
configsToPublish.remove(key);
}
}
return branchRelease(parentNamespace, childNamespace, releaseName, releaseComment,
configsToPublish, baseReleaseId, operator,
ReleaseOperation.GRAY_RELEASE, isEmergencyPublish);
}
@Transactional
public Release grayDeletionPublish(Namespace namespace, String releaseName, String releaseComment,
String operator, boolean isEmergencyPublish, Set<String> grayDelKeys) {
checkLock(namespace, isEmergencyPublish, operator);
Map<String, String> operateNamespaceItems = getNamespaceItems(namespace);
Namespace parentNamespace = namespaceService.findParentNamespace(namespace);
//branch release
if (parentNamespace != null) {
return publishBranchNamespace(parentNamespace, namespace, operateNamespaceItems,
releaseName, releaseComment, operator, isEmergencyPublish, grayDelKeys);
}else {
throw new NotFoundException("Parent namespace not found");
}
}
private void checkLock(Namespace namespace, boolean isEmergencyPublish, String operator) {
if (!isEmergencyPublish) {
NamespaceLock lock = namespaceLockService.findLock(namespace.getId());
......@@ -222,17 +265,8 @@ public class ReleaseService {
Map<String, String> childNamespaceItems,
String releaseName, String releaseComment,
String operator, boolean isEmergencyPublish) {
Release parentLatestRelease = findLatestActiveRelease(parentNamespace);
Map<String, String> parentConfigurations = parentLatestRelease != null ?
gson.fromJson(parentLatestRelease.getConfigurations(),
GsonType.CONFIG) : new HashMap<>();
long baseReleaseId = parentLatestRelease == null ? 0 : parentLatestRelease.getId();
Map<String, String> childNamespaceToPublishConfigs = mergeConfiguration(parentConfigurations, childNamespaceItems);
return branchRelease(parentNamespace, childNamespace, releaseName, releaseComment,
childNamespaceToPublishConfigs, baseReleaseId, operator,
ReleaseOperation.GRAY_RELEASE, isEmergencyPublish);
return publishBranchNamespace(parentNamespace, childNamespace, childNamespaceItems, releaseName, releaseComment,
operator, isEmergencyPublish, null);
}
......
package com.ctrip.framework.apollo;
import com.ctrip.framework.apollo.enums.ConfigSourceType;
import com.google.common.base.Function;
import java.util.Date;
......@@ -178,6 +179,14 @@ public interface Config {
*/
public void addChangeListener(ConfigChangeListener listener, Set<String> interestedKeys);
/**
* Remove the change listener
*
* @param listener the specific config change listener to remove
* @return true if the specific config change listener is found and removed
*/
public boolean removeChangeListener(ConfigChangeListener listener);
/**
* Return a set of the property names
*
......@@ -195,4 +204,11 @@ public interface Config {
* @return the property value
*/
public <T> T getProperty(String key, Function<String, T> function, T defaultValue);
/**
* Return the config's source type, i.e. where is the config loaded from
*
* @return the config's source type
*/
public ConfigSourceType getSourceType();
}
package com.ctrip.framework.apollo;
import com.ctrip.framework.apollo.core.enums.ConfigFileFormat;
import com.ctrip.framework.apollo.enums.ConfigSourceType;
/**
* @author Jason Song(song_s@ctrip.com)
......@@ -36,4 +37,19 @@ public interface ConfigFile {
* @param listener the config file change listener
*/
void addChangeListener(ConfigFileChangeListener listener);
/**
* Remove the change listener
*
* @param listener the specific config change listener to remove
* @return true if the specific config change listener is found and removed
*/
public boolean removeChangeListener(ConfigChangeListener listener);
/**
* Return the config's source type, i.e. where is the config loaded from
*
* @return the config's source type
*/
public ConfigSourceType getSourceType();
}
package com.ctrip.framework.apollo.enums;
/**
* To indicate the config's source type, i.e. where is the config loaded from
*/
public enum ConfigSourceType {
REMOTE("Loaded from remote config service"), LOCAL("Loaded from local cache"), NONE("Load failed");
private final String description;
ConfigSourceType(String description) {
this.description = description;
}
public String getDescription() {
return description;
}
}
......@@ -85,6 +85,12 @@ public abstract class AbstractConfig implements Config {
}
}
@Override
public boolean removeChangeListener(ConfigChangeListener listener) {
m_interestedKeys.remove(listener);
return m_listeners.remove(listener);
}
@Override
public Integer getIntProperty(String key, Integer defaultValue) {
try {
......
package com.ctrip.framework.apollo.internals;
import com.ctrip.framework.apollo.ConfigChangeListener;
import com.ctrip.framework.apollo.enums.ConfigSourceType;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.ExecutorService;
......@@ -25,10 +27,12 @@ import com.google.common.collect.Lists;
public abstract class AbstractConfigFile implements ConfigFile, RepositoryChangeListener {
private static final Logger logger = LoggerFactory.getLogger(AbstractConfigFile.class);
private static ExecutorService m_executorService;
protected ConfigRepository m_configRepository;
protected String m_namespace;
protected AtomicReference<Properties> m_configProperties;
private List<ConfigFileChangeListener> m_listeners = Lists.newCopyOnWriteArrayList();
protected final ConfigRepository m_configRepository;
protected final String m_namespace;
protected final AtomicReference<Properties> m_configProperties;
private final List<ConfigFileChangeListener> m_listeners = Lists.newCopyOnWriteArrayList();
private volatile ConfigSourceType m_sourceType = ConfigSourceType.NONE;
static {
m_executorService = Executors.newCachedThreadPool(ApolloThreadFactory
......@@ -45,6 +49,7 @@ public abstract class AbstractConfigFile implements ConfigFile, RepositoryChange
private void initialize() {
try {
m_configProperties.set(m_configRepository.getConfig());
m_sourceType = m_configRepository.getSourceType();
} catch (Throwable ex) {
Tracer.logError(ex);
logger.warn("Init Apollo Config File failed - namespace: {}, reason: {}.",
......@@ -74,6 +79,7 @@ public abstract class AbstractConfigFile implements ConfigFile, RepositoryChange
String oldValue = getContent();
update(newProperties);
m_sourceType = m_configRepository.getSourceType();
String newValue = getContent();
......@@ -97,6 +103,16 @@ public abstract class AbstractConfigFile implements ConfigFile, RepositoryChange
}
}
@Override
public boolean removeChangeListener(ConfigChangeListener listener) {
return m_listeners.remove(listener);
}
@Override
public ConfigSourceType getSourceType() {
return m_sourceType;
}
private void fireConfigChange(final ConfigFileChangeEvent changeEvent) {
for (final ConfigFileChangeListener listener : m_listeners) {
m_executorService.submit(new Runnable() {
......
package com.ctrip.framework.apollo.internals;
import com.ctrip.framework.apollo.enums.ConfigSourceType;
import java.util.Properties;
/**
......@@ -29,4 +30,11 @@ public interface ConfigRepository {
* @param listener the listener to remove
*/
public void removeChangeListener(RepositoryChangeListener listener);
/**
* Return the config's source type, i.e. where is the config loaded from
*
* @return the config's source type
*/
public ConfigSourceType getSourceType();
}
package com.ctrip.framework.apollo.internals;
import com.ctrip.framework.apollo.enums.ConfigSourceType;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
......@@ -30,10 +31,12 @@ import com.google.common.util.concurrent.RateLimiter;
public class DefaultConfig extends AbstractConfig implements RepositoryChangeListener {
private static final Logger logger = LoggerFactory.getLogger(DefaultConfig.class);
private final String m_namespace;
private Properties m_resourceProperties;
private AtomicReference<Properties> m_configProperties;
private ConfigRepository m_configRepository;
private RateLimiter m_warnLogRateLimiter;
private final Properties m_resourceProperties;
private final AtomicReference<Properties> m_configProperties;
private final ConfigRepository m_configRepository;
private final RateLimiter m_warnLogRateLimiter;
private volatile ConfigSourceType m_sourceType = ConfigSourceType.NONE;
/**
* Constructor.
......@@ -52,7 +55,7 @@ public class DefaultConfig extends AbstractConfig implements RepositoryChangeLis
private void initialize() {
try {
m_configProperties.set(m_configRepository.getConfig());
updateConfig(m_configRepository.getConfig(), m_configRepository.getSourceType());
} catch (Throwable ex) {
Tracer.logError(ex);
logger.warn("Init Apollo Local Config failed - namespace: {}, reason: {}.",
......@@ -105,6 +108,11 @@ public class DefaultConfig extends AbstractConfig implements RepositoryChangeLis
return stringPropertyNames(properties);
}
@Override
public ConfigSourceType getSourceType() {
return m_sourceType;
}
private Set<String> stringPropertyNames(Properties properties) {
//jdk9以下版本Properties#enumerateStringProperties方法存在性能问题,keys() + get(k) 重复迭代, jdk9之后改为entrySet遍历.
Map<String, String> h = new HashMap<>();
......@@ -123,10 +131,12 @@ public class DefaultConfig extends AbstractConfig implements RepositoryChangeLis
if (newProperties.equals(m_configProperties.get())) {
return;
}
ConfigSourceType sourceType = m_configRepository.getSourceType();
Properties newConfigProperties = new Properties();
newConfigProperties.putAll(newProperties);
Map<String, ConfigChange> actualChanges = updateAndCalcConfigChanges(newConfigProperties);
Map<String, ConfigChange> actualChanges = updateAndCalcConfigChanges(newConfigProperties, sourceType);
//check double checked result
if (actualChanges.isEmpty()) {
......@@ -138,7 +148,13 @@ public class DefaultConfig extends AbstractConfig implements RepositoryChangeLis
Tracer.logEvent("Apollo.Client.ConfigChanges", m_namespace);
}
private Map<String, ConfigChange> updateAndCalcConfigChanges(Properties newConfigProperties) {
private void updateConfig(Properties newConfigProperties, ConfigSourceType sourceType) {
m_configProperties.set(newConfigProperties);
m_sourceType = sourceType;
}
private Map<String, ConfigChange> updateAndCalcConfigChanges(Properties newConfigProperties,
ConfigSourceType sourceType) {
List<ConfigChange> configChanges =
calcPropertyChanges(m_namespace, m_configProperties.get(), newConfigProperties);
......@@ -153,7 +169,7 @@ public class DefaultConfig extends AbstractConfig implements RepositoryChangeLis
}
//2. update m_configProperties
m_configProperties.set(newConfigProperties);
updateConfig(newConfigProperties, sourceType);
clearConfigCache();
//3. use getProperty to update configChange's new value and calc the final changes
......
package com.ctrip.framework.apollo.internals;
import com.ctrip.framework.apollo.enums.ConfigSourceType;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
......@@ -38,6 +39,8 @@ public class LocalFileConfigRepository extends AbstractConfigRepository
private volatile Properties m_fileProperties;
private volatile ConfigRepository m_upstream;
private volatile ConfigSourceType m_sourceType = ConfigSourceType.LOCAL;
/**
* Constructor.
*
......@@ -104,6 +107,11 @@ public class LocalFileConfigRepository extends AbstractConfigRepository
upstreamConfigRepository.addChangeListener(this);
}
@Override
public ConfigSourceType getSourceType() {
return m_sourceType;
}
@Override
public void onRepositoryChange(String namespace, Properties newProperties) {
if (newProperties.equals(m_fileProperties)) {
......@@ -111,7 +119,7 @@ public class LocalFileConfigRepository extends AbstractConfigRepository
}
Properties newFileProperties = new Properties();
newFileProperties.putAll(newProperties);
updateFileProperties(newFileProperties);
updateFileProperties(newFileProperties, m_upstream.getSourceType());
this.fireRepositoryChange(namespace, newProperties);
}
......@@ -129,6 +137,7 @@ public class LocalFileConfigRepository extends AbstractConfigRepository
try {
transaction.addData("Basedir", m_baseDir.getAbsolutePath());
m_fileProperties = this.loadFromLocalCacheFile(m_baseDir, m_namespace);
m_sourceType = ConfigSourceType.LOCAL;
transaction.setStatus(Transaction.SUCCESS);
} catch (Throwable ex) {
Tracer.logEvent("ApolloConfigException", ExceptionUtil.getDetailMessage(ex));
......@@ -140,6 +149,7 @@ public class LocalFileConfigRepository extends AbstractConfigRepository
}
if (m_fileProperties == null) {
m_sourceType = ConfigSourceType.NONE;
throw new ApolloConfigException(
"Load config from local config failed!", exception);
}
......@@ -150,8 +160,7 @@ public class LocalFileConfigRepository extends AbstractConfigRepository
return false;
}
try {
Properties properties = m_upstream.getConfig();
updateFileProperties(properties);
updateFileProperties(m_upstream.getConfig(), m_upstream.getSourceType());
return true;
} catch (Throwable ex) {
Tracer.logError(ex);
......@@ -162,7 +171,8 @@ public class LocalFileConfigRepository extends AbstractConfigRepository
return false;
}
private synchronized void updateFileProperties(Properties newProperties) {
private synchronized void updateFileProperties(Properties newProperties, ConfigSourceType sourceType) {
this.m_sourceType = sourceType;
if (newProperties.equals(m_fileProperties)) {
return;
}
......
package com.ctrip.framework.apollo.internals;
import com.ctrip.framework.apollo.enums.ConfigSourceType;
import java.util.Collections;
import java.util.List;
import java.util.Map;
......@@ -47,21 +48,22 @@ public class RemoteConfigRepository extends AbstractConfigRepository {
private static final Logger logger = LoggerFactory.getLogger(RemoteConfigRepository.class);
private static final Joiner STRING_JOINER = Joiner.on(ConfigConsts.CLUSTER_NAMESPACE_SEPARATOR);
private static final Joiner.MapJoiner MAP_JOINER = Joiner.on("&").withKeyValueSeparator("=");
private ConfigServiceLocator m_serviceLocator;
private HttpUtil m_httpUtil;
private ConfigUtil m_configUtil;
private RemoteConfigLongPollService remoteConfigLongPollService;
private static final Escaper pathEscaper = UrlEscapers.urlPathSegmentEscaper();
private static final Escaper queryParamEscaper = UrlEscapers.urlFormParameterEscaper();
private final ConfigServiceLocator m_serviceLocator;
private final HttpUtil m_httpUtil;
private final ConfigUtil m_configUtil;
private final RemoteConfigLongPollService remoteConfigLongPollService;
private volatile AtomicReference<ApolloConfig> m_configCache;
private final String m_namespace;
private final static ScheduledExecutorService m_executorService;
private AtomicReference<ServiceDTO> m_longPollServiceDto;
private AtomicReference<ApolloNotificationMessages> m_remoteMessages;
private RateLimiter m_loadConfigRateLimiter;
private AtomicBoolean m_configNeedForceRefresh;
private SchedulePolicy m_loadConfigFailSchedulePolicy;
private Gson gson;
private static final Escaper pathEscaper = UrlEscapers.urlPathSegmentEscaper();
private static final Escaper queryParamEscaper = UrlEscapers.urlFormParameterEscaper();
private final AtomicReference<ServiceDTO> m_longPollServiceDto;
private final AtomicReference<ApolloNotificationMessages> m_remoteMessages;
private final RateLimiter m_loadConfigRateLimiter;
private final AtomicBoolean m_configNeedForceRefresh;
private final SchedulePolicy m_loadConfigFailSchedulePolicy;
private final Gson gson;
static {
m_executorService = Executors.newScheduledThreadPool(1,
......@@ -105,6 +107,11 @@ public class RemoteConfigRepository extends AbstractConfigRepository {
//remote config doesn't need upstream
}
@Override
public ConfigSourceType getSourceType() {
return ConfigSourceType.REMOTE;
}
private void schedulePeriodicRefresh() {
logger.debug("Schedule periodic refresh with interval: {} {}",
m_configUtil.getRefreshInterval(), m_configUtil.getRefreshIntervalTimeUnit());
......
package com.ctrip.framework.apollo.internals;
import com.ctrip.framework.apollo.enums.ConfigSourceType;
import java.util.Collections;
import java.util.List;
import java.util.Map;
......@@ -24,6 +25,7 @@ public class SimpleConfig extends AbstractConfig implements RepositoryChangeList
private final String m_namespace;
private final ConfigRepository m_configRepository;
private volatile Properties m_configProperties;
private volatile ConfigSourceType m_sourceType = ConfigSourceType.NONE;
/**
* Constructor.
......@@ -39,7 +41,7 @@ public class SimpleConfig extends AbstractConfig implements RepositoryChangeList
private void initialize() {
try {
m_configProperties = m_configRepository.getConfig();
updateConfig(m_configRepository.getConfig(), m_configRepository.getSourceType());
} catch (Throwable ex) {
Tracer.logError(ex);
logger.warn("Init Apollo Simple Config failed - namespace: {}, reason: {}", m_namespace,
......@@ -69,6 +71,11 @@ public class SimpleConfig extends AbstractConfig implements RepositoryChangeList
return m_configProperties.stringPropertyNames();
}
@Override
public ConfigSourceType getSourceType() {
return m_sourceType;
}
@Override
public synchronized void onRepositoryChange(String namespace, Properties newProperties) {
if (newProperties.equals(m_configProperties)) {
......@@ -77,9 +84,7 @@ public class SimpleConfig extends AbstractConfig implements RepositoryChangeList
Properties newConfigProperties = new Properties();
newConfigProperties.putAll(newProperties);
List<ConfigChange>
changes =
calcPropertyChanges(namespace, m_configProperties, newConfigProperties);
List<ConfigChange> changes = calcPropertyChanges(namespace, m_configProperties, newConfigProperties);
Map<String, ConfigChange> changeMap = Maps.uniqueIndex(changes,
new Function<ConfigChange, String>() {
@Override
......@@ -88,11 +93,16 @@ public class SimpleConfig extends AbstractConfig implements RepositoryChangeList
}
});
m_configProperties = newConfigProperties;
updateConfig(newConfigProperties, m_configRepository.getSourceType());
clearConfigCache();
this.fireConfigChange(new ConfigChangeEvent(m_namespace, changeMap));
Tracer.logEvent("Apollo.Client.ConfigChanges", m_namespace);
}
private void updateConfig(Properties newConfigProperties, ConfigSourceType sourceType) {
m_configProperties = newConfigProperties;
m_sourceType = sourceType;
}
}
......@@ -65,7 +65,7 @@ public class ApolloJsonValueProcessor extends ApolloProcessor implements BeanFac
Set<String> keys = placeholderHelper.extractPlaceholderKeys(placeholder);
for (String key : keys) {
SpringValue springValue = new SpringValue(key, placeholder, bean, beanName, field, true);
springValueRegistry.register(key, springValue);
springValueRegistry.register(beanFactory, key, springValue);
logger.debug("Monitoring {}", springValue);
}
}
......@@ -102,7 +102,7 @@ public class ApolloJsonValueProcessor extends ApolloProcessor implements BeanFac
for (String key : keys) {
SpringValue springValue = new SpringValue(key, apolloJsonValue.value(), bean, beanName,
method, true);
springValueRegistry.register(key, springValue);
springValueRegistry.register(beanFactory, key, springValue);
logger.debug("Monitoring {}", springValue);
}
}
......
......@@ -19,9 +19,12 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.context.annotation.Bean;
/**
......@@ -30,7 +33,7 @@ import org.springframework.context.annotation.Bean;
* @author github.com/zhegexiaohuozi seimimaster@gmail.com
* @since 2017/12/20.
*/
public class SpringValueProcessor extends ApolloProcessor implements BeanFactoryPostProcessor {
public class SpringValueProcessor extends ApolloProcessor implements BeanFactoryPostProcessor, BeanFactoryAware {
private static final Logger logger = LoggerFactory.getLogger(SpringValueProcessor.class);
......@@ -38,21 +41,22 @@ public class SpringValueProcessor extends ApolloProcessor implements BeanFactory
private final PlaceholderHelper placeholderHelper;
private final SpringValueRegistry springValueRegistry;
private static Multimap<String, SpringValueDefinition> beanName2SpringValueDefinitions =
LinkedListMultimap.create();
private BeanFactory beanFactory;
private Multimap<String, SpringValueDefinition> beanName2SpringValueDefinitions;
public SpringValueProcessor() {
configUtil = ApolloInjector.getInstance(ConfigUtil.class);
placeholderHelper = SpringInjector.getInstance(PlaceholderHelper.class);
springValueRegistry = SpringInjector.getInstance(SpringValueRegistry.class);
beanName2SpringValueDefinitions = LinkedListMultimap.create();
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
throws BeansException {
if (configUtil.isAutoUpdateInjectedSpringPropertiesEnabled()) {
if (configUtil.isAutoUpdateInjectedSpringPropertiesEnabled() && beanFactory instanceof BeanDefinitionRegistry) {
beanName2SpringValueDefinitions = SpringValueDefinitionProcessor
.getBeanName2SpringValueDefinitions();
.getBeanName2SpringValueDefinitions((BeanDefinitionRegistry) beanFactory);
}
}
......@@ -82,7 +86,7 @@ public class SpringValueProcessor extends ApolloProcessor implements BeanFactory
for (String key : keys) {
SpringValue springValue = new SpringValue(key, value.value(), bean, beanName, field, false);
springValueRegistry.register(key, springValue);
springValueRegistry.register(beanFactory, key, springValue);
logger.debug("Monitoring {}", springValue);
}
}
......@@ -112,7 +116,7 @@ public class SpringValueProcessor extends ApolloProcessor implements BeanFactory
for (String key : keys) {
SpringValue springValue = new SpringValue(key, value.value(), bean, beanName, method, false);
springValueRegistry.register(key, springValue);
springValueRegistry.register(beanFactory, key, springValue);
logger.info("Monitoring {}", springValue);
}
}
......@@ -135,7 +139,7 @@ public class SpringValueProcessor extends ApolloProcessor implements BeanFactory
}
SpringValue springValue = new SpringValue(definition.getKey(), definition.getPlaceholder(),
bean, beanName, method, false);
springValueRegistry.register(definition.getKey(), springValue);
springValueRegistry.register(beanFactory, definition.getKey(), springValue);
logger.debug("Monitoring {}", springValue);
} catch (Throwable ex) {
logger.error("Failed to enable auto update feature for {}.{}", bean.getClass(),
......@@ -147,4 +151,8 @@ public class SpringValueProcessor extends ApolloProcessor implements BeanFactory
beanName2SpringValueDefinitions.removeAll(beanName);
}
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
}
}
......@@ -11,9 +11,11 @@ import com.google.common.collect.Multimap;
import com.ctrip.framework.apollo.Config;
import com.ctrip.framework.apollo.ConfigService;
import com.google.common.collect.Sets;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.Set;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.context.EnvironmentAware;
......@@ -25,6 +27,8 @@ import org.springframework.core.env.Environment;
import java.util.Collection;
import java.util.Iterator;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.PropertySource;
/**
* Apollo Property Sources processor for Spring Annotation Based Application. <br /> <br />
......@@ -38,7 +42,7 @@ import java.util.Iterator;
*/
public class PropertySourcesProcessor implements BeanFactoryPostProcessor, EnvironmentAware, PriorityOrdered {
private static final Multimap<Integer, String> NAMESPACE_NAMES = LinkedHashMultimap.create();
private static final AtomicBoolean INITIALIZED = new AtomicBoolean(false);
private static final Set<BeanFactory> AUTO_UPDATE_INITIALIZED_BEAN_FACTORIES = Sets.newConcurrentHashSet();
private final ConfigPropertySourceFactory configPropertySourceFactory = SpringInjector
.getInstance(ConfigPropertySourceFactory.class);
......@@ -51,11 +55,8 @@ public class PropertySourcesProcessor implements BeanFactoryPostProcessor, Envir
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
if (INITIALIZED.compareAndSet(false, true)) {
initializePropertySources();
initializeAutoUpdatePropertiesFeature(beanFactory);
}
initializePropertySources();
initializeAutoUpdatePropertiesFeature(beanFactory);
}
private void initializePropertySources() {
......@@ -78,9 +79,16 @@ public class PropertySourcesProcessor implements BeanFactoryPostProcessor, Envir
}
}
// clean up
NAMESPACE_NAMES.clear();
// add after the bootstrap property source or to the first
if (environment.getPropertySources()
.contains(PropertySourcesConstants.APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME)) {
// ensure ApolloBootstrapPropertySources is still the first
ensureBootstrapPropertyPrecedence(environment);
environment.getPropertySources()
.addAfter(PropertySourcesConstants.APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME, composite);
} else {
......@@ -88,8 +96,24 @@ public class PropertySourcesProcessor implements BeanFactoryPostProcessor, Envir
}
}
private void ensureBootstrapPropertyPrecedence(ConfigurableEnvironment environment) {
MutablePropertySources propertySources = environment.getPropertySources();
PropertySource<?> bootstrapPropertySource = propertySources
.get(PropertySourcesConstants.APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME);
// not exists or already in the first place
if (bootstrapPropertySource == null || propertySources.precedenceOf(bootstrapPropertySource) == 0) {
return;
}
propertySources.remove(PropertySourcesConstants.APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME);
propertySources.addFirst(bootstrapPropertySource);
}
private void initializeAutoUpdatePropertiesFeature(ConfigurableListableBeanFactory beanFactory) {
if (!configUtil.isAutoUpdateInjectedSpringPropertiesEnabled()) {
if (!configUtil.isAutoUpdateInjectedSpringPropertiesEnabled() ||
!AUTO_UPDATE_INITIALIZED_BEAN_FACTORIES.add(beanFactory)) {
return;
}
......@@ -108,12 +132,6 @@ public class PropertySourcesProcessor implements BeanFactoryPostProcessor, Envir
this.environment = (ConfigurableEnvironment) environment;
}
//only for test
private static void reset() {
NAMESPACE_NAMES.clear();
INITIALIZED.set(false);
}
@Override
public int getOrder() {
//make it as early as possible
......
......@@ -51,7 +51,7 @@ public class AutoUpdateConfigChangeListener implements ConfigChangeListener{
}
for (String key : keys) {
// 1. check whether the changed key is relevant
Collection<SpringValue> targetValues = springValueRegistry.get(key);
Collection<SpringValue> targetValues = springValueRegistry.get(beanFactory, key);
if (targetValues == null || targetValues.isEmpty()) {
continue;
}
......
package com.ctrip.framework.apollo.spring.property;
import com.ctrip.framework.apollo.spring.util.SpringInjector;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import org.springframework.beans.BeansException;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.PropertyValue;
......@@ -30,9 +32,9 @@ import com.google.common.collect.Multimap;
* </pre>
*/
public class SpringValueDefinitionProcessor implements BeanDefinitionRegistryPostProcessor {
private static final Multimap<String, SpringValueDefinition> beanName2SpringValueDefinitions =
LinkedListMultimap.create();
private static final AtomicBoolean initialized = new AtomicBoolean(false);
private static final Map<BeanDefinitionRegistry, Multimap<String, SpringValueDefinition>> beanName2SpringValueDefinitions =
Maps.newConcurrentMap();
private static final Set<BeanDefinitionRegistry> PROPERTY_VALUES_PROCESSED_BEAN_FACTORIES = Sets.newConcurrentHashSet();
private final ConfigUtil configUtil;
private final PlaceholderHelper placeholderHelper;
......@@ -54,16 +56,27 @@ public class SpringValueDefinitionProcessor implements BeanDefinitionRegistryPos
}
public static Multimap<String, SpringValueDefinition> getBeanName2SpringValueDefinitions() {
return beanName2SpringValueDefinitions;
public static Multimap<String, SpringValueDefinition> getBeanName2SpringValueDefinitions(BeanDefinitionRegistry registry) {
Multimap<String, SpringValueDefinition> springValueDefinitions = beanName2SpringValueDefinitions.get(registry);
if (springValueDefinitions == null) {
springValueDefinitions = LinkedListMultimap.create();
}
return springValueDefinitions;
}
private void processPropertyValues(BeanDefinitionRegistry beanRegistry) {
if (!initialized.compareAndSet(false, true)) {
if (!PROPERTY_VALUES_PROCESSED_BEAN_FACTORIES.add(beanRegistry)) {
// already initialized
return;
}
if (!beanName2SpringValueDefinitions.containsKey(beanRegistry)) {
beanName2SpringValueDefinitions.put(beanRegistry, LinkedListMultimap.<String, SpringValueDefinition>create());
}
Multimap<String, SpringValueDefinition> springValueDefinitions = beanName2SpringValueDefinitions.get(beanRegistry);
String[] beanNames = beanRegistry.getBeanDefinitionNames();
for (String beanName : beanNames) {
BeanDefinition beanDefinition = beanRegistry.getBeanDefinition(beanName);
......@@ -82,16 +95,9 @@ public class SpringValueDefinitionProcessor implements BeanDefinitionRegistryPos
}
for (String key : keys) {
beanName2SpringValueDefinitions.put(beanName,
new SpringValueDefinition(key, placeholder, propertyValue.getName()));
springValueDefinitions.put(beanName, new SpringValueDefinition(key, placeholder, propertyValue.getName()));
}
}
}
}
//only for test
private static void reset() {
initialized.set(false);
beanName2SpringValueDefinitions.clear();
}
}
package com.ctrip.framework.apollo.spring.property;
import com.google.common.collect.LinkedListMultimap;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import java.util.Collection;
import java.util.Map;
import org.springframework.beans.factory.BeanFactory;
public class SpringValueRegistry {
private final Multimap<String, SpringValue> registry = LinkedListMultimap.create();
public void register(String key, SpringValue springValue) {
registry.put(key, springValue);
private final Map<BeanFactory, Multimap<String, SpringValue>> registry = Maps.newConcurrentMap();
private final Object LOCK = new Object();
public void register(BeanFactory beanFactory, String key, SpringValue springValue) {
if (!registry.containsKey(beanFactory)) {
synchronized (LOCK) {
if (!registry.containsKey(beanFactory)) {
registry.put(beanFactory, LinkedListMultimap.<String, SpringValue>create());
}
}
}
registry.get(beanFactory).put(key, springValue);
}
public Collection<SpringValue> get(String key) {
return registry.get(key);
public Collection<SpringValue> get(BeanFactory beanFactory, String key) {
Multimap<String, SpringValue> beanFactorySpringValues = registry.get(beanFactory);
if (beanFactorySpringValues == null) {
return null;
}
return beanFactorySpringValues.get(key);
}
}
......@@ -2,6 +2,7 @@ package com.ctrip.framework.apollo;
import static org.junit.Assert.assertEquals;
import com.ctrip.framework.apollo.enums.ConfigSourceType;
import java.util.Set;
import org.junit.Before;
......@@ -101,6 +102,11 @@ public class ConfigServiceTest {
public Set<String> getPropertyNames() {
return null;
}
@Override
public ConfigSourceType getSourceType() {
return null;
}
}
private static class MockConfigFile implements ConfigFile {
......@@ -137,6 +143,16 @@ public class ConfigServiceTest {
public void addChangeListener(ConfigFileChangeListener listener) {
}
@Override
public boolean removeChangeListener(ConfigChangeListener listener) {
return false;
}
@Override
public ConfigSourceType getSourceType() {
return null;
}
}
public static class MockConfigFactory implements ConfigFactory {
......
......@@ -5,6 +5,7 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.mock;
import com.ctrip.framework.apollo.enums.ConfigSourceType;
import java.util.Properties;
import java.util.Set;
......@@ -103,6 +104,11 @@ public class DefaultConfigManagerTest {
public Set<String> getPropertyNames() {
return null;
}
@Override
public ConfigSourceType getSourceType() {
return null;
}
};
}
......
......@@ -3,11 +3,13 @@ package com.ctrip.framework.apollo.internals;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
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.enums.ConfigSourceType;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import java.io.File;
......@@ -49,6 +51,7 @@ public class DefaultConfigTest {
private String someNamespace;
private ConfigRepository configRepository;
private Properties someProperties;
private ConfigSourceType someSourceType;
@Before
public void setUp() throws Exception {
......@@ -98,6 +101,8 @@ public class DefaultConfigTest {
someProperties.setProperty(someKey, someLocalFileValue);
someProperties.setProperty(anotherKey, someLocalFileValue);
when(configRepository.getConfig()).thenReturn(someProperties);
someSourceType = ConfigSourceType.LOCAL;
when(configRepository.getSourceType()).thenReturn(someSourceType);
//set up resource file
File resourceFile = new File(someResourceDir, someNamespace + ".properties");
......@@ -121,6 +126,7 @@ public class DefaultConfigTest {
assertEquals(someLocalFileValue, anotherKeyValue);
assertEquals(someResourceValue, lastKeyValue);
assertEquals(someSourceType, defaultConfig.getSourceType());
}
@Test
......@@ -598,6 +604,8 @@ public class DefaultConfigTest {
.of(someKey, someLocalFileValue, anotherKey, someLocalFileValue, keyToBeDeleted,
keyToBeDeletedValue, yetAnotherKey, yetAnotherValue));
when(configRepository.getConfig()).thenReturn(someProperties);
someSourceType = ConfigSourceType.LOCAL;
when(configRepository.getSourceType()).thenReturn(someSourceType);
//set up resource file
File resourceFile = new File(someResourceDir, someNamespace + ".properties");
......@@ -606,6 +614,8 @@ public class DefaultConfigTest {
DefaultConfig defaultConfig =
new DefaultConfig(someNamespace, configRepository);
assertEquals(someSourceType, defaultConfig.getSourceType());
final SettableFuture<ConfigChangeEvent> configChangeFuture = SettableFuture.create();
ConfigChangeListener someListener = new ConfigChangeListener() {
@Override
......@@ -624,6 +634,9 @@ public class DefaultConfigTest {
newProperties.putAll(ImmutableMap
.of(someKey, someKeyNewValue, anotherKey, anotherKeyNewValue, newKey, newValue));
ConfigSourceType anotherSourceType = ConfigSourceType.REMOTE;
when(configRepository.getSourceType()).thenReturn(anotherSourceType);
defaultConfig.onRepositoryChange(someNamespace, newProperties);
ConfigChangeEvent changeEvent = configChangeFuture.get(500, TimeUnit.MILLISECONDS);
......@@ -653,6 +666,8 @@ public class DefaultConfigTest {
assertEquals(null, newKeyChange.getOldValue());
assertEquals(newValue, newKeyChange.getNewValue());
assertEquals(PropertyChangeType.ADDED, newKeyChange.getChangeType());
assertEquals(anotherSourceType, defaultConfig.getSourceType());
}
@Test
......@@ -705,6 +720,62 @@ public class DefaultConfigTest {
assertFalse(interestedInSomeKeyNotChangedFuture.isDone());
}
@Test
public void testRemoveChangeListener() throws Exception {
String someNamespace = "someNamespace";
final ConfigChangeEvent someConfigChangEvent = mock(ConfigChangeEvent.class);
ConfigChangeEvent anotherConfigChangEvent = mock(ConfigChangeEvent.class);
final SettableFuture<ConfigChangeEvent> someListenerFuture1 = SettableFuture.create();
final SettableFuture<ConfigChangeEvent> someListenerFuture2 = SettableFuture.create();
ConfigChangeListener someListener = new ConfigChangeListener() {
@Override
public void onChange(ConfigChangeEvent changeEvent) {
if (someConfigChangEvent == changeEvent) {
someListenerFuture1.set(changeEvent);
} else {
someListenerFuture2.set(changeEvent);
}
}
};
final SettableFuture<ConfigChangeEvent> anotherListenerFuture1 = SettableFuture.create();
final SettableFuture<ConfigChangeEvent> anotherListenerFuture2 = SettableFuture.create();
ConfigChangeListener anotherListener = new ConfigChangeListener() {
@Override
public void onChange(ConfigChangeEvent changeEvent) {
if (someConfigChangEvent == changeEvent) {
anotherListenerFuture1.set(changeEvent);
} else {
anotherListenerFuture2.set(changeEvent);
}
}
};
ConfigChangeListener yetAnotherListener = mock(ConfigChangeListener.class);
DefaultConfig config = new DefaultConfig(someNamespace, mock(ConfigRepository.class));
config.addChangeListener(someListener);
config.addChangeListener(anotherListener);
config.fireConfigChange(someConfigChangEvent);
assertEquals(someConfigChangEvent, someListenerFuture1.get(500, TimeUnit.MILLISECONDS));
assertEquals(someConfigChangEvent, anotherListenerFuture1.get(500, TimeUnit.MILLISECONDS));
assertFalse(config.removeChangeListener(yetAnotherListener));
assertTrue(config.removeChangeListener(someListener));
config.fireConfigChange(anotherConfigChangEvent);
assertEquals(anotherConfigChangEvent, anotherListenerFuture2.get(500, TimeUnit.MILLISECONDS));
TimeUnit.MILLISECONDS.sleep(100);
assertFalse(someListenerFuture2.isDone());
}
@Test
public void testGetPropertyNames() {
String someKeyPrefix = "someKey";
......@@ -768,6 +839,34 @@ public class DefaultConfigTest {
}, Lists.<String>newArrayList()), Lists.newArrayList());
}
@Test
public void testLoadFromRepositoryFailedAndThenRecovered() {
String someKey = "someKey";
String someValue = "someValue";
String someDefaultValue = "someDefaultValue";
ConfigSourceType someSourceType = ConfigSourceType.REMOTE;
when(configRepository.getConfig()).thenThrow(mock(RuntimeException.class));
DefaultConfig defaultConfig =
new DefaultConfig(someNamespace, configRepository);
verify(configRepository, times(1)).addChangeListener(defaultConfig);
assertEquals(ConfigSourceType.NONE, defaultConfig.getSourceType());
assertEquals(someDefaultValue, defaultConfig.getProperty(someKey, someDefaultValue));
someProperties = new Properties();
someProperties.setProperty(someKey, someValue);
when(configRepository.getSourceType()).thenReturn(someSourceType);
defaultConfig.onRepositoryChange(someNamespace, someProperties);
assertEquals(someSourceType, defaultConfig.getSourceType());
assertEquals(someValue, defaultConfig.getProperty(someKey, someDefaultValue));
}
private void checkDatePropertyWithFormat(Config config, Date expected, String propertyName, String format, Date
defaultValue) {
assertEquals(expected, config.getDateProperty(propertyName, format, defaultValue));
......
......@@ -6,6 +6,7 @@ import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.when;
import com.ctrip.framework.apollo.enums.ConfigSourceType;
import java.util.Properties;
import org.junit.Before;
......@@ -26,6 +27,8 @@ public class JsonConfigFileTest {
@Mock
private ConfigRepository configRepository;
private ConfigSourceType someSourceType;
@Before
public void setUp() throws Exception {
someNamespace = "someName";
......@@ -38,7 +41,10 @@ public class JsonConfigFileTest {
String someValue = "someValue";
someProperties.setProperty(key, someValue);
someSourceType = ConfigSourceType.LOCAL;
when(configRepository.getConfig()).thenReturn(someProperties);
when(configRepository.getSourceType()).thenReturn(someSourceType);
JsonConfigFile configFile = new JsonConfigFile(someNamespace, configRepository);
......@@ -46,6 +52,7 @@ public class JsonConfigFileTest {
assertEquals(someNamespace, configFile.getNamespace());
assertTrue(configFile.hasContent());
assertEquals(someValue, configFile.getContent());
assertEquals(someSourceType, configFile.getSourceType());
}
@Test
......@@ -66,6 +73,7 @@ public class JsonConfigFileTest {
assertFalse(configFile.hasContent());
assertNull(configFile.getContent());
assertEquals(ConfigSourceType.NONE, configFile.getSourceType());
}
@Test
......@@ -76,18 +84,26 @@ public class JsonConfigFileTest {
String anotherValue = "anotherValue";
someProperties.setProperty(key, someValue);
someSourceType = ConfigSourceType.LOCAL;
when(configRepository.getConfig()).thenReturn(someProperties);
when(configRepository.getSourceType()).thenReturn(someSourceType);
JsonConfigFile configFile = new JsonConfigFile(someNamespace, configRepository);
assertEquals(someValue, configFile.getContent());
assertEquals(someSourceType, configFile.getSourceType());
Properties anotherProperties = new Properties();
anotherProperties.setProperty(key, anotherValue);
ConfigSourceType anotherSourceType = ConfigSourceType.REMOTE;
when(configRepository.getSourceType()).thenReturn(anotherSourceType);
configFile.onRepositoryChange(someNamespace, anotherProperties);
assertEquals(anotherValue, configFile.getContent());
assertEquals(anotherSourceType, configFile.getSourceType());
}
@Test
......@@ -97,16 +113,21 @@ public class JsonConfigFileTest {
String someValue = "someValue";
someProperties.setProperty(key, someValue);
someSourceType = ConfigSourceType.LOCAL;
when(configRepository.getConfig()).thenThrow(new RuntimeException("someError"));
when(configRepository.getSourceType()).thenReturn(someSourceType);
JsonConfigFile configFile = new JsonConfigFile(someNamespace, configRepository);
assertFalse(configFile.hasContent());
assertNull(configFile.getContent());
assertEquals(ConfigSourceType.NONE, configFile.getSourceType());
configFile.onRepositoryChange(someNamespace, someProperties);
assertTrue(configFile.hasContent());
assertEquals(someValue, configFile.getContent());
assertEquals(someSourceType, configFile.getSourceType());
}
}
......@@ -9,6 +9,7 @@ import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import com.ctrip.framework.apollo.enums.ConfigSourceType;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
......@@ -38,6 +39,7 @@ public class LocalFileConfigRepositoryTest {
private static String someCluster = "someCluster";
private String defaultKey;
private String defaultValue;
private ConfigSourceType someSourceType;
@Before
public void setUp() throws Exception {
......@@ -49,8 +51,10 @@ public class LocalFileConfigRepositoryTest {
defaultKey = "defaultKey";
defaultValue = "defaultValue";
someProperties.setProperty(defaultKey, defaultValue);
someSourceType = ConfigSourceType.REMOTE;
upstreamRepo = mock(ConfigRepository.class);
when(upstreamRepo.getConfig()).thenReturn(someProperties);
when(upstreamRepo.getSourceType()).thenReturn(someSourceType);
MockInjector.reset();
MockInjector.setInstance(ConfigUtil.class, new MockConfigUtil());
......@@ -95,7 +99,7 @@ public class LocalFileConfigRepositoryTest {
Properties properties = localRepo.getConfig();
assertEquals(someValue, properties.getProperty(someKey));
assertEquals(ConfigSourceType.LOCAL, localRepo.getSourceType());
}
@Test
......@@ -112,12 +116,12 @@ public class LocalFileConfigRepositoryTest {
Properties properties = localRepo.getConfig();
assertEquals(defaultValue, properties.getProperty(defaultKey));
assertEquals(someSourceType, localRepo.getSourceType());
}
@Test
public void testLoadConfigWithNoLocalFile() throws Exception {
LocalFileConfigRepository
localFileConfigRepository =
LocalFileConfigRepository localFileConfigRepository =
new LocalFileConfigRepository(someNamespace, upstreamRepo);
localFileConfigRepository.setLocalCacheDir(someBaseDir, true);
......@@ -126,6 +130,7 @@ public class LocalFileConfigRepositoryTest {
assertThat(
"LocalFileConfigRepository's properties should be the same as fallback repo's when there is no local cache",
result.entrySet(), equalTo(someProperties.entrySet()));
assertEquals(someSourceType, localFileConfigRepository.getSourceType());
}
@Test
......@@ -146,7 +151,7 @@ public class LocalFileConfigRepositoryTest {
assertThat(
"LocalFileConfigRepository should persist local cache files and return that afterwards",
someProperties.entrySet(), equalTo(anotherProperties.entrySet()));
assertEquals(someSourceType, localRepo.getSourceType());
}
@Test
......@@ -155,6 +160,9 @@ public class LocalFileConfigRepositoryTest {
LocalFileConfigRepository localFileConfigRepository =
new LocalFileConfigRepository(someNamespace, upstreamRepo);
assertEquals(someSourceType, localFileConfigRepository.getSourceType());
localFileConfigRepository.setLocalCacheDir(someBaseDir, true);
localFileConfigRepository.addChangeListener(someListener);
......@@ -163,6 +171,9 @@ public class LocalFileConfigRepositoryTest {
Properties anotherProperties = new Properties();
anotherProperties.put("anotherKey", "anotherValue");
ConfigSourceType anotherSourceType = ConfigSourceType.NONE;
when(upstreamRepo.getSourceType()).thenReturn(anotherSourceType);
localFileConfigRepository.onRepositoryChange(someNamespace, anotherProperties);
final ArgumentCaptor<Properties> captor = ArgumentCaptor.forClass(Properties.class);
......@@ -170,7 +181,7 @@ public class LocalFileConfigRepositoryTest {
verify(someListener, times(1)).onRepositoryChange(eq(someNamespace), captor.capture());
assertEquals(anotherProperties, captor.getValue());
assertEquals(anotherSourceType, localFileConfigRepository.getSourceType());
}
public static class MockConfigUtil extends ConfigUtil {
......
......@@ -12,6 +12,7 @@ import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import com.ctrip.framework.apollo.enums.ConfigSourceType;
import java.lang.reflect.Type;
import java.util.List;
import java.util.Map;
......@@ -105,6 +106,7 @@ public class RemoteConfigRepositoryTest {
Properties config = remoteConfigRepository.getConfig();
assertEquals(configurations, config);
assertEquals(ConfigSourceType.REMOTE, remoteConfigRepository.getSourceType());
remoteConfigLongPollService.stopLongPollingRefresh();
}
......
package com.ctrip.framework.apollo.internals;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import com.ctrip.framework.apollo.enums.ConfigSourceType;
import java.util.Properties;
import java.util.concurrent.TimeUnit;
......@@ -28,6 +30,7 @@ public class SimpleConfigTest {
private String someNamespace;
@Mock
private ConfigRepository configRepository;
private ConfigSourceType someSourceType;
@Before
public void setUp() throws Exception {
......@@ -41,22 +44,28 @@ public class SimpleConfigTest {
String someValue = "someValue";
someProperties.setProperty(someKey, someValue);
someSourceType = ConfigSourceType.LOCAL;
when(configRepository.getConfig()).thenReturn(someProperties);
when(configRepository.getSourceType()).thenReturn(someSourceType);
SimpleConfig config = new SimpleConfig(someNamespace, configRepository);
assertEquals(someValue, config.getProperty(someKey, null));
assertEquals(someSourceType, config.getSourceType());
}
@Test
public void testLoadConfigFromConfigRepositoryError() throws Exception {
when(configRepository.getConfig()).thenThrow(Throwable.class);
String someKey = "someKey";
String anyValue = "anyValue" + Math.random();
when(configRepository.getConfig()).thenThrow(mock(RuntimeException.class));
Config config = new SimpleConfig(someNamespace, configRepository);
String someKey = "someKey";
String anyValue = "anyValue" + Math.random();
assertEquals(anyValue, config.getProperty(someKey, anyValue));
assertEquals(ConfigSourceType.NONE, config.getSourceType());
}
@Test
......@@ -74,7 +83,10 @@ public class SimpleConfigTest {
String someValueNew = "someValueNew";
anotherProperties.putAll(ImmutableMap.of(someKey, someValueNew, newKey, newValue));
someSourceType = ConfigSourceType.LOCAL;
when(configRepository.getConfig()).thenReturn(someProperties);
when(configRepository.getSourceType()).thenReturn(someSourceType);
final SettableFuture<ConfigChangeEvent> configChangeFuture = SettableFuture.create();
ConfigChangeListener someListener = new ConfigChangeListener() {
......@@ -85,8 +97,14 @@ public class SimpleConfigTest {
};
SimpleConfig config = new SimpleConfig(someNamespace, configRepository);
assertEquals(someSourceType, config.getSourceType());
config.addChangeListener(someListener);
ConfigSourceType anotherSourceType = ConfigSourceType.REMOTE;
when(configRepository.getSourceType()).thenReturn(anotherSourceType);
config.onRepositoryChange(someNamespace, anotherProperties);
ConfigChangeEvent changeEvent = configChangeFuture.get(500, TimeUnit.MILLISECONDS);
......@@ -108,5 +126,7 @@ public class SimpleConfigTest {
assertEquals(null, newKeyChange.getOldValue());
assertEquals(newValue, newKeyChange.getNewValue());
assertEquals(PropertyChangeType.ADDED, newKeyChange.getChangeType());
assertEquals(anotherSourceType, config.getSourceType());
}
}
......@@ -33,16 +33,10 @@ import com.google.common.collect.Maps;
*/
public abstract class AbstractSpringIntegrationTest {
private static final Map<String, Config> CONFIG_REGISTRY = Maps.newHashMap();
private static Method PROPERTY_SOURCES_PROCESSOR_CLEAR;
private static Method SPRING_VALUE_DEFINITION_PROCESS_CLEAR;
private static Method CONFIG_SERVICE_RESET;
static {
try {
PROPERTY_SOURCES_PROCESSOR_CLEAR = PropertySourcesProcessor.class.getDeclaredMethod("reset");
ReflectionUtils.makeAccessible(PROPERTY_SOURCES_PROCESSOR_CLEAR);
SPRING_VALUE_DEFINITION_PROCESS_CLEAR = SpringValueDefinitionProcessor.class.getDeclaredMethod("reset");
ReflectionUtils.makeAccessible(SPRING_VALUE_DEFINITION_PROCESS_CLEAR);
CONFIG_SERVICE_RESET = ConfigService.class.getDeclaredMethod("reset");
ReflectionUtils.makeAccessible(CONFIG_SERVICE_RESET);
} catch (NoSuchMethodException e) {
......@@ -112,10 +106,6 @@ public abstract class AbstractSpringIntegrationTest {
}
protected static void doSetUp() {
//as PropertySourcesProcessor has some static states, so we must manually clear its state
ReflectionUtils.invokeMethod(PROPERTY_SOURCES_PROCESSOR_CLEAR, null);
//as SpringValueDefinitionProcessor has some static states, so we must manually clear its state
ReflectionUtils.invokeMethod(SPRING_VALUE_DEFINITION_PROCESS_CLEAR, null);
//as ConfigService is singleton, so we must manually clear its container
ReflectionUtils.invokeMethod(CONFIG_SERVICE_RESET, null);
MockInjector.reset();
......
......@@ -9,8 +9,8 @@ MAINTAINER ameizi <sxyx2008@163.com>
ENV VERSION 1.1.0-SNAPSHOT
RUN echo "http://mirrors.aliyun.com/alpine/v3.6/main" > /etc/apk/repositories \
&& echo "http://mirrors.aliyun.com/alpine/v3.6/community" >> /etc/apk/repositories \
RUN echo "http://mirrors.aliyun.com/alpine/v3.8/main" > /etc/apk/repositories \
&& echo "http://mirrors.aliyun.com/alpine/v3.8/community" >> /etc/apk/repositories \
&& apk update upgrade \
&& apk add --no-cache procps unzip curl bash tzdata \
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
......
......@@ -4,12 +4,9 @@ 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.actuate.system.ApplicationPidFileWriter;
import org.springframework.boot.actuate.system.EmbeddedServerPortFileWriter;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
......@@ -35,10 +32,7 @@ import org.springframework.transaction.annotation.EnableTransactionManagement;
public class ConfigServiceApplication {
public static void main(String[] args) throws Exception {
ConfigurableApplicationContext context =
new SpringApplicationBuilder(ConfigServiceApplication.class).run(args);
context.addApplicationListener(new ApplicationPidFileWriter());
context.addApplicationListener(new EmbeddedServerPortFileWriter());
SpringApplication.run(ConfigServiceApplication.class, args);
}
}
......@@ -11,4 +11,5 @@ server:
port: 8080
logging:
file: /opt/logs/100003171/apollo-configservice.log
path: /opt/logs/100003171
file: ${logging.path}/apollo-configservice.log
......@@ -3,10 +3,22 @@
<include resource="org/springframework/boot/logging/logback/defaults.xml" />
<property name="LOG_FILE"
value="${LOG_FILE:-${LOG_PATH:-${LOG_TEMP:-${java.io.tmpdir:-/tmp}}/}apollo-configservice.log}" />
<property name="LOG_PATH" value="${LOG_PATH:-${LOG_TEMP:-${java.io.tmpdir:-/tmp}}}" />
<include resource="org/springframework/boot/logging/logback/file-appender.xml" />
<include resource="org/springframework/boot/logging/logback/console-appender.xml" />
<root level="INFO">
<appender-ref ref="FILE" />
<appender-ref ref="CONSOLE" />
</root>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/apollo-configservice.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxFileSize>50MB</maxFileSize>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
</encoder>
</appender>
</configuration>
......@@ -10,6 +10,11 @@
<modelVersion>4.0.0</modelVersion>
<artifactId>apollo-mockserver</artifactId>
<name>Apollo Mock Server</name>
<properties>
<java.version>1.7</java.version>
</properties>
<dependencyManagement>
<dependencies>
......
......@@ -5,10 +5,9 @@ import com.ctrip.framework.apollo.core.dto.ApolloConfig;
import com.ctrip.framework.apollo.core.dto.ApolloConfigNotification;
import com.ctrip.framework.apollo.core.utils.ResourceUtils;
import com.ctrip.framework.apollo.internals.ConfigServiceLocator;
import com.ctrip.framework.apollo.spring.config.PropertySourcesProcessor;
import com.ctrip.framework.apollo.spring.property.SpringValueDefinitionProcessor;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Method;
......@@ -19,7 +18,6 @@ import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.stream.Collectors;
import okhttp3.mockwebserver.Dispatcher;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
......@@ -37,8 +35,6 @@ public class EmbeddedApollo extends ExternalResource {
private static final Type notificationType = new TypeToken<List<ApolloConfigNotification>>() {
}.getType();
private static Method PROPERTY_SOURCES_PROCESSOR_CLEAR;
private static Method SPRING_VALUE_DEFINITION_PROCESS_CLEAR;
private static Method CONFIG_SERVICE_LOCATOR_CLEAR;
private static ConfigServiceLocator CONFIG_SERVICE_LOCATOR;
......@@ -51,10 +47,6 @@ public class EmbeddedApollo extends ExternalResource {
static {
try {
System.setProperty("apollo.longPollingInitialDelayInMills", "0");
PROPERTY_SOURCES_PROCESSOR_CLEAR = PropertySourcesProcessor.class.getDeclaredMethod("reset");
PROPERTY_SOURCES_PROCESSOR_CLEAR.setAccessible(true);
SPRING_VALUE_DEFINITION_PROCESS_CLEAR = SpringValueDefinitionProcessor.class.getDeclaredMethod("reset");
SPRING_VALUE_DEFINITION_PROCESS_CLEAR.setAccessible(true);
CONFIG_SERVICE_LOCATOR = ApolloInjector.getInstance(ConfigServiceLocator.class);
CONFIG_SERVICE_LOCATOR_CLEAR = ConfigServiceLocator.class.getDeclaredMethod("initConfigServices");
CONFIG_SERVICE_LOCATOR_CLEAR.setAccessible(true);
......@@ -105,9 +97,6 @@ public class EmbeddedApollo extends ExternalResource {
private void clear() throws Exception {
resetOverriddenProperties();
// clear Apollo states
PROPERTY_SOURCES_PROCESSOR_CLEAR.invoke(null);
SPRING_VALUE_DEFINITION_PROCESS_CLEAR.invoke(null);
}
private void mockConfigServiceUrl(String url) throws Exception {
......@@ -119,8 +108,10 @@ public class EmbeddedApollo extends ExternalResource {
private String loadConfigFor(String namespace) {
String filename = String.format("mockdata-%s.properties", namespace);
final Properties prop = ResourceUtils.readConfigFile(filename, new Properties());
Map<String, String> configurations = prop.stringPropertyNames().stream().collect(
Collectors.toMap(key -> key, prop::getProperty));
Map<String, String> configurations = Maps.newHashMap();
for (String propertyName : prop.stringPropertyNames()) {
configurations.put(propertyName, prop.getProperty(propertyName));
}
ApolloConfig apolloConfig = new ApolloConfig("someAppId", "someCluster", namespace, "someReleaseKey");
Map<String, String> mergedConfigurations = mergeOverriddenProperties(namespace, configurations);
......
......@@ -4,6 +4,7 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import com.ctrip.framework.apollo.Config;
import com.ctrip.framework.apollo.ConfigChangeListener;
import com.ctrip.framework.apollo.ConfigService;
import com.ctrip.framework.apollo.model.ConfigChangeEvent;
import com.google.common.util.concurrent.SettableFuture;
......@@ -32,9 +33,14 @@ public class ApolloMockServerApiTest {
Config otherConfig = ConfigService.getConfig(otherNamespace);
SettableFuture<ConfigChangeEvent> future = SettableFuture.create();
final SettableFuture<ConfigChangeEvent> future = SettableFuture.create();
otherConfig.addChangeListener(future::set);
otherConfig.addChangeListener(new ConfigChangeListener() {
@Override
public void onChange(ConfigChangeEvent changeEvent) {
future.set(changeEvent);
}
});
assertEquals("otherValue1", otherConfig.getProperty("key1", null));
assertEquals("otherValue2", otherConfig.getProperty("key2", null));
......
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>apollo</artifactId>
<groupId>com.ctrip.framework.apollo</groupId>
<version>1.1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>apollo-openapi</artifactId>
<name>Apollo Open Api</name>
<properties>
<java.version>1.7</java.version>
</properties>
<dependencies>
<dependency>
<groupId>com.ctrip.framework.apollo</groupId>
<artifactId>apollo-core</artifactId>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<exclusions>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
package com.ctrip.framework.apollo.openapi.client;
import com.ctrip.framework.apollo.openapi.client.constant.ApolloOpenApiConstants;
import com.ctrip.framework.apollo.openapi.client.service.AppOpenApiService;
import com.ctrip.framework.apollo.openapi.client.service.ItemOpenApiService;
import com.ctrip.framework.apollo.openapi.client.service.NamespaceOpenApiService;
import com.ctrip.framework.apollo.openapi.client.service.ReleaseOpenApiService;
import com.ctrip.framework.apollo.openapi.dto.NamespaceReleaseDTO;
import com.ctrip.framework.apollo.openapi.dto.OpenAppNamespaceDTO;
import com.ctrip.framework.apollo.openapi.dto.OpenEnvClusterDTO;
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.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import java.util.List;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicHeader;
/**
* This class contains collections of methods to access Apollo Open Api.
* <br />
* For more information, please refer <a href="https://github.com/ctripcorp/apollo/wiki/">Apollo Wiki</a>.
*
*/
public class ApolloOpenApiClient {
private final String portalUrl;
private final String token;
private final AppOpenApiService appService;
private final ItemOpenApiService itemService;
private final ReleaseOpenApiService releaseService;
private final NamespaceOpenApiService namespaceService;
private ApolloOpenApiClient(String portalUrl, String token, RequestConfig requestConfig) {
this.portalUrl = portalUrl;
this.token = token;
CloseableHttpClient client = HttpClients.custom().setDefaultRequestConfig(requestConfig)
.setDefaultHeaders(Lists.newArrayList(new BasicHeader("Authorization", token))).build();
Gson gson = new GsonBuilder().setDateFormat(ApolloOpenApiConstants.JSON_DATE_FORMAT).create();
String baseUrl = this.portalUrl + ApolloOpenApiConstants.OPEN_API_V1_PREFIX;
appService = new AppOpenApiService(client, baseUrl, gson);
namespaceService = new NamespaceOpenApiService(client, baseUrl, gson);
itemService = new ItemOpenApiService(client, baseUrl, gson);
releaseService = new ReleaseOpenApiService(client, baseUrl, gson);
}
/**
* Get the environment and cluster information
*/
public List<OpenEnvClusterDTO> getEnvClusterInfo(String appId) {
return appService.getEnvClusterInfo(appId);
}
/**
* Get the namespaces
*/
public List<OpenNamespaceDTO> getNamespaces(String appId, String env, String clusterName) {
return namespaceService.getNamespaces(appId, env, clusterName);
}
/**
* Get the namespace
*/
public OpenNamespaceDTO getNamespace(String appId, String env, String clusterName, String namespaceName) {
return namespaceService.getNamespace(appId, env, clusterName, namespaceName);
}
/**
* Create the app namespace
*/
public OpenAppNamespaceDTO createAppNamespace(OpenAppNamespaceDTO appNamespaceDTO) {
return namespaceService.createAppNamespace(appNamespaceDTO);
}
/**
* Get the namespace lock
*/
public OpenNamespaceLockDTO getNamespaceLock(String appId, String env, String clusterName, String namespaceName) {
return namespaceService.getNamespaceLock(appId, env, clusterName, namespaceName);
}
/**
* Add config
* @return the created config
*/
public OpenItemDTO createItem(String appId, String env, String clusterName, String namespaceName,
OpenItemDTO itemDTO) {
return itemService.createItem(appId, env, clusterName, namespaceName, itemDTO);
}
/**
* Update config
*/
public void updateItem(String appId, String env, String clusterName, String namespaceName, OpenItemDTO itemDTO) {
itemService.updateItem(appId, env, clusterName, namespaceName, itemDTO);
}
/**
* Create config if not exists or update config if already exists
*/
public void createOrUpdateItem(String appId, String env, String clusterName, String namespaceName, OpenItemDTO itemDTO) {
itemService.createOrUpdateItem(appId, env, clusterName, namespaceName, itemDTO);
}
/**
* Remove config
*
* @param operator the user who removes the item
*/
public void removeItem(String appId, String env, String clusterName, String namespaceName, String key,
String operator) {
itemService.removeItem(appId, env, clusterName, namespaceName, key, operator);
}
/**
* publish namespace
* @return the released configurations
*/
public OpenReleaseDTO publishNamespace(String appId, String env, String clusterName, String namespaceName,
NamespaceReleaseDTO releaseDTO) {
return releaseService.publishNamespace(appId, env, clusterName, namespaceName, releaseDTO);
}
/**
* @return the latest active release information or <code>null</code> if not found
*/
public OpenReleaseDTO getLatestActiveRelease(String appId, String env, String clusterName, String namespaceName) {
return releaseService.getLatestActiveRelease(appId, env, clusterName, namespaceName);
}
public String getPortalUrl() {
return portalUrl;
}
public String getToken() {
return token;
}
public static ApolloOpenApiClientBuilder newBuilder() {
return new ApolloOpenApiClientBuilder();
}
public static class ApolloOpenApiClientBuilder {
private String portalUrl;
private String token;
private int connectTimeout = -1;
private int readTimeout = -1;
/**
* @param portalUrl The apollo portal url, e.g http://localhost:8070
*/
public ApolloOpenApiClientBuilder withPortalUrl(String portalUrl) {
this.portalUrl = portalUrl;
return this;
}
/**
* @param token The authorization token, e.g. e16e5cd903fd0c97a116c873b448544b9d086de8
*/
public ApolloOpenApiClientBuilder withToken(String token) {
this.token = token;
return this;
}
/**
* @param connectTimeout an int that specifies the connect timeout value in milliseconds
*/
public ApolloOpenApiClientBuilder withConnectTimeout(int connectTimeout) {
this.connectTimeout = connectTimeout;
return this;
}
/**
* @param readTimeout an int that specifies the timeout value to be used in milliseconds
*/
public ApolloOpenApiClientBuilder withReadTimeout(int readTimeout) {
this.readTimeout = readTimeout;
return this;
}
public ApolloOpenApiClient build() {
Preconditions.checkArgument(!Strings.isNullOrEmpty(portalUrl), "Portal url should not be null or empty!");
Preconditions.checkArgument(portalUrl.startsWith("http://") || portalUrl.startsWith("https://"), "Portal url should start with http:// or https://" );
Preconditions.checkArgument(!Strings.isNullOrEmpty(token), "Token should not be null or empty!");
if (connectTimeout < 0) {
connectTimeout = ApolloOpenApiConstants.DEFAULT_CONNECT_TIMEOUT;
}
if (readTimeout < 0) {
readTimeout = ApolloOpenApiConstants.DEFAULT_READ_TIMEOUT;
}
RequestConfig requestConfig = RequestConfig.custom().setConnectTimeout(connectTimeout)
.setSocketTimeout(readTimeout).build();
return new ApolloOpenApiClient(portalUrl, token, requestConfig);
}
}
}
package com.ctrip.framework.apollo.openapi.client.constant;
public interface ApolloOpenApiConstants {
int DEFAULT_CONNECT_TIMEOUT = 1000; //1 second
int DEFAULT_READ_TIMEOUT = 5000; //5 seconds
String OPEN_API_V1_PREFIX = "/openapi/v1";
String JSON_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ssZ";
}
package com.ctrip.framework.apollo.openapi.client.exception;
public class ApolloOpenApiException extends RuntimeException {
public ApolloOpenApiException(int status, String reason, String message) {
super(String.format("Request to apollo open api failed, status code: %d, reason: %s, message: %s", status, reason,
message));
}
}
package com.ctrip.framework.apollo.openapi.client.service;
import com.ctrip.framework.apollo.openapi.client.exception.ApolloOpenApiException;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.escape.Escaper;
import com.google.common.net.UrlEscapers;
import com.google.gson.Gson;
import java.io.IOException;
import org.apache.http.HttpResponse;
import org.apache.http.StatusLine;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
abstract class AbstractOpenApiService {
private static final Escaper pathEscaper = UrlEscapers.urlPathSegmentEscaper();
private static final Escaper queryParamEscaper = UrlEscapers.urlFormParameterEscaper();
private final String baseUrl;
protected final CloseableHttpClient client;
protected final Gson gson;
AbstractOpenApiService(CloseableHttpClient client, String baseUrl, Gson gson) {
this.client = client;
this.baseUrl = baseUrl;
this.gson = gson;
}
protected CloseableHttpResponse get(String path) throws IOException {
HttpGet get = new HttpGet(String.format("%s/%s", baseUrl, path));
return execute(get);
}
protected CloseableHttpResponse post(String path, Object entity) throws IOException {
HttpPost post = new HttpPost(String.format("%s/%s", baseUrl, path));
return execute(post, entity);
}
protected CloseableHttpResponse put(String path, Object entity) throws IOException {
HttpPut put = new HttpPut(String.format("%s/%s", baseUrl, path));
return execute(put, entity);
}
protected CloseableHttpResponse delete(String path) throws IOException {
HttpDelete delete = new HttpDelete(String.format("%s/%s", baseUrl, path));
return execute(delete);
}
protected String escapePath(String path) {
return pathEscaper.escape(path);
}
protected String escapeParam(String param) {
return queryParamEscaper.escape(param);
}
private CloseableHttpResponse execute(HttpEntityEnclosingRequestBase requestBase, Object entity) throws IOException {
requestBase.setEntity(new StringEntity(gson.toJson(entity), ContentType.APPLICATION_JSON));
return execute(requestBase);
}
private CloseableHttpResponse execute(HttpUriRequest request) throws IOException {
CloseableHttpResponse response = client.execute(request);
checkHttpResponseStatus(response);
return response;
}
private void checkHttpResponseStatus(HttpResponse response) {
if (response.getStatusLine().getStatusCode() == 200) {
return;
}
StatusLine status = response.getStatusLine();
String message = "";
try {
message = EntityUtils.toString(response.getEntity());
} catch (IOException e) {
//ignore
}
throw new ApolloOpenApiException(status.getStatusCode(), status.getReasonPhrase(), message);
}
protected void checkNotEmpty(String value, String name) {
Preconditions.checkArgument(!Strings.isNullOrEmpty(value), name + " should not be null or empty");
}
}
package com.ctrip.framework.apollo.openapi.client.service;
import com.ctrip.framework.apollo.openapi.dto.OpenEnvClusterDTO;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type;
import java.util.List;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
public class AppOpenApiService extends AbstractOpenApiService {
private static final Type OPEN_ENV_CLUSTER_DTO_LIST_TYPE = new TypeToken<List<OpenEnvClusterDTO>>() {
}.getType();
public AppOpenApiService(CloseableHttpClient client, String baseUrl, Gson gson) {
super(client, baseUrl, gson);
}
public List<OpenEnvClusterDTO> getEnvClusterInfo(String appId) {
checkNotEmpty(appId, "App id");
String path = String.format("apps/%s/envclusters", escapePath(appId));
try (CloseableHttpResponse response = get(path)) {
return gson.fromJson(EntityUtils.toString(response.getEntity()), OPEN_ENV_CLUSTER_DTO_LIST_TYPE);
} catch (Throwable ex) {
throw new RuntimeException(String.format("Load env cluster information for appId: %s failed", appId), ex);
}
}
}
package com.ctrip.framework.apollo.openapi.client.service;
import com.ctrip.framework.apollo.core.ConfigConsts;
import com.ctrip.framework.apollo.openapi.dto.OpenItemDTO;
import com.google.common.base.Strings;
import com.google.gson.Gson;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
public class ItemOpenApiService extends AbstractOpenApiService {
public ItemOpenApiService(CloseableHttpClient client, String baseUrl, Gson gson) {
super(client, baseUrl, gson);
}
public OpenItemDTO createItem(String appId, String env, String clusterName, String namespaceName, OpenItemDTO itemDTO) {
if (Strings.isNullOrEmpty(clusterName)) {
clusterName = ConfigConsts.CLUSTER_NAME_DEFAULT;
}
if (Strings.isNullOrEmpty(namespaceName)) {
namespaceName = ConfigConsts.NAMESPACE_APPLICATION;
}
checkNotEmpty(appId, "App id");
checkNotEmpty(env, "Env");
checkNotEmpty(itemDTO.getKey(), "Item key");
checkNotEmpty(itemDTO.getValue(), "Item value");
checkNotEmpty(itemDTO.getDataChangeCreatedBy(), "Item created by");
String path = String.format("envs/%s/apps/%s/clusters/%s/namespaces/%s/items",
escapePath(env), escapePath(appId), escapePath(clusterName), escapePath(namespaceName));
try (CloseableHttpResponse response = post(path, itemDTO)) {
return gson.fromJson(EntityUtils.toString(response.getEntity()), OpenItemDTO.class);
} catch (Throwable ex) {
throw new RuntimeException(String
.format("Create item: %s for appId: %s, cluster: %s, namespace: %s in env: %s failed", itemDTO.getKey(),
appId, clusterName, namespaceName, env), ex);
}
}
public void updateItem(String appId, String env, String clusterName, String namespaceName, OpenItemDTO itemDTO) {
if (Strings.isNullOrEmpty(clusterName)) {
clusterName = ConfigConsts.CLUSTER_NAME_DEFAULT;
}
if (Strings.isNullOrEmpty(namespaceName)) {
namespaceName = ConfigConsts.NAMESPACE_APPLICATION;
}
checkNotEmpty(appId, "App id");
checkNotEmpty(env, "Env");
checkNotEmpty(itemDTO.getKey(), "Item key");
checkNotEmpty(itemDTO.getValue(), "Item value");
checkNotEmpty(itemDTO.getDataChangeLastModifiedBy(), "Item modified by");
String path = String.format("envs/%s/apps/%s/clusters/%s/namespaces/%s/items/%s",
escapePath(env), escapePath(appId), escapePath(clusterName), escapePath(namespaceName),
escapePath(itemDTO.getKey()));
try (CloseableHttpResponse ignored = put(path, itemDTO)) {
} catch (Throwable ex) {
throw new RuntimeException(String
.format("Update item: %s for appId: %s, cluster: %s, namespace: %s in env: %s failed", itemDTO.getKey(),
appId, clusterName, namespaceName, env), ex);
}
}
public void createOrUpdateItem(String appId, String env, String clusterName, String namespaceName, OpenItemDTO itemDTO) {
if (Strings.isNullOrEmpty(clusterName)) {
clusterName = ConfigConsts.CLUSTER_NAME_DEFAULT;
}
if (Strings.isNullOrEmpty(namespaceName)) {
namespaceName = ConfigConsts.NAMESPACE_APPLICATION;
}
checkNotEmpty(appId, "App id");
checkNotEmpty(env, "Env");
checkNotEmpty(itemDTO.getKey(), "Item key");
checkNotEmpty(itemDTO.getValue(), "Item value");
checkNotEmpty(itemDTO.getDataChangeCreatedBy(), "Item created by");
if (Strings.isNullOrEmpty(itemDTO.getDataChangeLastModifiedBy())) {
itemDTO.setDataChangeLastModifiedBy(itemDTO.getDataChangeCreatedBy());
}
String path = String.format("envs/%s/apps/%s/clusters/%s/namespaces/%s/items/%s?createIfNotExists=true",
escapePath(env), escapePath(appId), escapePath(clusterName), escapePath(namespaceName),
escapePath(itemDTO.getKey()));
try (CloseableHttpResponse ignored = put(path, itemDTO)) {
} catch (Throwable ex) {
throw new RuntimeException(String
.format("CreateOrUpdate item: %s for appId: %s, cluster: %s, namespace: %s in env: %s failed", itemDTO.getKey(),
appId, clusterName, namespaceName, env), ex);
}
}
public void removeItem(String appId, String env, String clusterName, String namespaceName, String key, String operator) {
if (Strings.isNullOrEmpty(clusterName)) {
clusterName = ConfigConsts.CLUSTER_NAME_DEFAULT;
}
if (Strings.isNullOrEmpty(namespaceName)) {
namespaceName = ConfigConsts.NAMESPACE_APPLICATION;
}
checkNotEmpty(appId, "App id");
checkNotEmpty(env, "Env");
checkNotEmpty(key, "Item key");
checkNotEmpty(operator, "Operator");
String path = String.format("envs/%s/apps/%s/clusters/%s/namespaces/%s/items/%s?operator=%s",
escapePath(env), escapePath(appId), escapePath(clusterName), escapePath(namespaceName), escapePath(key),
escapeParam(operator));
try (CloseableHttpResponse ignored = delete(path)) {
} catch (Throwable ex) {
throw new RuntimeException(String
.format("Remove item: %s for appId: %s, cluster: %s, namespace: %s in env: %s failed", key, appId,
clusterName, namespaceName, env), ex);
}
}
}
package com.ctrip.framework.apollo.openapi.client.service;
import com.ctrip.framework.apollo.core.ConfigConsts;
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 com.ctrip.framework.apollo.openapi.dto.OpenNamespaceLockDTO;
import com.google.common.base.Strings;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type;
import java.util.List;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
public class NamespaceOpenApiService extends AbstractOpenApiService {
private static final Type OPEN_NAMESPACE_DTO_LIST_TYPE = new TypeToken<List<OpenNamespaceDTO>>() {
}.getType();
public NamespaceOpenApiService(CloseableHttpClient client, String baseUrl, Gson gson) {
super(client, baseUrl, gson);
}
public OpenNamespaceDTO getNamespace(String appId, String env, String clusterName, String namespaceName) {
if (Strings.isNullOrEmpty(clusterName)) {
clusterName = ConfigConsts.CLUSTER_NAME_DEFAULT;
}
if (Strings.isNullOrEmpty(namespaceName)) {
namespaceName = ConfigConsts.NAMESPACE_APPLICATION;
}
checkNotEmpty(appId, "App id");
checkNotEmpty(env, "Env");
String path = String.format("envs/%s/apps/%s/clusters/%s/namespaces/%s", escapePath(env), escapePath(appId),
escapePath(clusterName), escapePath(namespaceName));
try (CloseableHttpResponse response = get(path)) {
return gson.fromJson(EntityUtils.toString(response.getEntity()), OpenNamespaceDTO.class);
} catch (Throwable ex) {
throw new RuntimeException(String
.format("Get namespace for appId: %s, cluster: %s, namespace: %s in env: %s failed", appId, clusterName,
namespaceName, env), ex);
}
}
public List<OpenNamespaceDTO> getNamespaces(String appId, String env, String clusterName) {
if (Strings.isNullOrEmpty(clusterName)) {
clusterName = ConfigConsts.CLUSTER_NAME_DEFAULT;
}
checkNotEmpty(appId, "App id");
checkNotEmpty(env, "Env");
String path = String.format("envs/%s/apps/%s/clusters/%s/namespaces", escapePath(env), escapePath(appId),
escapePath(clusterName));
try (CloseableHttpResponse response = get(path)) {
return gson.fromJson(EntityUtils.toString(response.getEntity()), OPEN_NAMESPACE_DTO_LIST_TYPE);
} catch (Throwable ex) {
throw new RuntimeException(String
.format("Get namespaces for appId: %s, cluster: %s in env: %s failed", appId, clusterName, env), ex);
}
}
public OpenAppNamespaceDTO createAppNamespace(OpenAppNamespaceDTO appNamespaceDTO) {
checkNotEmpty(appNamespaceDTO.getAppId(), "App id");
checkNotEmpty(appNamespaceDTO.getName(), "Name");
checkNotEmpty(appNamespaceDTO.getDataChangeCreatedBy(), "Created by");
if (Strings.isNullOrEmpty(appNamespaceDTO.getFormat())) {
appNamespaceDTO.setFormat(ConfigFileFormat.Properties.getValue());
}
String path = String.format("apps/%s/appnamespaces", escapePath(appNamespaceDTO.getAppId()));
try (CloseableHttpResponse response = post(path, appNamespaceDTO)) {
return gson.fromJson(EntityUtils.toString(response.getEntity()), OpenAppNamespaceDTO.class);
} catch (Throwable ex) {
throw new RuntimeException(String
.format("Create app namespace: %s for appId: %s, format: %s failed", appNamespaceDTO.getName(),
appNamespaceDTO.getAppId(), appNamespaceDTO.getFormat()), ex);
}
}
public OpenNamespaceLockDTO getNamespaceLock(String appId, String env, String clusterName, String namespaceName) {
if (Strings.isNullOrEmpty(clusterName)) {
clusterName = ConfigConsts.CLUSTER_NAME_DEFAULT;
}
if (Strings.isNullOrEmpty(namespaceName)) {
namespaceName = ConfigConsts.NAMESPACE_APPLICATION;
}
checkNotEmpty(appId, "App id");
checkNotEmpty(env, "Env");
String path = String.format("envs/%s/apps/%s/clusters/%s/namespaces/%s/lock", escapePath(env), escapePath(appId),
escapePath(clusterName), escapePath(namespaceName));
try (CloseableHttpResponse response = get(path)) {
return gson.fromJson(EntityUtils.toString(response.getEntity()), OpenNamespaceLockDTO.class);
} catch (Throwable ex) {
throw new RuntimeException(String
.format("Get namespace lock for appId: %s, cluster: %s, namespace: %s in env: %s failed", appId, clusterName,
namespaceName, env), ex);
}
}
}
package com.ctrip.framework.apollo.openapi.client.service;
import com.ctrip.framework.apollo.core.ConfigConsts;
import com.ctrip.framework.apollo.openapi.dto.NamespaceReleaseDTO;
import com.ctrip.framework.apollo.openapi.dto.OpenReleaseDTO;
import com.google.common.base.Strings;
import com.google.gson.Gson;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
public class ReleaseOpenApiService extends AbstractOpenApiService {
public ReleaseOpenApiService(CloseableHttpClient client, String baseUrl, Gson gson) {
super(client, baseUrl, gson);
}
public OpenReleaseDTO publishNamespace(String appId, String env, String clusterName, String namespaceName,
NamespaceReleaseDTO releaseDTO) {
if (Strings.isNullOrEmpty(clusterName)) {
clusterName = ConfigConsts.CLUSTER_NAME_DEFAULT;
}
if (Strings.isNullOrEmpty(namespaceName)) {
namespaceName = ConfigConsts.NAMESPACE_APPLICATION;
}
checkNotEmpty(appId, "App id");
checkNotEmpty(env, "Env");
checkNotEmpty(releaseDTO.getReleaseTitle(), "Release title");
checkNotEmpty(releaseDTO.getReleasedBy(), "Released by");
String path = String.format("envs/%s/apps/%s/clusters/%s/namespaces/%s/releases",
escapePath(env), escapePath(appId), escapePath(clusterName), escapePath(namespaceName));
try (CloseableHttpResponse response = post(path, releaseDTO)) {
return gson.fromJson(EntityUtils.toString(response.getEntity()), OpenReleaseDTO.class);
} catch (Throwable ex) {
throw new RuntimeException(String
.format("Release namespace: %s for appId: %s, cluster: %s in env: %s failed", namespaceName, appId,
clusterName, env), ex);
}
}
public OpenReleaseDTO getLatestActiveRelease(String appId, String env, String clusterName, String namespaceName) {
if (Strings.isNullOrEmpty(clusterName)) {
clusterName = ConfigConsts.CLUSTER_NAME_DEFAULT;
}
if (Strings.isNullOrEmpty(namespaceName)) {
namespaceName = ConfigConsts.NAMESPACE_APPLICATION;
}
checkNotEmpty(appId, "App id");
checkNotEmpty(env, "Env");
String path = String.format("envs/%s/apps/%s/clusters/%s/namespaces/%s/releases/latest",
escapePath(env), escapePath(appId), escapePath(clusterName), escapePath(namespaceName));
try (CloseableHttpResponse response = get(path)) {
return gson.fromJson(EntityUtils.toString(response.getEntity()), OpenReleaseDTO.class);
} catch (Throwable ex) {
throw new RuntimeException(String
.format("Get latest active release for appId: %s, cluster: %s, namespace: %s in env: %s failed", appId,
clusterName, namespaceName, env), ex);
}
}
}
package com.ctrip.framework.apollo.openapi.dto;
import java.util.Date;
public class BaseDTO {
protected String dataChangeCreatedBy;
protected String dataChangeLastModifiedBy;
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 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;
}
}
package com.ctrip.framework.apollo.openapi.dto;
import java.util.Set;
public class NamespaceGrayDelReleaseDTO extends NamespaceReleaseDTO {
private Set<String> grayDelKeys;
public Set<String> getGrayDelKeys() {
return grayDelKeys;
}
public void setGrayDelKeys(Set<String> grayDelKeys) {
this.grayDelKeys = grayDelKeys;
}
}
package com.ctrip.framework.apollo.openapi.dto;
public class NamespaceReleaseDTO {
private String releaseTitle;
private String releaseComment;
private String releasedBy;
private boolean isEmergencyPublish;
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;
}
}
package com.ctrip.framework.apollo.openapi.dto;
import com.ctrip.framework.apollo.common.dto.BaseDTO;
public class OpenAppNamespaceDTO extends BaseDTO {
private String name;
......
package com.ctrip.framework.apollo.openapi.dto;
import java.util.Set;
public class OpenGrayReleaseRuleDTO extends BaseDTO{
private String appId;
private String clusterName;
private String namespaceName;
private String branchName;
private Set<OpenGrayReleaseRuleItemDTO> ruleItems;
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 Set<OpenGrayReleaseRuleItemDTO> getRuleItems() {
return ruleItems;
}
public void setRuleItems(Set<OpenGrayReleaseRuleItemDTO> ruleItems) {
this.ruleItems = ruleItems;
}
}
package com.ctrip.framework.apollo.openapi.dto;
import java.util.Set;
public class OpenGrayReleaseRuleItemDTO {
private String clientAppId;
private Set<String> clientIpList;
public String getClientAppId() {
return clientAppId;
}
public void setClientAppId(String clientAppId) {
this.clientAppId = clientAppId;
}
public Set<String> getClientIpList() {
return clientIpList;
}
public void setClientIpList(Set<String> clientIpList) {
this.clientIpList = clientIpList;
}
}
package com.ctrip.framework.apollo.openapi.dto;
import com.ctrip.framework.apollo.common.dto.BaseDTO;
public class OpenItemDTO extends BaseDTO {
private String key;
......
package com.ctrip.framework.apollo.openapi.dto;
import com.ctrip.framework.apollo.common.dto.BaseDTO;
import java.util.List;
public class OpenNamespaceDTO extends BaseDTO {
......
package com.ctrip.framework.apollo.openapi.dto;
import com.ctrip.framework.apollo.common.dto.BaseDTO;
import java.util.Map;
public class OpenReleaseDTO extends BaseDTO {
......
package com.ctrip.framework.apollo.openapi.client;
import static org.junit.Assert.*;
import org.junit.Test;
public class ApolloOpenApiClientTest {
@Test
public void testCreate() {
String someUrl = "http://someUrl";
String someToken = "someToken";
ApolloOpenApiClient client = ApolloOpenApiClient.newBuilder().withPortalUrl(someUrl).withToken(someToken).build();
assertEquals(someUrl, client.getPortalUrl());
assertEquals(someToken, client.getToken());
}
@Test(expected = IllegalArgumentException.class)
public void testCreateWithInvalidUrl() {
String someInvalidUrl = "someInvalidUrl";
String someToken = "someToken";
ApolloOpenApiClient.newBuilder().withPortalUrl(someInvalidUrl).withToken(someToken).build();
}
}
package com.ctrip.framework.apollo.openapi.client.service;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.when;
import com.ctrip.framework.apollo.openapi.client.constant.ApolloOpenApiConstants;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import org.apache.http.StatusLine;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.impl.client.CloseableHttpClient;
import org.junit.Before;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
@RunWith(MockitoJUnitRunner.class)
abstract class AbstractOpenApiServiceTest {
@Mock
protected CloseableHttpClient httpClient;
@Mock
protected CloseableHttpResponse someHttpResponse;
@Mock
protected StatusLine statusLine;
protected Gson gson;
protected String someBaseUrl;
@Before
public void setUp() throws Exception {
gson = new GsonBuilder().setDateFormat(ApolloOpenApiConstants.JSON_DATE_FORMAT).create();
someBaseUrl = "http://someBaseUrl";
when(someHttpResponse.getStatusLine()).thenReturn(statusLine);
when(statusLine.getStatusCode()).thenReturn(200);
when(httpClient.execute(any(HttpUriRequest.class))).thenReturn(someHttpResponse);
}
}
package com.ctrip.framework.apollo.openapi.client.service;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.entity.StringEntity;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
public class AppOpenApiServiceTest extends AbstractOpenApiServiceTest {
private AppOpenApiService appOpenApiService;
private String someAppId;
@Override
@Before
public void setUp() throws Exception {
super.setUp();
someAppId = "someAppId";
StringEntity responseEntity = new StringEntity("[]");
when(someHttpResponse.getEntity()).thenReturn(responseEntity);
appOpenApiService = new AppOpenApiService(httpClient, someBaseUrl, gson);
}
@Test
public void testGetEnvClusterInfo() throws Exception {
final ArgumentCaptor<HttpGet> request = ArgumentCaptor.forClass(HttpGet.class);
appOpenApiService.getEnvClusterInfo(someAppId);
verify(httpClient, times(1)).execute(request.capture());
HttpGet get = request.getValue();
assertEquals(String
.format("%s/apps/%s/envclusters", someBaseUrl, someAppId), get.getURI().toString());
}
@Test(expected = RuntimeException.class)
public void testGetEnvClusterInfoWithError() throws Exception {
when(statusLine.getStatusCode()).thenReturn(500);
appOpenApiService.getEnvClusterInfo(someAppId);
}
}
package com.ctrip.framework.apollo.openapi.client.service;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import com.ctrip.framework.apollo.openapi.dto.OpenItemDTO;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.util.EntityUtils;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
public class ItemOpenApiServiceTest extends AbstractOpenApiServiceTest {
private ItemOpenApiService itemOpenApiService;
private String someAppId;
private String someEnv;
private String someCluster;
private String someNamespace;
@Override
@Before
public void setUp() throws Exception {
super.setUp();
someAppId = "someAppId";
someEnv = "someEnv";
someCluster = "someCluster";
someNamespace = "someNamespace";
StringEntity responseEntity = new StringEntity("{}");
when(someHttpResponse.getEntity()).thenReturn(responseEntity);
itemOpenApiService = new ItemOpenApiService(httpClient, someBaseUrl, gson);
}
@Test
public void testCreateItem() throws Exception {
String someKey = "someKey";
String someValue = "someValue";
String someCreatedBy = "someCreatedBy";
OpenItemDTO itemDTO = new OpenItemDTO();
itemDTO.setKey(someKey);
itemDTO.setValue(someValue);
itemDTO.setDataChangeCreatedBy(someCreatedBy);
final ArgumentCaptor<HttpPost> request = ArgumentCaptor.forClass(HttpPost.class);
itemOpenApiService.createItem(someAppId, someEnv, someCluster, someNamespace, itemDTO);
verify(httpClient, times(1)).execute(request.capture());
HttpPost post = request.getValue();
assertEquals(String
.format("%s/envs/%s/apps/%s/clusters/%s/namespaces/%s/items", someBaseUrl, someEnv, someAppId, someCluster,
someNamespace), post.getURI().toString());
StringEntity entity = (StringEntity) post.getEntity();
assertEquals(ContentType.APPLICATION_JSON.toString(), entity.getContentType().getValue());
assertEquals(gson.toJson(itemDTO), EntityUtils.toString(entity));
}
@Test(expected = RuntimeException.class)
public void testCreateItemWithError() throws Exception {
String someKey = "someKey";
String someValue = "someValue";
String someCreatedBy = "someCreatedBy";
OpenItemDTO itemDTO = new OpenItemDTO();
itemDTO.setKey(someKey);
itemDTO.setValue(someValue);
itemDTO.setDataChangeCreatedBy(someCreatedBy);
when(statusLine.getStatusCode()).thenReturn(400);
itemOpenApiService.createItem(someAppId, someEnv, someCluster, someNamespace, itemDTO);
}
@Test
public void testUpdateItem() throws Exception {
String someKey = "someKey";
String someValue = "someValue";
String someModifiedBy = "someModifiedBy";
OpenItemDTO itemDTO = new OpenItemDTO();
itemDTO.setKey(someKey);
itemDTO.setValue(someValue);
itemDTO.setDataChangeLastModifiedBy(someModifiedBy);
final ArgumentCaptor<HttpPut> request = ArgumentCaptor.forClass(HttpPut.class);
itemOpenApiService.updateItem(someAppId, someEnv, someCluster, someNamespace, itemDTO);
verify(httpClient, times(1)).execute(request.capture());
HttpPut put = request.getValue();
assertEquals(String
.format("%s/envs/%s/apps/%s/clusters/%s/namespaces/%s/items/%s", someBaseUrl, someEnv, someAppId, someCluster,
someNamespace, someKey), put.getURI().toString());
}
@Test(expected = RuntimeException.class)
public void testUpdateItemWithError() throws Exception {
String someKey = "someKey";
String someValue = "someValue";
String someModifiedBy = "someModifiedBy";
OpenItemDTO itemDTO = new OpenItemDTO();
itemDTO.setKey(someKey);
itemDTO.setValue(someValue);
itemDTO.setDataChangeLastModifiedBy(someModifiedBy);
when(statusLine.getStatusCode()).thenReturn(400);
itemOpenApiService.updateItem(someAppId, someEnv, someCluster, someNamespace, itemDTO);
}
@Test
public void testCreateOrUpdateItem() throws Exception {
String someKey = "someKey";
String someValue = "someValue";
String someCreatedBy = "someCreatedBy";
OpenItemDTO itemDTO = new OpenItemDTO();
itemDTO.setKey(someKey);
itemDTO.setValue(someValue);
itemDTO.setDataChangeCreatedBy(someCreatedBy);
final ArgumentCaptor<HttpPut> request = ArgumentCaptor.forClass(HttpPut.class);
itemOpenApiService.createOrUpdateItem(someAppId, someEnv, someCluster, someNamespace, itemDTO);
verify(httpClient, times(1)).execute(request.capture());
HttpPut put = request.getValue();
assertEquals(String
.format("%s/envs/%s/apps/%s/clusters/%s/namespaces/%s/items/%s?createIfNotExists=true", someBaseUrl, someEnv,
someAppId, someCluster, someNamespace, someKey), put.getURI().toString());
}
@Test(expected = RuntimeException.class)
public void testCreateOrUpdateItemWithError() throws Exception {
String someKey = "someKey";
String someValue = "someValue";
String someCreatedBy = "someCreatedBy";
OpenItemDTO itemDTO = new OpenItemDTO();
itemDTO.setKey(someKey);
itemDTO.setValue(someValue);
itemDTO.setDataChangeCreatedBy(someCreatedBy);
when(statusLine.getStatusCode()).thenReturn(400);
itemOpenApiService.createOrUpdateItem(someAppId, someEnv, someCluster, someNamespace, itemDTO);
}
@Test
public void testRemoveItem() throws Exception {
String someKey = "someKey";
String someOperator = "someOperator";
final ArgumentCaptor<HttpDelete> request = ArgumentCaptor.forClass(HttpDelete.class);
itemOpenApiService.removeItem(someAppId, someEnv, someCluster, someNamespace, someKey, someOperator);
verify(httpClient, times(1)).execute(request.capture());
HttpDelete delete = request.getValue();
assertEquals(String
.format("%s/envs/%s/apps/%s/clusters/%s/namespaces/%s/items/%s?operator=%s", someBaseUrl, someEnv,
someAppId, someCluster, someNamespace, someKey, someOperator), delete.getURI().toString());
}
@Test(expected = RuntimeException.class)
public void testRemoveItemWithError() throws Exception {
String someKey = "someKey";
String someOperator = "someOperator";
when(statusLine.getStatusCode()).thenReturn(404);
itemOpenApiService.removeItem(someAppId, someEnv, someCluster, someNamespace, someKey, someOperator);
}
}
package com.ctrip.framework.apollo.openapi.client.service;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import com.ctrip.framework.apollo.openapi.dto.OpenAppNamespaceDTO;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
public class NamespaceOpenApiServiceTest extends AbstractOpenApiServiceTest {
private NamespaceOpenApiService namespaceOpenApiService;
private String someAppId;
private String someEnv;
private String someCluster;
private String someNamespace;
@Override
@Before
public void setUp() throws Exception {
super.setUp();
someAppId = "someAppId";
someEnv = "someEnv";
someCluster = "someCluster";
someNamespace = "someNamespace";
StringEntity responseEntity = new StringEntity("{}");
when(someHttpResponse.getEntity()).thenReturn(responseEntity);
namespaceOpenApiService = new NamespaceOpenApiService(httpClient, someBaseUrl, gson);
}
@Test
public void testGetNamespace() throws Exception {
final ArgumentCaptor<HttpGet> request = ArgumentCaptor.forClass(HttpGet.class);
namespaceOpenApiService.getNamespace(someAppId, someEnv, someCluster, someNamespace);
verify(httpClient, times(1)).execute(request.capture());
HttpGet get = request.getValue();
assertEquals(String
.format("%s/envs/%s/apps/%s/clusters/%s/namespaces/%s", someBaseUrl, someEnv, someAppId, someCluster,
someNamespace), get.getURI().toString());
}
@Test(expected = RuntimeException.class)
public void testGetNamespaceWithError() throws Exception {
when(statusLine.getStatusCode()).thenReturn(404);
namespaceOpenApiService.getNamespace(someAppId, someEnv, someCluster, someNamespace);
}
@Test
public void testGetNamespaces() throws Exception {
StringEntity responseEntity = new StringEntity("[]");
when(someHttpResponse.getEntity()).thenReturn(responseEntity);
final ArgumentCaptor<HttpGet> request = ArgumentCaptor.forClass(HttpGet.class);
namespaceOpenApiService.getNamespaces(someAppId, someEnv, someCluster);
verify(httpClient, times(1)).execute(request.capture());
HttpGet get = request.getValue();
assertEquals(String
.format("%s/envs/%s/apps/%s/clusters/%s/namespaces", someBaseUrl, someEnv, someAppId, someCluster),
get.getURI().toString());
}
@Test(expected = RuntimeException.class)
public void testGetNamespacesWithError() throws Exception {
when(statusLine.getStatusCode()).thenReturn(404);
namespaceOpenApiService.getNamespaces(someAppId, someEnv, someCluster);
}
@Test
public void testCreateAppNamespace() throws Exception {
String someName = "someName";
String someCreatedBy = "someCreatedBy";
OpenAppNamespaceDTO appNamespaceDTO = new OpenAppNamespaceDTO();
appNamespaceDTO.setAppId(someAppId);
appNamespaceDTO.setName(someName);
appNamespaceDTO.setDataChangeCreatedBy(someCreatedBy);
final ArgumentCaptor<HttpPost> request = ArgumentCaptor.forClass(HttpPost.class);
namespaceOpenApiService.createAppNamespace(appNamespaceDTO);
verify(httpClient, times(1)).execute(request.capture());
HttpPost post = request.getValue();
assertEquals(String.format("%s/apps/%s/appnamespaces", someBaseUrl, someAppId), post.getURI().toString());
}
@Test(expected = RuntimeException.class)
public void testCreateAppNamespaceWithError() throws Exception {
String someName = "someName";
String someCreatedBy = "someCreatedBy";
OpenAppNamespaceDTO appNamespaceDTO = new OpenAppNamespaceDTO();
appNamespaceDTO.setAppId(someAppId);
appNamespaceDTO.setName(someName);
appNamespaceDTO.setDataChangeCreatedBy(someCreatedBy);
when(statusLine.getStatusCode()).thenReturn(400);
namespaceOpenApiService.createAppNamespace(appNamespaceDTO);
}
@Test
public void testGetNamespaceLock() throws Exception {
final ArgumentCaptor<HttpGet> request = ArgumentCaptor.forClass(HttpGet.class);
namespaceOpenApiService.getNamespaceLock(someAppId, someEnv, someCluster, someNamespace);
verify(httpClient, times(1)).execute(request.capture());
HttpGet post = request.getValue();
assertEquals(String
.format("%s/envs/%s/apps/%s/clusters/%s/namespaces/%s/lock", someBaseUrl, someEnv, someAppId, someCluster,
someNamespace), post.getURI().toString());
}
@Test(expected = RuntimeException.class)
public void testGetNamespaceLockWithError() throws Exception {
when(statusLine.getStatusCode()).thenReturn(404);
namespaceOpenApiService.getNamespaceLock(someAppId, someEnv, someCluster, someNamespace);
}
}
package com.ctrip.framework.apollo.openapi.client.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.openapi.dto.NamespaceReleaseDTO;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
public class ReleaseOpenApiServiceTest extends AbstractOpenApiServiceTest {
private ReleaseOpenApiService releaseOpenApiService;
private String someAppId;
private String someEnv;
private String someCluster;
private String someNamespace;
@Override
@Before
public void setUp() throws Exception {
super.setUp();
someAppId = "someAppId";
someEnv = "someEnv";
someCluster = "someCluster";
someNamespace = "someNamespace";
StringEntity responseEntity = new StringEntity("{}");
when(someHttpResponse.getEntity()).thenReturn(responseEntity);
releaseOpenApiService = new ReleaseOpenApiService(httpClient, someBaseUrl, gson);
}
@Test
public void testPublishNamespace() throws Exception {
String someReleaseTitle = "someReleaseTitle";
String someReleasedBy = "someReleasedBy";
NamespaceReleaseDTO namespaceReleaseDTO = new NamespaceReleaseDTO();
namespaceReleaseDTO.setReleaseTitle(someReleaseTitle);
namespaceReleaseDTO.setReleasedBy(someReleasedBy);
final ArgumentCaptor<HttpPost> request = ArgumentCaptor.forClass(HttpPost.class);
releaseOpenApiService.publishNamespace(someAppId, someEnv, someCluster, someNamespace, namespaceReleaseDTO);
verify(httpClient, times(1)).execute(request.capture());
HttpPost post = request.getValue();
assertEquals(String
.format("%s/envs/%s/apps/%s/clusters/%s/namespaces/%s/releases", someBaseUrl, someEnv, someAppId, someCluster,
someNamespace), post.getURI().toString());
}
@Test(expected = RuntimeException.class)
public void testPublishNamespaceWithError() throws Exception {
String someReleaseTitle = "someReleaseTitle";
String someReleasedBy = "someReleasedBy";
NamespaceReleaseDTO namespaceReleaseDTO = new NamespaceReleaseDTO();
namespaceReleaseDTO.setReleaseTitle(someReleaseTitle);
namespaceReleaseDTO.setReleasedBy(someReleasedBy);
when(statusLine.getStatusCode()).thenReturn(400);
releaseOpenApiService.publishNamespace(someAppId, someEnv, someCluster, someNamespace, namespaceReleaseDTO);
}
@Test
public void testGetLatestActiveRelease() throws Exception {
final ArgumentCaptor<HttpGet> request = ArgumentCaptor.forClass(HttpGet.class);
releaseOpenApiService.getLatestActiveRelease(someAppId, someEnv, someCluster, someNamespace);
verify(httpClient, times(1)).execute(request.capture());
HttpGet get = request.getValue();
assertEquals(String
.format("%s/envs/%s/apps/%s/clusters/%s/namespaces/%s/releases/latest", someBaseUrl, someEnv, someAppId, someCluster,
someNamespace), get.getURI().toString());
}
@Test(expected = RuntimeException.class)
public void testGetLatestActiveReleaseWithError() throws Exception {
when(statusLine.getStatusCode()).thenReturn(400);
releaseOpenApiService.getLatestActiveRelease(someAppId, someEnv, someCluster, someNamespace);
}
}
<?xml version="1.0" encoding="UTF-8"?>
<configuration monitorInterval="60">
<appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="[apollo-open-api][%t]%d %-5p [%c] %m%n"/>
</Console>
<Async name="Async" includeLocation="true">
<AppenderRef ref="Console"/>
</Async>
</appenders>
<loggers>
<logger name="com.ctrip.framework.apollo" additivity="false" level="trace">
<AppenderRef ref="Async" level="WARN"/>
</logger>
<root level="INFO">
<AppenderRef ref="Async"/>
</root>
</loggers>
</configuration>
......@@ -22,6 +22,10 @@
<groupId>com.ctrip.framework.apollo</groupId>
<artifactId>apollo-common</artifactId>
</dependency>
<dependency>
<groupId>com.ctrip.framework.apollo</groupId>
<artifactId>apollo-openapi</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
......
......@@ -11,8 +11,8 @@ MAINTAINER ameizi <sxyx2008@163.com>
ENV VERSION 1.1.0-SNAPSHOT
RUN echo "http://mirrors.aliyun.com/alpine/v3.6/main" > /etc/apk/repositories \
&& echo "http://mirrors.aliyun.com/alpine/v3.6/community" >> /etc/apk/repositories \
RUN echo "http://mirrors.aliyun.com/alpine/v3.8/main" > /etc/apk/repositories \
&& echo "http://mirrors.aliyun.com/alpine/v3.8/community" >> /etc/apk/repositories \
&& apk update upgrade \
&& apk add --no-cache procps unzip curl bash tzdata \
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
......@@ -25,6 +25,6 @@ RUN unzip /apollo-portal/apollo-portal-${VERSION}-github.zip -d /apollo-portal \
&& sed -i '$d' /apollo-portal/scripts/startup.sh \
&& echo "tail -f /dev/null" >> /apollo-portal/scripts/startup.sh
EXPOSE 8080
EXPOSE 8070
CMD ["/apollo-portal/scripts/startup.sh"]
package com.ctrip.framework.apollo.openapi.util;
import com.ctrip.framework.apollo.common.dto.*;
import com.ctrip.framework.apollo.openapi.dto.*;
import com.google.common.base.Preconditions;
import com.google.common.reflect.TypeToken;
import com.google.gson.Gson;
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.AppNamespace;
import com.ctrip.framework.apollo.common.utils.BeanUtils;
import com.ctrip.framework.apollo.openapi.dto.OpenAppNamespaceDTO;
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.portal.entity.bo.ItemBO;
import com.ctrip.framework.apollo.portal.entity.bo.NamespaceBO;
import org.springframework.util.CollectionUtils;
import java.lang.reflect.Type;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.*;
import java.util.stream.Collectors;
public class OpenApiBeanUtils {
......@@ -116,4 +107,43 @@ public class OpenApiBeanUtils {
return lock;
}
public static OpenGrayReleaseRuleDTO transformFromGrayReleaseRuleDTO(GrayReleaseRuleDTO grayReleaseRuleDTO){
OpenGrayReleaseRuleDTO openGrayReleaseRuleDTO = new OpenGrayReleaseRuleDTO();
openGrayReleaseRuleDTO.setAppId(grayReleaseRuleDTO.getAppId());
openGrayReleaseRuleDTO.setBranchName(grayReleaseRuleDTO.getBranchName());
openGrayReleaseRuleDTO.setClusterName(grayReleaseRuleDTO.getClusterName());
openGrayReleaseRuleDTO.setNamespaceName(grayReleaseRuleDTO.getNamespaceName());
Set<GrayReleaseRuleItemDTO> grayReleaseRuleItemDTOSet = grayReleaseRuleDTO.getRuleItems();
Set<OpenGrayReleaseRuleItemDTO> ruleItems = new HashSet<>();
grayReleaseRuleItemDTOSet.forEach(grayReleaseRuleItemDTO -> {
OpenGrayReleaseRuleItemDTO item = new OpenGrayReleaseRuleItemDTO();
item.setClientAppId(grayReleaseRuleItemDTO.getClientAppId());
item.setClientIpList(grayReleaseRuleItemDTO.getClientIpList());
ruleItems.add(item);
});
openGrayReleaseRuleDTO.setRuleItems(ruleItems);
return openGrayReleaseRuleDTO;
}
public static GrayReleaseRuleDTO transformToGrayReleaseRuleDTO(OpenGrayReleaseRuleDTO openGrayReleaseRuleDTO){
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<OpenGrayReleaseRuleItemDTO> openGrayReleaseRuleItemDTOSet = openGrayReleaseRuleDTO.getRuleItems();
openGrayReleaseRuleItemDTOSet.forEach(openGrayReleaseRuleItemDTO ->{
String clientAppId = openGrayReleaseRuleItemDTO.getClientAppId();
Set<String> clientIpList = openGrayReleaseRuleItemDTO.getClientIpList();
GrayReleaseRuleItemDTO ruleItem = new GrayReleaseRuleItemDTO(clientAppId, clientIpList);
grayReleaseRuleDTO.addRuleItem(ruleItem);
});
return grayReleaseRuleDTO;
}
}
......@@ -11,6 +11,7 @@ import com.ctrip.framework.apollo.portal.service.ItemService;
import com.ctrip.framework.apollo.portal.spi.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
......@@ -20,6 +21,7 @@ import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import org.springframework.web.client.HttpStatusCodeException;
@RestController("openapiItemController")
......@@ -40,10 +42,10 @@ public class ItemController {
RequestPrecondition.checkArguments(
!StringUtils.isContainEmpty(item.getKey(), item.getValue(), item.getDataChangeCreatedBy()),
"key,value,dataChangeCreatedBy 字段不能为空");
"key, value and dataChangeCreatedBy should not be null or empty");
if (userService.findByUserId(item.getDataChangeCreatedBy()) == null) {
throw new BadRequestException("用户不存在.");
throw new BadRequestException("User " + item.getDataChangeCreatedBy() + " doesn't exist!");
}
ItemDTO toCreate = OpenApiBeanUtils.transformToItemDTO(item);
......@@ -64,13 +66,14 @@ public class ItemController {
@RequestMapping(value = "/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/items/{key:.+}", method = RequestMethod.PUT)
public void updateItem(@PathVariable String appId, @PathVariable String env,
@PathVariable String clusterName, @PathVariable String namespaceName,
@PathVariable String key, @RequestBody OpenItemDTO item, HttpServletRequest request) {
@PathVariable String key, @RequestBody OpenItemDTO item,
@RequestParam(defaultValue = "false") boolean createIfNotExists, HttpServletRequest request) {
RequestPrecondition.checkArguments(item != null, "item payload can not be empty");
RequestPrecondition.checkArguments(
!StringUtils.isContainEmpty(item.getKey(), item.getValue(), item.getDataChangeLastModifiedBy()),
"key,value,dataChangeLastModifiedBy can not be empty");
"key, value and dataChangeLastModifiedBy can not be empty");
RequestPrecondition.checkArguments(item.getKey().equals(key), "Key in path and payload is not consistent");
......@@ -78,16 +81,25 @@ public class ItemController {
throw new BadRequestException("user(dataChangeLastModifiedBy) not exists");
}
ItemDTO toUpdateItem = itemService.loadItem(Env.fromString(env), appId, clusterName, namespaceName, item.getKey());
if (toUpdateItem == null) {
throw new BadRequestException("item not exists");
try {
ItemDTO toUpdateItem = itemService
.loadItem(Env.fromString(env), appId, clusterName, namespaceName, item.getKey());
//protect. only value,comment,lastModifiedBy can be modified
toUpdateItem.setComment(item.getComment());
toUpdateItem.setValue(item.getValue());
toUpdateItem.setDataChangeLastModifiedBy(item.getDataChangeLastModifiedBy());
itemService.updateItem(appId, Env.fromString(env), clusterName, namespaceName, toUpdateItem);
} catch (Throwable ex) {
if (ex instanceof HttpStatusCodeException) {
// check createIfNotExists
if (((HttpStatusCodeException) ex).getStatusCode().equals(HttpStatus.NOT_FOUND) && createIfNotExists) {
createItem(appId, env, clusterName, namespaceName, item, request);
return;
}
}
throw ex;
}
//protect. only value,comment,lastModifiedBy can be modified
toUpdateItem.setComment(item.getComment());
toUpdateItem.setValue(item.getValue());
toUpdateItem.setDataChangeLastModifiedBy(item.getDataChangeLastModifiedBy());
itemService.updateItem(appId, Env.fromString(env), clusterName, namespaceName, toUpdateItem);
}
......
package com.ctrip.framework.apollo.openapi.v1.controller;
/**
* Created by qianjie on 8/10/17.
*/
import com.ctrip.framework.apollo.common.dto.GrayReleaseRuleDTO;
import com.ctrip.framework.apollo.common.dto.NamespaceDTO;
import com.ctrip.framework.apollo.common.utils.BeanUtils;
import com.ctrip.framework.apollo.core.enums.Env;
import com.ctrip.framework.apollo.openapi.auth.ConsumerPermissionValidator;
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.component.PermissionValidator;
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 org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
@RestController("openapiNamespaceBranchController")
@RequestMapping("/openapi/v1/envs/{env}")
public class NamespaceBranchController {
@Autowired
private ConsumerPermissionValidator consumerPermissionValidator;
@Autowired
private ReleaseService releaseService;
@Autowired
private NamespaceBranchService namespaceBranchService;
@RequestMapping(value = "/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/branches", method = RequestMethod.GET)
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 = "@consumerPermissionValidator.hasCreateNamespacePermission(#request, #appId)")
@RequestMapping(value = "/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/branches", method = RequestMethod.POST)
public OpenNamespaceDTO createBranch(@PathVariable String appId,
@PathVariable String env,
@PathVariable String clusterName,
@PathVariable String namespaceName,
@RequestParam("operator") String operator,
HttpServletRequest request) {
NamespaceDTO namespaceDTO = namespaceBranchService.createBranch(appId, Env.valueOf(env.toUpperCase()), clusterName, namespaceName, operator);
if (namespaceDTO == null) {
return null;
}
return BeanUtils.transfrom(OpenNamespaceDTO.class, namespaceDTO);
}
@PreAuthorize(value = "@consumerPermissionValidator.hasModifyNamespacePermission(#request, #appId, #namespaceName, #env)")
@RequestMapping(value = "/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/branches/{branchName}", method = RequestMethod.DELETE)
public void deleteBranch(@PathVariable String appId,
@PathVariable String env,
@PathVariable String clusterName,
@PathVariable String namespaceName,
@PathVariable String branchName,
@RequestParam("operator") String operator,
HttpServletRequest request) {
boolean canDelete = consumerPermissionValidator.hasReleaseNamespacePermission(request, appId, namespaceName, env) ||
(consumerPermissionValidator.hasModifyNamespacePermission(request, appId, namespaceName, env) &&
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);
}
@RequestMapping(value = "/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/branches/{branchName}/rules", method = RequestMethod.GET)
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 = "@consumerPermissionValidator.hasModifyNamespacePermission(#request, #appId, #namespaceName, #env)")
@RequestMapping(value = "/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/branches/{branchName}/rules", method = RequestMethod.PUT)
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,
HttpServletRequest request) {
GrayReleaseRuleDTO grayReleaseRuleDTO = OpenApiBeanUtils.transformToGrayReleaseRuleDTO(rules);
namespaceBranchService
.updateBranchGrayRules(appId, Env.valueOf(env.toUpperCase()), clusterName, namespaceName, branchName, grayReleaseRuleDTO, operator);
}
}
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.core.enums.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.service.NamespaceBranchService;
import com.ctrip.framework.apollo.portal.service.ReleaseService;
import com.ctrip.framework.apollo.portal.spi.UserService;
......@@ -18,6 +22,7 @@ import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
......@@ -32,13 +37,15 @@ public class ReleaseController {
private ReleaseService releaseService;
@Autowired
private UserService userService;
@Autowired
private NamespaceBranchService namespaceBranchService;
@PreAuthorize(value = "@consumerPermissionValidator.hasReleaseNamespacePermission(#request, #appId, #namespaceName, #env)")
@RequestMapping(value = "/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/releases", method = RequestMethod.POST)
public OpenReleaseDTO createRelease(@PathVariable String appId, @PathVariable String env,
@PathVariable String clusterName,
@PathVariable String namespaceName,
@RequestBody NamespaceReleaseModel model,
@RequestBody NamespaceReleaseDTO model,
HttpServletRequest request) {
checkModel(model != null);
......@@ -50,12 +57,14 @@ public class ReleaseController {
throw new BadRequestException("user(releaseBy) not exists");
}
model.setAppId(appId);
model.setEnv(Env.fromString(env).toString());
model.setClusterName(clusterName);
model.setNamespaceName(namespaceName);
NamespaceReleaseModel releaseModel = BeanUtils.transfrom(NamespaceReleaseModel.class, model);
releaseModel.setAppId(appId);
releaseModel.setEnv(Env.fromString(env).toString());
releaseModel.setClusterName(clusterName);
releaseModel.setNamespaceName(namespaceName);
return OpenApiBeanUtils.transformFromReleaseDTO(releaseService.publish(model));
return OpenApiBeanUtils.transformFromReleaseDTO(releaseService.publish(releaseModel));
}
@RequestMapping(value = "/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/releases/latest", method = RequestMethod.GET)
......@@ -71,4 +80,82 @@ public class ReleaseController {
return OpenApiBeanUtils.transformFromReleaseDTO(releaseDTO);
}
@PreAuthorize(value = "@consumerPermissionValidator.hasReleaseNamespacePermission(#request, #appId, #namespaceName, #env)")
@RequestMapping(value = "/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/branches/{branchName}/merge", method = RequestMethod.POST)
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, HttpServletRequest request) {
checkModel(model != null);
RequestPrecondition.checkArguments(!StringUtils.isContainEmpty(model.getReleasedBy(), model
.getReleaseTitle()),
"Params(releaseTitle and releasedBy) can not be empty");
if (userService.findByUserId(model.getReleasedBy()) == null) {
throw new BadRequestException("user(releaseBy) not exists");
}
ReleaseDTO mergedRelease = namespaceBranchService.merge(appId, Env.valueOf(env.toUpperCase()), clusterName, namespaceName, branchName,
model.getReleaseTitle(), model.getReleaseComment(),
model.isEmergencyPublish(), deleteBranch, model.getReleasedBy());
return OpenApiBeanUtils.transformFromReleaseDTO(mergedRelease);
}
@PreAuthorize(value = "@consumerPermissionValidator.hasReleaseNamespacePermission(#request, #appId, #namespaceName, #env)")
@RequestMapping(value = "/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/branches/{branchName}/releases",
method = RequestMethod.POST)
public OpenReleaseDTO createGrayRelease(@PathVariable String appId,
@PathVariable String env, @PathVariable String clusterName,
@PathVariable String namespaceName, @PathVariable String branchName,
@RequestBody NamespaceReleaseDTO model,
HttpServletRequest request) {
checkModel(model != null);
RequestPrecondition.checkArguments(!StringUtils.isContainEmpty(model.getReleasedBy(), model
.getReleaseTitle()),
"Params(releaseTitle and releasedBy) can not be empty");
if (userService.findByUserId(model.getReleasedBy()) == null) {
throw new BadRequestException("user(releaseBy) not exists");
}
NamespaceReleaseModel releaseModel = BeanUtils.transfrom(NamespaceReleaseModel.class, model);
releaseModel.setAppId(appId);
releaseModel.setEnv(Env.fromString(env).toString());
releaseModel.setClusterName(branchName);
releaseModel.setNamespaceName(namespaceName);
return OpenApiBeanUtils.transformFromReleaseDTO(releaseService.publish(releaseModel));
}
@PreAuthorize(value = "@consumerPermissionValidator.hasReleaseNamespacePermission(#request, #appId, #namespaceName, #env)")
@RequestMapping(value = "/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/branches/{branchName}/gray-del-releases",
method = RequestMethod.POST)
public OpenReleaseDTO createGrayDelRelease(@PathVariable String appId,
@PathVariable String env, @PathVariable String clusterName,
@PathVariable String namespaceName, @PathVariable String branchName,
@RequestBody NamespaceGrayDelReleaseDTO model,
HttpServletRequest request) {
checkModel(model != null);
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 new BadRequestException("user(releaseBy) not exists");
}
NamespaceGrayDelReleaseModel releaseModel = BeanUtils.transfrom(NamespaceGrayDelReleaseModel.class, model);
releaseModel.setAppId(appId);
releaseModel.setEnv(env.toUpperCase());
releaseModel.setClusterName(branchName);
releaseModel.setNamespaceName(namespaceName);
return OpenApiBeanUtils.transformFromReleaseDTO(releaseService.publish(releaseModel, releaseModel.getReleasedBy()));
}
}
......@@ -4,10 +4,7 @@ import com.ctrip.framework.apollo.common.ApolloCommonConfig;
import com.ctrip.framework.apollo.openapi.PortalOpenApiConfig;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.actuate.system.ApplicationPidFileWriter;
import org.springframework.boot.actuate.system.EmbeddedServerPortFileWriter;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
......@@ -22,8 +19,6 @@ import org.springframework.transaction.annotation.EnableTransactionManagement;
public class PortalApplication {
public static void main(String[] args) throws Exception {
ConfigurableApplicationContext context = SpringApplication.run(PortalApplication.class, args);
context.addApplicationListener(new ApplicationPidFileWriter());
context.addApplicationListener(new EmbeddedServerPortFileWriter());
SpringApplication.run(PortalApplication.class, args);
}
}
......@@ -274,6 +274,27 @@ public class AdminServiceAPI {
return response;
}
public ReleaseDTO createGrayDeletionRelease(String appId, Env env, String clusterName, String namespace,
String releaseName, String releaseComment, String operator,
boolean isEmergencyPublish, Set<String> grayDelKeys) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.parseMediaType(MediaType.APPLICATION_FORM_URLENCODED_VALUE + ";charset=UTF-8"));
MultiValueMap<String, String> 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<MultiValueMap<String, String>> entity =
new HttpEntity<>(parameters, headers);
ReleaseDTO response = restTemplate.post(
env, "apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/gray-del-releases", entity,
ReleaseDTO.class, appId, clusterName, namespace);
return response;
}
public ReleaseDTO updateAndPublish(String appId, Env env, String clusterName, String namespace,
String releaseName, String releaseComment, String branchName,
boolean isEmergencyPublish, boolean deleteBranch, ItemChangeSets changeSets) {
......
......@@ -161,16 +161,18 @@ public class NamespaceController {
@PreAuthorize(value = "@permissionValidator.hasCreateAppNamespacePermission(#appId, #appNamespace)")
@RequestMapping(value = "/apps/{appId}/appnamespaces", method = RequestMethod.POST)
public AppNamespace createAppNamespace(@PathVariable String appId, @RequestBody AppNamespace appNamespace) {
public AppNamespace createAppNamespace(@PathVariable String appId,
@RequestParam(defaultValue = "true") boolean appendNamespacePrefix,
@RequestBody AppNamespace appNamespace) {
RequestPrecondition.checkArgumentsNotEmpty(appNamespace.getAppId(), appNamespace.getName());
if (!InputValidator.isValidAppNamespace(appNamespace.getName())) {
throw new BadRequestException(String.format("Namespace格式错误: %s",
InputValidator.INVALID_CLUSTER_NAMESPACE_MESSAGE + " & "
+ InputValidator.INVALID_NAMESPACE_NAMESPACE_MESSAGE));
InputValidator.INVALID_CLUSTER_NAMESPACE_MESSAGE + " & "
+ InputValidator.INVALID_NAMESPACE_NAMESPACE_MESSAGE));
}
AppNamespace createdAppNamespace = appNamespaceService.createAppNamespaceInLocal(appNamespace);
AppNamespace createdAppNamespace = appNamespaceService.createAppNamespaceInLocal(appNamespace, appendNamespacePrefix);
if (portalConfig.canAppAdminCreatePrivateNamespace() || createdAppNamespace.isPublic()) {
assignNamespaceRoleToOperator(appId, appNamespace.getName());
......
package com.ctrip.framework.apollo.portal.entity.model;
import java.util.Set;
public class NamespaceGrayDelReleaseModel extends NamespaceReleaseModel implements Verifiable {
private Set<String> grayDelKeys;
public Set<String> getGrayDelKeys() {
return grayDelKeys;
}
public void setGrayDelKeys(Set<String> grayDelKeys) {
this.grayDelKeys = grayDelKeys;
}
}
......@@ -12,7 +12,7 @@ public interface AppNamespaceRepository extends PagingAndSortingRepository<AppNa
AppNamespace findByName(String namespaceName);
AppNamespace findByNameAndIsPublic(String namespaceName, boolean isPublic);
List<AppNamespace> findByNameAndIsPublic(String namespaceName, boolean isPublic);
List<AppNamespace> findByIsPublicTrue();
......
......@@ -9,16 +9,23 @@ 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.Sets;
import java.util.Set;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Objects;
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();
@Autowired
private UserInfoHolder userInfoHolder;
@Autowired
......@@ -38,7 +45,17 @@ public class AppNamespaceService {
}
public AppNamespace findPublicAppNamespace(String namespaceName) {
return appNamespaceRepository.findByNameAndIsPublic(namespaceName, true);
List<AppNamespace> appNamespaces = appNamespaceRepository.findByNameAndIsPublic(namespaceName, true);
if (CollectionUtils.isEmpty(appNamespaces)) {
return null;
}
return appNamespaces.get(0);
}
private List<AppNamespace> findAllPrivateAppNamespaces(String namespaceName) {
return appNamespaceRepository.findByNameAndIsPublic(namespaceName, false);
}
public AppNamespace findByAppIdAndName(String appId, String namespaceName) {
......@@ -69,8 +86,12 @@ public class AppNamespaceService {
return Objects.isNull(appNamespaceRepository.findByAppIdAndName(appId, namespaceName));
}
@Transactional
public AppNamespace createAppNamespaceInLocal(AppNamespace appNamespace) {
return createAppNamespaceInLocal(appNamespace, true);
}
@Transactional
public AppNamespace createAppNamespaceInLocal(AppNamespace appNamespace, boolean appendNamespacePrefix) {
String appId = appNamespace.getAppId();
//add app org id as prefix
......@@ -82,7 +103,7 @@ public class AppNamespaceService {
StringBuilder appNamespaceName = new StringBuilder();
//add prefix postfix
appNamespaceName
.append(appNamespace.isPublic() ? app.getOrgId() + "." : "")
.append(appNamespace.isPublic() && appendNamespacePrefix ? app.getOrgId() + "." : "")
.append(appNamespace.getName())
.append(appNamespace.formatAsEnum() == ConfigFileFormat.Properties ? "" : "." + appNamespace.getFormat());
appNamespace.setName(appNamespaceName.toString());
......@@ -103,12 +124,9 @@ public class AppNamespaceService {
appNamespace.setDataChangeLastModifiedBy(operator);
// unique check
// globally uniqueness check
if (appNamespace.isPublic()) {
AppNamespace publicAppNamespace = findPublicAppNamespace(appNamespace.getName());
if (publicAppNamespace != null) {
throw new BadRequestException("Public AppNamespace " + appNamespace.getName() + " already exists in appId: " + publicAppNamespace.getAppId() + "!");
}
checkAppNamespaceGlobalUniqueness(appNamespace);
}
if (!appNamespace.isPublic() &&
......@@ -124,6 +142,30 @@ public class AppNamespaceService {
return createdAppNamespace;
}
private void checkAppNamespaceGlobalUniqueness(AppNamespace appNamespace) {
AppNamespace publicAppNamespace = findPublicAppNamespace(appNamespace.getName());
if (publicAppNamespace != null) {
throw new BadRequestException("Public AppNamespace " + appNamespace.getName() + " already exists in appId: " + publicAppNamespace.getAppId() + "!");
}
List<AppNamespace> privateAppNamespaces = findAllPrivateAppNamespaces(appNamespace.getName());
if (!CollectionUtils.isEmpty(privateAppNamespaces)) {
Set<String> 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!");
}
}
@Transactional
public AppNamespace deleteAppNamespace(String appId, String namespaceName) {
AppNamespace appNamespace = appNamespaceRepository.findByAppIdAndName(appId, namespaceName);
......
......@@ -40,11 +40,17 @@ public class NamespaceBranchService {
@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,
userInfoHolder.getUser().getUserId());
operator);
Tracer.logEvent(TracerEventType.CREATE_GRAY_RELEASE, String.format("%s+%s+%s+%s", appId, env, parentClusterName,
namespaceName));
namespaceName));
return createdBranch;
}
......@@ -59,45 +65,61 @@ public class NamespaceBranchService {
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));
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));
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);
ItemChangeSets changeSets = calculateBranchChangeSet(appId, env, clusterName, namespaceName, branchName, operator);
ReleaseDTO mergedResult =
releaseService.updateAndPublish(appId, env, clusterName, namespaceName, title, comment,
branchName, isEmergencyPublish, deleteBranch, changeSets);
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));
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 branchName, String operator) {
NamespaceBO parentNamespace = namespaceService.loadNamespaceBO(appId, env, clusterName, namespaceName);
if (parentNamespace == null) {
......@@ -115,7 +137,7 @@ public class NamespaceBranchService {
ItemChangeSets changeSets = itemsComparator.compareIgnoreBlankAndCommentItem(parentNamespace.getBaseInfo().getId(),
masterItems, branchItems);
changeSets.setDeleteItems(Collections.emptyList());
changeSets.setDataChangeLastModifiedBy(userInfoHolder.getUser().getUserId());
changeSets.setDataChangeLastModifiedBy(operator);
return changeSets;
}
......
package com.ctrip.framework.apollo.portal.service;
import com.ctrip.framework.apollo.portal.entity.model.NamespaceGrayDelReleaseModel;
import com.google.common.base.Objects;
import com.google.gson.Gson;
......@@ -60,6 +61,24 @@ public class ReleaseService {
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) {
......
......@@ -10,7 +10,8 @@ server:
port: 8080
logging:
file: /opt/logs/100003173/apollo-portal.log
path: /opt/logs/100003173
file: ${logging.path}/apollo-portal.log
endpoints:
health:
......
......@@ -3,10 +3,22 @@
<include resource="org/springframework/boot/logging/logback/defaults.xml" />
<property name="LOG_FILE"
value="${LOG_FILE:-${LOG_PATH:-${LOG_TEMP:-${java.io.tmpdir:-/tmp}}/}apollo-portal.log}" />
<property name="LOG_PATH" value="${LOG_PATH:-${LOG_TEMP:-${java.io.tmpdir:-/tmp}}}" />
<include resource="org/springframework/boot/logging/logback/file-appender.xml" />
<include resource="org/springframework/boot/logging/logback/console-appender.xml" />
<root level="INFO">
<appender-ref ref="FILE" />
<appender-ref ref="CONSOLE" />
</root>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/apollo-portal.%d{yyyy-MM-dd}.%i.log}</fileNamePattern>
<maxFileSize>50MB</maxFileSize>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
</encoder>
</appender>
</configuration>
......@@ -95,9 +95,9 @@
<label class="col-sm-3 control-label">
<apollorequiredfield></apollorequiredfield>
名称</label>
<div class="col-sm-4" valdr-form-group>
<div ng-class="{'input-group':appNamespace.isPublic}">
<span class="input-group-addon" ng-show="appNamespace.isPublic"
<div class="col-sm-5" valdr-form-group>
<div ng-class="{'input-group':appNamespace.isPublic && appendNamespacePrefix}">
<span class="input-group-addon" ng-show="appNamespace.isPublic && appendNamespacePrefix"
ng-bind="appBaseInfo.namespacePrefix"></span>
<input type="text" name="namespaceName" class="form-control"
ng-model="appNamespace.name">
......@@ -116,9 +116,25 @@
</div>
&nbsp;&nbsp;
<span ng-show="appNamespace.isPublic" ng-bind="concatNamespace()"
<span ng-show="appNamespace.isPublic && appendNamespacePrefix" ng-bind="concatNamespace()"
style="line-height: 34px;"></span>
</div>
<div class="form-group" ng-show="type == 'create' && appNamespace.isPublic">
<label class="col-sm-3 control-label">
自动添加部门前缀
</label>
<div class="col-sm-6" valdr-form-group>
<div>
<label class="checkbox-inline">
<input type="checkbox" ng-model="appendNamespacePrefix" />
{{appBaseInfo.namespacePrefix}}
</label>
</div>
<small>(公共Namespace的名称需要全局唯一,添加部门前缀有助于保证全局唯一性)</small>
</div>
</div>
<div class="form-group" ng-show="type == 'create' && (pageSetting.canAppAdminCreatePrivateNamespace || hasRootPermission)">
<label class="col-sm-3 control-label">
<apollorequiredfield></apollorequiredfield>
......
......@@ -11,6 +11,7 @@ namespace_module.controller("LinkNamespaceController",
$scope.step = 1;
$scope.submitBtnDisabled = false;
$scope.appendNamespacePrefix = true;
PermissionService.has_root_permission().then(function (result) {
$scope.hasRootPermission = result.hasPermission;
......@@ -125,7 +126,9 @@ namespace_module.controller("LinkNamespaceController",
}
$scope.submitBtnDisabled = true;
NamespaceService.createAppNamespace($scope.appId, $scope.appNamespace).then(
//only append namespace prefix for public app namespace
var appendNamespacePrefix = $scope.appNamespace.isPublic ? $scope.appendNamespacePrefix : false;
NamespaceService.createAppNamespace($scope.appId, $scope.appNamespace, appendNamespacePrefix).then(
function (result) {
$scope.step = 2;
setTimeout(function () {
......
......@@ -12,7 +12,7 @@ appService.service("NamespaceService", ['$resource', '$q', function ($resource,
},
createAppNamespace: {
method: 'POST',
url: '/apps/:appId/appnamespaces',
url: '/apps/:appId/appnamespaces?appendNamespacePrefix=:appendNamespacePrefix',
isArray: false
},
getNamespacePublishInfo: {
......@@ -60,11 +60,12 @@ appService.service("NamespaceService", ['$resource', '$q', function ($resource,
return d.promise;
}
function createAppNamespace(appId, appnamespace) {
function createAppNamespace(appId, appnamespace, appendNamespacePrefix) {
var d = $q.defer();
namespace_source.createAppNamespace({
appId: appId
}, appnamespace, function (result) {
appId: appId,
appendNamespacePrefix: appendNamespacePrefix
}, appnamespace, function (result) {
d.resolve(result);
}, function (result) {
d.reject(result);
......
......@@ -2,7 +2,10 @@ appService.service('PermissionService', ['$resource', '$q', function ($resource,
var permission_resource = $resource('', {}, {
init_app_namespace_permission: {
method: 'POST',
url: '/apps/:appId/initPermission?namespace=:namespace'
url: '/apps/:appId/initPermission',
headers: {
'Content-Type': 'text/plain;charset=UTF-8'
}
},
has_app_permission: {
method: 'GET',
......@@ -30,11 +33,17 @@ appService.service('PermissionService', ['$resource', '$q', function ($resource,
},
assign_namespace_role_to_user: {
method: 'POST',
url: '/apps/:appId/namespaces/:namespaceName/roles/:roleType'
url: '/apps/:appId/namespaces/:namespaceName/roles/:roleType',
headers: {
'Content-Type': 'text/plain;charset=UTF-8'
}
},
assign_namespace_env_role_to_user: {
method: 'POST',
url: '/apps/:appId/envs/:env/namespaces/:namespaceName/roles/:roleType'
url: '/apps/:appId/envs/:env/namespaces/:namespaceName/roles/:roleType',
headers: {
'Content-Type': 'text/plain;charset=UTF-8'
}
},
remove_namespace_role_from_user: {
method: 'DELETE',
......@@ -50,7 +59,10 @@ appService.service('PermissionService', ['$resource', '$q', function ($resource,
},
assign_app_role_to_user: {
method: 'POST',
url: '/apps/:appId/roles/:roleType'
url: '/apps/:appId/roles/:roleType',
headers: {
'Content-Type': 'text/plain;charset=UTF-8'
}
},
remove_app_role_from_user: {
method: 'DELETE',
......@@ -61,8 +73,7 @@ appService.service('PermissionService', ['$resource', '$q', function ($resource,
function initAppNamespacePermission(appId, namespace) {
var d = $q.defer();
permission_resource.init_app_namespace_permission({
appId: appId,
namespace: namespace
appId: appId
}, namespace,
function (result) {
d.resolve(result);
......
......@@ -76,6 +76,31 @@ public class AppNamespaceServiceTest extends AbstractIntegrationTest {
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 = assmbleBaseAppNamespace();
appNamespace.setPublic(true);
appNamespace.setName("old");
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 = assmbleBaseAppNamespace();
appNamespace.setPublic(true);
appNamespace.setName("datasource");
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)
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment