package org.zalando.baigan.service.github;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.Callable;
import java.util.concurrent.Executors;
import org.eclipse.egit.github.core.RepositoryContents;
import org.eclipse.egit.github.core.RepositoryId;
import org.eclipse.egit.github.core.client.GitHubClient;
import org.eclipse.egit.github.core.service.ContentsService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.zalando.baigan.model.Configuration;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.guava.GuavaModule;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
import com.google.common.cache.CacheLoader;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import javax.annotation.Nonnull;
import static com.google.common.util.concurrent.MoreExecutors.listeningDecorator;
import static org.apache.commons.codec.binary.Base64.*;
/**
* This class implements the {@link CacheLoader} offering Configuration loading
* from a remote Git repository.
*
* @author mchand
*
*/
public class GitCacheLoader
extends CacheLoader<String, Map<String, Configuration>> {
private static final Logger LOG = LoggerFactory
.getLogger(GitCacheLoader.class);
private String latestSha;
private GitConfig config;
private final ListeningExecutorService executorService = listeningDecorator(Executors.newFixedThreadPool(1));
private ObjectMapper objectMapper = new ObjectMapper().registerModule(new GuavaModule());
private final ContentsService contentsService;
private static ContentsService buildContentsService(@Nonnull final GitConfig gitConfig) {
Objects.requireNonNull(gitConfig, "gitConfig is required");
final GitHubClient client = new GitHubClient(gitConfig.getGitHost());
client.setOAuth2Token(gitConfig.getOauthToken());
return new ContentsService(client);
}
public GitCacheLoader(@Nonnull final GitConfig gitConfig) {
this(gitConfig, buildContentsService(gitConfig));
}
@VisibleForTesting
GitCacheLoader(GitConfig gitConfig, ContentsService contentsService) {
this.config = gitConfig;
this.contentsService = contentsService;
}
@Override
public Map<String, Configuration> load(String key) throws Exception {
final RepositoryContents contents = getContentsForFile(key);
if (contents != null) {
return updateContent(contents);
}
LOG.warn("Failed to load the repository contents for {}", key);
return ImmutableMap.of();
}
private Map<String, Configuration> updateContent(
@Nonnull final RepositoryContents contents) {
final String contentsSha = contents.getSha();
LOG.info("Loading the new repository contents [ SHA:{} ; NAME:{} ] ",
contentsSha, contents.getPath());
final List<Configuration> configurations = getConfigurations(getTextContent(contents));
latestSha = contentsSha;
final ImmutableMap.Builder<String, Configuration> builder = ImmutableMap.builder();
for (final Configuration each : configurations) {
builder.put(each.getAlias(), each);
}
return builder.build();
}
public ListenableFuture<Map<String, Configuration>> reload(final String key,
final Map<String, Configuration> oldValue) throws Exception {
return createFuture(key, oldValue);
}
private ListenableFuture<Map<String, Configuration>> createFuture(
final String sourceFile,
final Map<String, Configuration> oldValue) {
final Callable<Map<String, Configuration>> callable = () -> {
final RepositoryContents contents = getContentsForFile(sourceFile);
// If the contents is null, return old value, this is to
// preserve in case Github is down.
// If the hash is null which is very unlikely, or it is same as
// the earlier one, we don't reload it
if (contents == null || Strings.isNullOrEmpty(contents.getSha())
|| contents.getSha().equals(latestSha)) {
return oldValue;
}
return updateContent(contents);
};
return executorService.submit(callable);
}
private RepositoryContents getContentsForFile(final String sourceFile) {
try {
final List<RepositoryContents> contents = contentsService
.getContents(
new RepositoryId(config.getRepoOwner(),
config.getRepoName()),
sourceFile, config.getRepoRefs());
return contents.get(0);
} catch (Exception e) {
LOG.warn("Failed to get contents from the Github repository ", e);
}
return null;
}
private String getTextContent(final RepositoryContents content) {
final String stringContent = content.getContent();
return new String(decodeBase64(stringContent.getBytes()));
}
private List<Configuration> getConfigurations(final String text) {
try {
return objectMapper.readValue(text,
new TypeReference<List<Configuration>>() {
});
} catch (IOException e) {
LOG.warn(
"Exception while deserializing the Configuration from the Github repository contents." +
"Please check to see if if matches the Configuration schema at " +
"https://github.com/zalando/baigan-config.",
e);
}
return ImmutableList.of();
}
}