| // Copyright (C) 2021 The Android Open Source Project |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package com.googlesource.gerrit.plugins.replicationstatus; |
| |
| import static com.googlesource.gerrit.plugins.replication.PushResultProcessing.resolveNodeName; |
| import static com.googlesource.gerrit.plugins.replication.ReplicationFileBasedConfig.replaceName; |
| import static com.googlesource.gerrit.plugins.replicationstatus.ReplicationStatus.CACHE_NAME; |
| |
| import com.google.common.cache.Cache; |
| import com.google.common.collect.Maps; |
| import com.google.common.flogger.FluentLogger; |
| import com.google.gerrit.entities.Project; |
| import com.google.gerrit.entities.Project.NameKey; |
| import com.google.gerrit.extensions.restapi.AuthException; |
| import com.google.gerrit.extensions.restapi.BadRequestException; |
| import com.google.gerrit.extensions.restapi.ResourceConflictException; |
| import com.google.gerrit.extensions.restapi.Response; |
| import com.google.gerrit.extensions.restapi.RestReadView; |
| import com.google.gerrit.server.git.GitRepositoryManager; |
| import com.google.gerrit.server.permissions.GlobalPermission; |
| import com.google.gerrit.server.permissions.PermissionBackend; |
| import com.google.gerrit.server.permissions.PermissionBackendException; |
| import com.google.gerrit.server.permissions.ProjectPermission; |
| import com.google.inject.Inject; |
| import com.google.inject.name.Named; |
| import com.googlesource.gerrit.plugins.replication.ReplicationConfig; |
| import com.googlesource.gerrit.plugins.replicationstatus.ProjectReplicationStatus.ProjectReplicationStatusResult; |
| import java.io.IOException; |
| import java.io.UnsupportedEncodingException; |
| import java.net.URISyntaxException; |
| import java.net.URLEncoder; |
| import java.util.Collections; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Optional; |
| import java.util.stream.Collectors; |
| import org.apache.commons.io.FilenameUtils; |
| import org.eclipse.jgit.errors.ConfigInvalidException; |
| import org.eclipse.jgit.errors.RepositoryNotFoundException; |
| import org.eclipse.jgit.lib.Ref; |
| import org.eclipse.jgit.lib.Repository; |
| import org.eclipse.jgit.transport.URIish; |
| |
| class ReplicationStatusAction implements RestReadView<ReplicationStatusProjectRemoteResource> { |
| private static final FluentLogger logger = FluentLogger.forEnclosingClass(); |
| |
| private final PermissionBackend permissionBackend; |
| private final GitRepositoryManager repoManager; |
| private final Cache<ReplicationStatus.Key, ReplicationStatus> replicationStatusCache; |
| private final List<RemoteURLConfiguration> remoteConfigurations; |
| |
| @Inject |
| ReplicationStatusAction( |
| PermissionBackend permissionBackend, |
| GitRepositoryManager repoManager, |
| @Named(CACHE_NAME) Cache<ReplicationStatus.Key, ReplicationStatus> replicationStatusCache, |
| ReplicationConfig replicationConfig, |
| ConfigParser configParser) |
| throws ConfigInvalidException { |
| this.permissionBackend = permissionBackend; |
| this.repoManager = repoManager; |
| this.replicationStatusCache = replicationStatusCache; |
| this.remoteConfigurations = configParser.parseRemotes(replicationConfig.getConfig()); |
| } |
| |
| @Override |
| public Response<ProjectReplicationStatus> apply(ReplicationStatusProjectRemoteResource resource) |
| throws AuthException, PermissionBackendException, BadRequestException, |
| ResourceConflictException, IOException { |
| |
| Project.NameKey projectNameKey = resource.getProjectNameKey(); |
| String remoteName = resource.getRemote(); |
| Optional<RemoteURLConfiguration> remoteConfig = |
| remoteConfigurations.stream() |
| .filter(config -> config.getName().equals(remoteName)) |
| .findFirst(); |
| |
| checkIsOwnerOrAdmin(projectNameKey); |
| |
| try (Repository git = repoManager.openRepository(projectNameKey)) { |
| List<Ref> refs = git.getRefDatabase().getRefs(); |
| Map<String, RemoteReplicationStatus> remoteStatuses = |
| remoteConfig |
| .map(config -> getRemoteReplicationStatuses(config, projectNameKey, refs)) |
| .orElse(Collections.emptyMap()); |
| |
| ProjectReplicationStatus.ProjectReplicationStatusResult overallStatus = |
| getOverallStatus(remoteStatuses); |
| ProjectReplicationStatus projectStatus = |
| ProjectReplicationStatus.create(remoteStatuses, overallStatus, projectNameKey.get()); |
| |
| return Response.ok(projectStatus); |
| |
| } catch (RepositoryNotFoundException e) { |
| throw new BadRequestException( |
| String.format("Project %s does not exist", projectNameKey.get())); |
| } |
| } |
| |
| private Map<String, RemoteReplicationStatus> getRemoteReplicationStatuses( |
| RemoteURLConfiguration config, NameKey projectNameKey, List<Ref> refs) { |
| return config.getUrls().stream() |
| .map(url -> getTargetURL(projectNameKey, config, url)) |
| .filter(Optional::isPresent) |
| .map( |
| targetUrl -> |
| Maps.immutableEntry( |
| targetUrl.get(), |
| RemoteReplicationStatus.create( |
| getRefStatuses(projectNameKey, refs, targetUrl.get())))) |
| .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); |
| } |
| |
| private ProjectReplicationStatusResult getOverallStatus( |
| Map<String, RemoteReplicationStatus> remoteStatuses) { |
| return remoteStatuses.values().stream() |
| .flatMap(status -> status.status().values().stream()) |
| .filter(ReplicationStatus::isFailure) |
| .findFirst() |
| .map(status -> ProjectReplicationStatus.ProjectReplicationStatusResult.FAILED) |
| .orElse(ProjectReplicationStatus.ProjectReplicationStatusResult.OK); |
| } |
| |
| private Map<String, ReplicationStatus> getRefStatuses( |
| Project.NameKey projectNameKey, List<Ref> refs, String uri) { |
| return refs.stream() |
| .map(ref -> ReplicationStatus.Key.create(projectNameKey, uri, ref.getName())) |
| .map( |
| key -> |
| Maps.immutableEntry( |
| key.ref(), Optional.ofNullable(replicationStatusCache.getIfPresent(key)))) |
| .filter(e -> e.getValue().isPresent()) |
| .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().get())); |
| } |
| |
| private Optional<String> getTargetURL( |
| Project.NameKey projectNameKey, RemoteURLConfiguration config, String url) { |
| try { |
| return Optional.of(resolveNodeName(getURI(config, new URIish(url), projectNameKey))); |
| } catch (URISyntaxException e) { |
| logger.atSevere().withCause(e).log( |
| "Cannot resolve target URI for template: %s and project name: %s", url, projectNameKey); |
| return Optional.empty(); |
| } |
| } |
| |
| /** |
| * This method was copied from replication plugin where the method is in protected scope {@link |
| * com.googlesource.gerrit.plugins.replication.Destination#getURI(URIish, NameKey)} |
| */ |
| @SuppressWarnings("javadoc") |
| private URIish getURI(RemoteURLConfiguration config, URIish template, Project.NameKey project) { |
| String name = project.get(); |
| if (needsUrlEncoding(template)) { |
| name = encode(name); |
| } |
| String remoteNameStyle = config.getRemoteNameStyle(); |
| if (remoteNameStyle.equals("dash")) { |
| name = name.replace("/", "-"); |
| } else if (remoteNameStyle.equals("underscore")) { |
| name = name.replace("/", "_"); |
| } else if (remoteNameStyle.equals("basenameOnly")) { |
| name = FilenameUtils.getBaseName(name); |
| } else if (!remoteNameStyle.equals("slash")) { |
| logger.atFine().log("Unknown remoteNameStyle: %s, falling back to slash", remoteNameStyle); |
| } |
| String replacedPath = replaceName(template.getPath(), name, config.isSingleProjectMatch()); |
| return (replacedPath != null) ? template.setPath(replacedPath) : template; |
| } |
| |
| /** |
| * This method was copied from replication plugin where the method is in protected scope {@link |
| * com.googlesource.gerrit.plugins.replication.Destination#needsUrlEncoding(URIish)} |
| */ |
| @SuppressWarnings("javadoc") |
| private static boolean needsUrlEncoding(URIish uri) { |
| return "http".equalsIgnoreCase(uri.getScheme()) |
| || "https".equalsIgnoreCase(uri.getScheme()) |
| || "amazon-s3".equalsIgnoreCase(uri.getScheme()); |
| } |
| |
| /** |
| * This method was copied from replication plugin where the method is in protected scope {@link |
| * com.googlesource.gerrit.plugins.replication.Destination#encode(String)} |
| */ |
| @SuppressWarnings("javadoc") |
| private static String encode(String str) { |
| try { |
| // Some cleanup is required. The '/' character is always encoded as %2F |
| // however remote servers will expect it to be not encoded as part of the |
| // path used to the repository. Space is incorrectly encoded as '+' for this |
| // context. In the path part of a URI space should be %20, but in form data |
| // space is '+'. Our cleanup replace fixes these two issues. |
| return URLEncoder.encode(str, "UTF-8").replaceAll("%2[fF]", "/").replace("+", "%20"); |
| } catch (UnsupportedEncodingException e) { |
| throw new RuntimeException(e); |
| } |
| } |
| |
| private void checkIsOwnerOrAdmin(Project.NameKey project) throws AuthException { |
| if (!permissionBackend.currentUser().testOrFalse(GlobalPermission.ADMINISTRATE_SERVER) |
| && !permissionBackend |
| .currentUser() |
| .project(project) |
| .testOrFalse(ProjectPermission.WRITE_CONFIG)) { |
| throw new AuthException("Administrate Server or Project owner required"); |
| } |
| } |
| } |