mirror of
https://github.com/scm-manager/scm-manager.git
synced 2026-01-04 22:59:51 +01:00
Enable Health Checks (#1621)
In the release of version 2.0.0 of SCM-Manager, the health checks had been neglected. This makes them visible again in the frontend and adds the ability to trigger them. In addition there are two types of health checks: The "normal" ones, now called "light checks", that are run on startup, and more intense checks run only on request. As a change to version 1.x, health checks will no longer be persisted for repositories. Co-authored-by: Eduard Heimbuch <eduard.heimbuch@cloudogu.com>
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
@@ -21,9 +21,9 @@ Strategie `benutzerdefiniert` ausgewählt ist, kann zusätzlich zum Repository N
|
||||
Ein archiviertes Repository kann nicht mehr verändert werden.
|
||||
|
||||
In dem Bereich "Repository exportieren" kann das Repository in unterschiedlichen Formaten exportiert werden.
|
||||
Während eines laufenden Exports, kann auf das Repository nur lesend zugriffen werden.
|
||||
Während eines laufenden Exports kann auf das Repository nur lesend zugriffen werden.
|
||||
Der Repository Export wird asynchron erstellt und auf dem Server gespeichert.
|
||||
Existiert bereits ein Export für dieses Repository auf dem Server, wird dieser vorher gelöscht, da es immer nur einen Export pro Repository geben kann.
|
||||
Existiert bereits ein Export für dieses Repository auf dem Server, wird dieser vorher gelöscht, da es immer nur einen Export pro Repository geben kann.
|
||||
Exporte werden 10 Tage nach deren Erstellung automatisch vom SCM-Server gelöscht.
|
||||
Falls ein Export existiert, wird über die blaue Info-Box angezeigt von wem, wann und wie dieser Export erzeugt wurde.
|
||||
|
||||
@@ -39,6 +39,20 @@ Das Ausgabeformat des Repository kann über die angebotenen Optionen verändert
|
||||
|
||||

|
||||
|
||||
Der Bereich „Integritätsprüfung“ bietet die Möglichkeit, eine Integritätsprüfung des Repositories zu starten. Hier
|
||||
werden (zum Teil ausführliche) Prüfungen ausgeführt, die z. B. sicherstellen, dass die Verzeichnisse korrekt
|
||||
eingebunden sind. Wenn bei dem Zugriff auf ein Repository Fehler auftreten, sollte zunächst eine solche
|
||||
Integritätsprüfung gestartet werden. Ein Teil dieser Prüfungen wird bei jedem Start des SCM-Managers ausgeführt.
|
||||
|
||||
Werden bei einer dieser Integritätsprüfungen Fehler gefunden, wird auf der Repository-Übersicht sowie auf den
|
||||
Detailseiten zum Repository neben dem Namen ein Tag „fehlerhaft" angezeigt. In den Einstellungen wird zudem eine Meldung
|
||||
eingeblendet. Durch Klick auf diese Meldung oder die Tags wird ein Popup mit weiteren Details angezeigt.
|
||||
|
||||
Der Server führt immer nur eine Prüfung zur Zeit durch. Es können jedoch für mehrere Repositories Prüfungen in die
|
||||
Warteschlange gestellt werden, die dann nacheinander durchgeführt werden.
|
||||
|
||||

|
||||
|
||||
### Berechtigungen
|
||||
|
||||
Dank des fein granularen Berechtigungskonzepts des SCM-Managers können Nutzern und Gruppen, basierend auf definierbaren
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
@@ -37,6 +37,19 @@ The output format of the repository can be changed via the offered options:
|
||||
|
||||

|
||||
|
||||
The section "Health Checks" provides the option to run health checks for the repository. These are (propably
|
||||
extensive) checks, ensuring that for example the paths are mounted correctly. If errors occur while accessing
|
||||
repositories, this should be your first place to look. A part of the checks are run every time SCM-Manager starts.
|
||||
|
||||
If errors are detected during these checks, a tag is shown near the name of the repository in the overview
|
||||
and on the header for the repository. Additionally there is a notification in the settings dialog for this
|
||||
repository. By clicking this message or the tags, a popup is shown with more information regarding the failures.
|
||||
|
||||
The server will run only one check at a time. Nonetheless you can enqueue multiple checks for different
|
||||
repositories, which will be executed after one another.
|
||||
|
||||

|
||||
|
||||
### Permissions
|
||||
|
||||
Thanks to the finely granular permission concept of SCM-Manager, users and groups can be authorized based on definable
|
||||
|
||||
2
gradle/changelog/health_checks.yaml
Normal file
2
gradle/changelog/health_checks.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
- type: added
|
||||
description: Frontend for, and enhancement of health checks ([#1621](https://github.com/scm-manager/scm-manager/pull/1621))
|
||||
@@ -21,15 +21,18 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
|
||||
package sonia.scm;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
|
||||
import java.io.Closeable;
|
||||
import sonia.scm.version.Version;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static java.lang.String.format;
|
||||
|
||||
/**
|
||||
* The main class for retrieving the home and the version of the SCM-Manager.
|
||||
* This class is a singleton which can be retrieved via injection
|
||||
@@ -83,4 +86,17 @@ public interface SCMContextProvider {
|
||||
* @return version of the SCM-Manager
|
||||
*/
|
||||
String getVersion();
|
||||
|
||||
/**
|
||||
* Returns the version of the SCM-Manager used in documentation urls (eg. version 2.17.0 and 2.17.1 will all result
|
||||
* in 2.17.x). The default implementation works for versions with three parts (major version, minor version,
|
||||
* and patch version, where the patch version will be replaces with an 'x').
|
||||
*
|
||||
* @return version of the SCM-Manager used in documentation urls
|
||||
* @since 2.17.0
|
||||
*/
|
||||
default String getDocumentationVersion() {
|
||||
Version parsedVersion = Version.parse(getVersion());
|
||||
return format("%s.%s.x", parsedVersion.getMajor(), parsedVersion.getMinor());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.repository;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import sonia.scm.event.Event;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
import static java.util.Collections.unmodifiableCollection;
|
||||
|
||||
/**
|
||||
* This event is triggered whenever a health check was run and either found issues
|
||||
* or issues reported earlier are fixed (that is, health has changed).
|
||||
*
|
||||
* @since 2.17.0
|
||||
*/
|
||||
@Event
|
||||
@AllArgsConstructor
|
||||
public class HealthCheckEvent {
|
||||
|
||||
private final Repository repository;
|
||||
private final Collection<HealthCheckFailure> previousFailures;
|
||||
private final Collection<HealthCheckFailure> currentFailures;
|
||||
|
||||
public Repository getRepository() {
|
||||
return repository;
|
||||
}
|
||||
|
||||
public Collection<HealthCheckFailure> getPreviousFailures() {
|
||||
return unmodifiableCollection(previousFailures);
|
||||
}
|
||||
|
||||
public Collection<HealthCheckFailure> getCurrentFailures() {
|
||||
return unmodifiableCollection(currentFailures);
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
|
||||
package sonia.scm.repository;
|
||||
|
||||
//~--- non-JDK imports --------------------------------------------------------
|
||||
@@ -32,6 +32,7 @@ import com.google.common.base.Objects;
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.XmlRootElement;
|
||||
import java.text.MessageFormat;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
|
||||
@@ -46,15 +47,18 @@ import javax.xml.bind.annotation.XmlRootElement;
|
||||
public final class HealthCheckFailure
|
||||
{
|
||||
|
||||
private static final String URL_TEMPLATE = "https://www.scm-manager.org/docs/{0}/en/user/repo/health-checks/%s";
|
||||
private static final String LATEST_VERSION = "latest";
|
||||
|
||||
/**
|
||||
* Constructs a new {@link HealthCheckFailure}.
|
||||
* Constructs a new {@link HealthCheckFailure}.
|
||||
* This constructor is only for JAXB.
|
||||
*
|
||||
*/
|
||||
HealthCheckFailure() {}
|
||||
|
||||
/**
|
||||
* Constructs a new {@link HealthCheckFailure}.
|
||||
* Constructs a new {@link HealthCheckFailure}.
|
||||
*
|
||||
* @param id id of the failure
|
||||
* @param summary summary of the failure
|
||||
@@ -62,7 +66,7 @@ public final class HealthCheckFailure
|
||||
*/
|
||||
public HealthCheckFailure(String id, String summary, String description)
|
||||
{
|
||||
this(id, summary, null, description);
|
||||
this(id, summary, (String) null, description);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -79,14 +83,49 @@ public final class HealthCheckFailure
|
||||
this.id = id;
|
||||
this.summary = summary;
|
||||
this.url = url;
|
||||
this.urlTemplated = false;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
//~--- methods --------------------------------------------------------------
|
||||
/**
|
||||
* Constructs ...
|
||||
*
|
||||
* @param id id of the failure
|
||||
* @param summary summary of the failure
|
||||
* @param urlTemplate template for the url of the failure (use {@link #urlForTitle(String)} to create this)
|
||||
* @param description description of the failure
|
||||
* @since 2.17.0
|
||||
*/
|
||||
public HealthCheckFailure(String id, String summary, UrlTemplate urlTemplate,
|
||||
String description)
|
||||
{
|
||||
this.id = id;
|
||||
this.summary = summary;
|
||||
this.url = urlTemplate.get();
|
||||
this.urlTemplated = true;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
* Use this to create {@link HealthCheckFailure} instances with an url for core health check failures.
|
||||
* @param title The title of the failure matching a health check documentation page.
|
||||
* @since 2.17.0
|
||||
*/
|
||||
public static UrlTemplate urlForTitle(String title) {
|
||||
return new UrlTemplate(String.format(URL_TEMPLATE, title));
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this to create {@link HealthCheckFailure} instances with a custom url for core health check
|
||||
* failures. If this url can be customized with a concrete version of SCM-Manager, you can use <code>{0}</code>
|
||||
* as a placeholder for the version. This will be replaces later on.
|
||||
* @param urlTemplate The url for this failure.
|
||||
* @since 2.17.0
|
||||
*/
|
||||
public static UrlTemplate templated(String urlTemplate) {
|
||||
return new UrlTemplate(urlTemplate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj)
|
||||
{
|
||||
@@ -103,25 +142,19 @@ public final class HealthCheckFailure
|
||||
final HealthCheckFailure other = (HealthCheckFailure) obj;
|
||||
|
||||
//J-
|
||||
return Objects.equal(id, other.id)
|
||||
return Objects.equal(id, other.id)
|
||||
&& Objects.equal(summary, other.summary)
|
||||
&& Objects.equal(url, other.url)
|
||||
&& Objects.equal(description, other.description);
|
||||
//J+
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public int hashCode()
|
||||
{
|
||||
return Objects.hashCode(id, summary, url, description);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
@@ -135,8 +168,6 @@ public final class HealthCheckFailure
|
||||
//J+
|
||||
}
|
||||
|
||||
//~--- get methods ----------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Returns the description of this failure.
|
||||
*
|
||||
@@ -168,16 +199,31 @@ public final class HealthCheckFailure
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the url of the failure.
|
||||
* Return the url of the failure. The url may potentially be templated. In the case you can get a
|
||||
* special url for an explicit version of SCM-Manager using {@link #getUrl(String)} whereas this
|
||||
* function will return a generic url for the {@value LATEST_VERSION} version.
|
||||
*
|
||||
* @return url of the failure
|
||||
*/
|
||||
public String getUrl()
|
||||
{
|
||||
return url;
|
||||
return getUrl(LATEST_VERSION);
|
||||
}
|
||||
|
||||
//~--- fields ---------------------------------------------------------------
|
||||
/**
|
||||
* Return the url of the failure for a concrete version of SCM-Manager (given the url is templated).
|
||||
*
|
||||
* @param version The version of SCM-Manager to create the url for.
|
||||
* @return url of the failure
|
||||
* @since 2.17.0
|
||||
*/
|
||||
public String getUrl(String version) {
|
||||
if (urlTemplated) {
|
||||
return MessageFormat.format(url, version);
|
||||
} else {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
/** description of failure */
|
||||
private String description;
|
||||
@@ -190,4 +236,19 @@ public final class HealthCheckFailure
|
||||
|
||||
/** url of failure */
|
||||
private String url;
|
||||
|
||||
/** Flag whether the url is a template or not */
|
||||
private boolean urlTemplated = false;
|
||||
|
||||
public static final class UrlTemplate {
|
||||
private final String url;
|
||||
|
||||
private UrlTemplate(String url) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
private String get() {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.repository;
|
||||
|
||||
import sonia.scm.plugin.Extension;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
@Extension
|
||||
public final class MetadataHealthCheck implements HealthCheck {
|
||||
|
||||
public static final HealthCheckFailure REPOSITORY_Directory_NOT_WRITABLE =
|
||||
new HealthCheckFailure("9cSV1eaVF1",
|
||||
"repository directory not writable",
|
||||
"The system user has no permissions to create or delete files in the repository directory.");
|
||||
public static final HealthCheckFailure METADATA_NOT_WRITABLE =
|
||||
new HealthCheckFailure("6bSUg4dZ41",
|
||||
"metadata file not writable",
|
||||
"The system user has no permissions to modify the metadata file for the repository.");
|
||||
private final RepositoryLocationResolver locationResolver;
|
||||
|
||||
@Inject
|
||||
public MetadataHealthCheck(RepositoryLocationResolver locationResolver) {
|
||||
this.locationResolver = locationResolver;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HealthCheckResult check(Repository repository) {
|
||||
Path repositoryLocation = locationResolver.forClass(Path.class).getLocation(repository.getId());
|
||||
if (!Files.isWritable(repositoryLocation)) {
|
||||
return HealthCheckResult.unhealthy(REPOSITORY_Directory_NOT_WRITABLE);
|
||||
}
|
||||
Path metadata = repositoryLocation.resolve("metadata.xml");
|
||||
if (!Files.isWritable(metadata)) {
|
||||
return HealthCheckResult.unhealthy(METADATA_NOT_WRITABLE);
|
||||
}
|
||||
return HealthCheckResult.healthy();
|
||||
}
|
||||
}
|
||||
@@ -37,7 +37,6 @@ import sonia.scm.util.ValidationUtil;
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.XmlElement;
|
||||
import javax.xml.bind.annotation.XmlElementWrapper;
|
||||
import javax.xml.bind.annotation.XmlRootElement;
|
||||
import javax.xml.bind.annotation.XmlTransient;
|
||||
import java.util.Arrays;
|
||||
@@ -69,8 +68,7 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per
|
||||
private String contact;
|
||||
private Long creationDate;
|
||||
private String description;
|
||||
@XmlElement(name = "healthCheckFailure")
|
||||
@XmlElementWrapper(name = "healthCheckFailures")
|
||||
@XmlTransient
|
||||
private List<HealthCheckFailure> healthCheckFailures;
|
||||
private String id;
|
||||
private Long lastModified;
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
|
||||
package sonia.scm.repository.api;
|
||||
|
||||
/**
|
||||
@@ -48,7 +48,7 @@ public enum Command
|
||||
* @since 1.31
|
||||
*/
|
||||
INCOMING, OUTGOING, PUSH, PULL,
|
||||
|
||||
|
||||
/**
|
||||
* @since 1.43
|
||||
*/
|
||||
@@ -67,5 +67,10 @@ public enum Command
|
||||
/**
|
||||
* @since 2.11.0
|
||||
*/
|
||||
TAG;
|
||||
TAG,
|
||||
|
||||
/**
|
||||
* @since 2.17.0
|
||||
*/
|
||||
FULL_HEALTH_CHECK;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.repository.api;
|
||||
|
||||
import sonia.scm.repository.HealthCheckResult;
|
||||
import sonia.scm.repository.spi.FullHealthCheckCommand;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class FullHealthCheckCommandBuilder {
|
||||
|
||||
private final FullHealthCheckCommand command;
|
||||
|
||||
public FullHealthCheckCommandBuilder(FullHealthCheckCommand command) {
|
||||
this.command = command;
|
||||
}
|
||||
|
||||
public HealthCheckResult check() throws IOException {
|
||||
return command.check();
|
||||
}
|
||||
}
|
||||
@@ -466,6 +466,21 @@ public final class RepositoryService implements Closeable {
|
||||
return new LookupCommandBuilder(provider.getLookupCommand());
|
||||
}
|
||||
|
||||
/**
|
||||
* The full health check command inspects a repository in a way, that might take a while in contrast to the
|
||||
* light checks executed at startup.
|
||||
*
|
||||
* @return instance of {@link FullHealthCheckCommandBuilder}
|
||||
* @throws CommandNotSupportedException if the command is not supported
|
||||
* by the implementation of the repository service provider.
|
||||
* @since 2.17.0
|
||||
*/
|
||||
public FullHealthCheckCommandBuilder getFullCheckCommand() {
|
||||
LOG.debug("create full check command for repository {}",
|
||||
repository.getNamespaceAndName());
|
||||
return new FullHealthCheckCommandBuilder(provider.getFullHealthCheckCommand());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the command is supported by the repository service.
|
||||
*
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.repository.spi;
|
||||
|
||||
import sonia.scm.repository.HealthCheckResult;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public interface FullHealthCheckCommand {
|
||||
HealthCheckResult check() throws IOException;
|
||||
}
|
||||
@@ -21,7 +21,7 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
|
||||
package sonia.scm.repository.spi;
|
||||
|
||||
import sonia.scm.repository.Feature;
|
||||
@@ -119,7 +119,7 @@ public abstract class RepositoryServiceProvider implements Closeable
|
||||
*
|
||||
*
|
||||
* @return
|
||||
*
|
||||
*
|
||||
* @since 1.43
|
||||
*/
|
||||
public BundleCommand getBundleCommand()
|
||||
@@ -260,7 +260,7 @@ public abstract class RepositoryServiceProvider implements Closeable
|
||||
*
|
||||
*
|
||||
* @return
|
||||
*
|
||||
*
|
||||
* @since 1.43
|
||||
*/
|
||||
public UnbundleCommand getUnbundleCommand()
|
||||
@@ -291,4 +291,11 @@ public abstract class RepositoryServiceProvider implements Closeable
|
||||
{
|
||||
throw new CommandNotSupportedException(Command.LOOKUP);
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 2.17.0
|
||||
*/
|
||||
public FullHealthCheckCommand getFullHealthCheckCommand() {
|
||||
throw new CommandNotSupportedException(Command.FULL_HEALTH_CHECK);
|
||||
}
|
||||
}
|
||||
|
||||
67
scm-core/src/test/java/sonia/scm/SCMContextProviderTest.java
Normal file
67
scm-core/src/test/java/sonia/scm/SCMContextProviderTest.java
Normal file
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class SCMContextProviderTest {
|
||||
|
||||
@Test
|
||||
void shouldCreateCorrectDocumentationVersion() {
|
||||
SCMContextProvider scmContextProvider = new SCMContextProvider() {
|
||||
@Override
|
||||
public File getBaseDirectory() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Path resolve(Path path) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stage getStage() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Throwable getStartupError() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getVersion() {
|
||||
return "1.17.2";
|
||||
}
|
||||
};
|
||||
|
||||
assertThat(scmContextProvider.getDocumentationVersion()).isEqualTo("1.17.x");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.repository;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static sonia.scm.repository.HealthCheckFailure.templated;
|
||||
import static sonia.scm.repository.HealthCheckFailure.urlForTitle;
|
||||
|
||||
class HealthCheckFailureTest {
|
||||
|
||||
@Test
|
||||
void shouldCreateTemplatedUrl() {
|
||||
HealthCheckFailure failure = new HealthCheckFailure("1", "hyperdrive", urlForTitle("hyperdrive"), "Far too fast");
|
||||
|
||||
assertThat(failure.getUrl()).isEqualTo("https://www.scm-manager.org/docs/latest/en/user/repo/health-checks/hyperdrive");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCreateTemplatedUrlForGivenVersion() {
|
||||
HealthCheckFailure failure = new HealthCheckFailure("1", "hyperdrive", urlForTitle("hyperdrive"), "Far too fast");
|
||||
|
||||
assertThat(failure.getUrl("1.17.x")).isEqualTo("https://www.scm-manager.org/docs/1.17.x/en/user/repo/health-checks/hyperdrive");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCreateCustomTemplatedUrlForGivenVersion() {
|
||||
HealthCheckFailure failure = new HealthCheckFailure("1", "hyperdrive", templated("http://hog/{0}/error"), "Far too fast");
|
||||
|
||||
assertThat(failure.getUrl("1.17.x")).isEqualTo("http://hog/1.17.x/error");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnNullForUrlIfNotSet() {
|
||||
HealthCheckFailure failure = new HealthCheckFailure("1", "hyperdrive", "Far too fast");
|
||||
|
||||
assertThat(failure.getUrl()).isNull();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.repository.spi;
|
||||
|
||||
import com.aragost.javahg.commands.ExecutionException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.repository.HealthCheckFailure;
|
||||
import sonia.scm.repository.HealthCheckResult;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class HgFullHealthCheckCommand extends AbstractCommand implements FullHealthCheckCommand {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(HgFullHealthCheckCommand.class);
|
||||
|
||||
public HgFullHealthCheckCommand(HgCommandContext context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public HealthCheckResult check() throws IOException {
|
||||
HgVerifyCommand cmd = HgVerifyCommand.on(open());
|
||||
try {
|
||||
cmd.execute();
|
||||
return HealthCheckResult.healthy();
|
||||
} catch (ExecutionException e) {
|
||||
LOG.warn("hg verify failed for repository {}", getRepository(), e);
|
||||
return HealthCheckResult.unhealthy(new HealthCheckFailure("FaSUYbZUR1",
|
||||
"hg verify failed", "The check 'hg verify' failed for the repository."));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -59,7 +59,8 @@ public class HgRepositoryServiceProvider extends RepositoryServiceProvider {
|
||||
Command.PULL,
|
||||
Command.MODIFY,
|
||||
Command.BUNDLE,
|
||||
Command.UNBUNDLE
|
||||
Command.UNBUNDLE,
|
||||
Command.FULL_HEALTH_CHECK
|
||||
);
|
||||
|
||||
public static final Set<Feature> FEATURES = EnumSet.of(Feature.COMBINED_DEFAULT_BRANCH);
|
||||
@@ -188,4 +189,9 @@ public class HgRepositoryServiceProvider extends RepositoryServiceProvider {
|
||||
public UnbundleCommand getUnbundleCommand() {
|
||||
return new HgUnbundleCommand(context, lazyChangesetResolver, eventFactory);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FullHealthCheckCommand getFullHealthCheckCommand() {
|
||||
return new HgFullHealthCheckCommand(context);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.repository.spi;
|
||||
|
||||
import com.aragost.javahg.Repository;
|
||||
import com.aragost.javahg.internals.AbstractCommand;
|
||||
|
||||
public class HgVerifyCommand extends AbstractCommand {
|
||||
|
||||
public static final String COMMAND_NAME = "verify";
|
||||
|
||||
protected HgVerifyCommand(Repository repository) {
|
||||
super(repository, COMMAND_NAME);
|
||||
}
|
||||
|
||||
public static HgVerifyCommand on(Repository repository) {
|
||||
return new HgVerifyCommand(repository);
|
||||
}
|
||||
|
||||
public String execute() {
|
||||
return launchString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCommandName() {
|
||||
return COMMAND_NAME;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.repository.spi;
|
||||
|
||||
import com.aragost.javahg.commands.ExecutionException;
|
||||
import org.junit.Test;
|
||||
import sonia.scm.repository.HealthCheckResult;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
public class HgFullHealthCheckCommandTest extends AbstractHgCommandTestBase {
|
||||
|
||||
@Test
|
||||
public void shouldDetectMissingFile() throws IOException {
|
||||
HgFullHealthCheckCommand checkCommand = new HgFullHealthCheckCommand(cmdContext);
|
||||
File d = new File(cmdContext.open().getDirectory(), ".hg/store/data/c/d.txt.i");
|
||||
d.delete();
|
||||
|
||||
HealthCheckResult check = checkCommand.check();
|
||||
|
||||
assertThat(check.isHealthy()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldBeOkForValidRepository() throws IOException {
|
||||
HgFullHealthCheckCommand checkCommand = new HgFullHealthCheckCommand(cmdContext);
|
||||
|
||||
HealthCheckResult check = checkCommand.check();
|
||||
|
||||
assertThat(check.isHealthy()).isTrue();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.repository.spi;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.tmatesoft.svn.core.SVNException;
|
||||
import org.tmatesoft.svn.core.wc.SVNClientManager;
|
||||
import org.tmatesoft.svn.core.wc.admin.SVNAdminClient;
|
||||
import sonia.scm.repository.HealthCheckFailure;
|
||||
import sonia.scm.repository.HealthCheckResult;
|
||||
|
||||
public class SvnFullHealthCheckCommand extends AbstractSvnCommand implements FullHealthCheckCommand{
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(SvnFullHealthCheckCommand.class);
|
||||
|
||||
protected SvnFullHealthCheckCommand(SvnContext context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public HealthCheckResult check() {
|
||||
SVNClientManager clientManager= SVNClientManager.newInstance();
|
||||
SVNAdminClient adminClient = clientManager.getAdminClient();
|
||||
try {
|
||||
adminClient.doVerify(context.getDirectory());
|
||||
} catch (SVNException e) {
|
||||
LOG.warn("svn verify failed for repository {}", context.get(), e);
|
||||
return HealthCheckResult.unhealthy(new HealthCheckFailure("5FSV2kreE1",
|
||||
"svn verify failed", "The check 'svn verify' failed for the repository."));
|
||||
}
|
||||
|
||||
return HealthCheckResult.healthy();
|
||||
}
|
||||
}
|
||||
@@ -42,8 +42,16 @@ public class SvnRepositoryServiceProvider extends RepositoryServiceProvider {
|
||||
|
||||
//J-
|
||||
public static final Set<Command> COMMANDS = ImmutableSet.of(
|
||||
Command.BLAME, Command.BROWSE, Command.CAT, Command.DIFF,
|
||||
Command.LOG, Command.BUNDLE, Command.UNBUNDLE, Command.MODIFY, Command.LOOKUP
|
||||
Command.BLAME,
|
||||
Command.BROWSE,
|
||||
Command.CAT,
|
||||
Command.DIFF,
|
||||
Command.LOG,
|
||||
Command.BUNDLE,
|
||||
Command.UNBUNDLE,
|
||||
Command.MODIFY,
|
||||
Command.LOOKUP,
|
||||
Command.FULL_HEALTH_CHECK
|
||||
);
|
||||
//J+
|
||||
|
||||
@@ -120,4 +128,9 @@ public class SvnRepositoryServiceProvider extends RepositoryServiceProvider {
|
||||
public UnbundleCommand getUnbundleCommand() {
|
||||
return new SvnUnbundleCommand(context, hookContextFactory, new SvnLogCommand(context));
|
||||
}
|
||||
|
||||
@Override
|
||||
public FullHealthCheckCommand getFullHealthCheckCommand() {
|
||||
return new SvnFullHealthCheckCommand(context);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.repository.spi;
|
||||
|
||||
import org.junit.Test;
|
||||
import sonia.scm.repository.HealthCheckResult;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
public class SvnFullHealthCheckCommandTest extends AbstractSvnCommandTestBase {
|
||||
|
||||
@Test
|
||||
public void shouldBeOkForValidRepository() {
|
||||
HealthCheckResult check = new SvnFullHealthCheckCommand(createContext()).check();
|
||||
|
||||
assertThat(check.isHealthy()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldDetectMissingFile() {
|
||||
File revision4 = new File(createContext().getDirectory(), "db/revs/0/4");
|
||||
revision4.delete();
|
||||
|
||||
HealthCheckResult check = new SvnFullHealthCheckCommand(createContext()).check();
|
||||
|
||||
assertThat(check.isHealthy()).isFalse();
|
||||
}
|
||||
}
|
||||
@@ -232,6 +232,27 @@ export const useUnarchiveRepository = () => {
|
||||
};
|
||||
};
|
||||
|
||||
export const useRunHealthCheck = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const { mutate, isLoading, error, data } = useMutation<unknown, Error, Repository>(
|
||||
repository => {
|
||||
const link = requiredLink(repository, "runHealthCheck");
|
||||
return apiClient.post(link);
|
||||
},
|
||||
{
|
||||
onSuccess: async (_, repository) => {
|
||||
await queryClient.invalidateQueries(repoQueryKey(repository));
|
||||
}
|
||||
}
|
||||
);
|
||||
return {
|
||||
runHealthCheck: (repository: Repository) => mutate(repository),
|
||||
isLoading,
|
||||
error,
|
||||
isRunning: !!data
|
||||
};
|
||||
};
|
||||
|
||||
export const useExportInfo = (repository: Repository): ApiResult<ExportInfo> => {
|
||||
const link = requiredLink(repository, "exportInfo");
|
||||
//TODO Refetch while exporting to update the page
|
||||
|
||||
@@ -35,9 +35,19 @@ type Props = {
|
||||
active: boolean;
|
||||
className?: string;
|
||||
headColor?: string;
|
||||
headTextColor?: string;
|
||||
};
|
||||
|
||||
export const Modal: FC<Props> = ({ title, closeFunction, body, footer, active, className, headColor = "light" }) => {
|
||||
export const Modal: FC<Props> = ({
|
||||
title,
|
||||
closeFunction,
|
||||
body,
|
||||
footer,
|
||||
active,
|
||||
className,
|
||||
headColor = "light",
|
||||
headTextColor = "black"
|
||||
}) => {
|
||||
const portalRootElement = usePortalRootElement("modalsRoot");
|
||||
|
||||
if (!portalRootElement) {
|
||||
@@ -56,7 +66,7 @@ export const Modal: FC<Props> = ({ title, closeFunction, body, footer, active, c
|
||||
<div className="modal-background" onClick={closeFunction} />
|
||||
<div className="modal-card">
|
||||
<header className={classNames("modal-card-head", `has-background-${headColor}`)}>
|
||||
<p className="modal-card-title is-marginless">{title}</p>
|
||||
<p className={`modal-card-title is-marginless has-text-${headTextColor}`}>{title}</p>
|
||||
<button className="delete" aria-label="close" onClick={closeFunction} />
|
||||
</header>
|
||||
<section className="modal-card-body">{body}</section>
|
||||
|
||||
60
scm-ui/ui-components/src/repos/HealthCheckFailureDetail.tsx
Normal file
60
scm-ui/ui-components/src/repos/HealthCheckFailureDetail.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
import React, { FC } from "react";
|
||||
import { Modal } from "../modals";
|
||||
import { HealthCheckFailure } from "@scm-manager/ui-types";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button } from "../buttons";
|
||||
import HealthCheckFailureList from "./HealthCheckFailureList";
|
||||
|
||||
type Props = {
|
||||
active: boolean;
|
||||
closeFunction: () => void;
|
||||
failures?: HealthCheckFailure[];
|
||||
};
|
||||
|
||||
const HealthCheckFailureDetail: FC<Props> = ({ active, closeFunction, failures }) => {
|
||||
const [t] = useTranslation("repos");
|
||||
|
||||
const footer = <Button label={t("healthCheckFailure.close")} action={closeFunction} color="grey" />;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
body={
|
||||
<div className={"content"}>
|
||||
<HealthCheckFailureList failures={failures} />
|
||||
</div>
|
||||
}
|
||||
title={t("healthCheckFailure.title")}
|
||||
closeFunction={closeFunction}
|
||||
active={active}
|
||||
footer={footer}
|
||||
headColor={"danger"}
|
||||
headTextColor={"white"}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default HealthCheckFailureDetail;
|
||||
69
scm-ui/ui-components/src/repos/HealthCheckFailureList.tsx
Normal file
69
scm-ui/ui-components/src/repos/HealthCheckFailureList.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
import { HealthCheckFailure } from "@scm-manager/ui-types";
|
||||
import React, { FC } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type Props = {
|
||||
failures?: HealthCheckFailure[];
|
||||
};
|
||||
|
||||
const HealthCheckFailureList: FC<Props> = ({ failures }) => {
|
||||
const [t] = useTranslation("plugins");
|
||||
|
||||
const translationOrDefault = (translationKey: string, defaultValue: string) => {
|
||||
const translation = t(translationKey);
|
||||
return translation === translationKey ? defaultValue : translation;
|
||||
};
|
||||
|
||||
if (!failures) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const failureLine = (failure: HealthCheckFailure) => {
|
||||
const summary = translationOrDefault(`healthCheckFailures.${failure.id}.summary`, failure.summary);
|
||||
const description = translationOrDefault(`healthCheckFailures.${failure.id}.description`, failure.description);
|
||||
|
||||
return (
|
||||
<li>
|
||||
<em>{summary}</em>
|
||||
<br />
|
||||
{description}
|
||||
<br />
|
||||
{failure.url && (
|
||||
<a href={failure.url} target="_blank" rel="noreferrer">
|
||||
{t("healthCheckFailures.detailUrl")}
|
||||
</a>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
const failureComponents = failures.map(failureLine);
|
||||
|
||||
return <ul>{failureComponents}</ul>;
|
||||
};
|
||||
|
||||
export default HealthCheckFailureList;
|
||||
@@ -29,6 +29,7 @@ import RepositoryAvatar from "./RepositoryAvatar";
|
||||
import { ExtensionPoint } from "@scm-manager/ui-extensions";
|
||||
import { withTranslation, WithTranslation } from "react-i18next";
|
||||
import styled from "styled-components";
|
||||
import HealthCheckFailureDetail from "./HealthCheckFailureDetail";
|
||||
|
||||
type DateProp = Date | string;
|
||||
|
||||
@@ -39,6 +40,10 @@ type Props = WithTranslation & {
|
||||
baseDate?: DateProp;
|
||||
};
|
||||
|
||||
type State = {
|
||||
showHealthCheck: boolean;
|
||||
};
|
||||
|
||||
const RepositoryTag = styled.span`
|
||||
margin-left: 0.2rem;
|
||||
background-color: #9a9a9a;
|
||||
@@ -50,8 +55,26 @@ const RepositoryTag = styled.span`
|
||||
font-weight: bold;
|
||||
font-size: 0.7rem;
|
||||
`;
|
||||
const RepositoryWarnTag = styled.span`
|
||||
margin-left: 0.2rem;
|
||||
background-color: #f14668;
|
||||
padding: 0.25rem;
|
||||
border-radius: 5px;
|
||||
color: white;
|
||||
overflow: visible;
|
||||
pointer-events: all;
|
||||
font-weight: bold;
|
||||
font-size: 0.7rem;
|
||||
cursor: help;
|
||||
`;
|
||||
|
||||
class RepositoryEntry extends React.Component<Props> {
|
||||
class RepositoryEntry extends React.Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
showHealthCheck: false
|
||||
};
|
||||
}
|
||||
createLink = (repository: Repository) => {
|
||||
return `/repo/${repository.namespace}/${repository.name}`;
|
||||
};
|
||||
@@ -154,6 +177,19 @@ class RepositoryEntry extends React.Component<Props> {
|
||||
repositoryFlags.push(<RepositoryTag title={t("exporting.tooltip")}>{t("repository.exporting")}</RepositoryTag>);
|
||||
}
|
||||
|
||||
if (repository.healthCheckFailures && repository.healthCheckFailures.length > 0) {
|
||||
repositoryFlags.push(
|
||||
<RepositoryWarnTag
|
||||
title={t("healthCheckFailure.tooltip")}
|
||||
onClick={() => {
|
||||
this.setState({ showHealthCheck: true });
|
||||
}}
|
||||
>
|
||||
{t("repository.healthCheckFailure")}
|
||||
</RepositoryWarnTag>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ExtensionPoint name="repository.card.beforeTitle" props={{ repository }} />
|
||||
@@ -168,16 +204,27 @@ class RepositoryEntry extends React.Component<Props> {
|
||||
const footerLeft = this.createFooterLeft(repository, repositoryLink);
|
||||
const footerRight = this.createFooterRight(repository, baseDate);
|
||||
const title = this.createTitle();
|
||||
return (
|
||||
<CardColumn
|
||||
avatar={<RepositoryAvatar repository={repository} />}
|
||||
title={title}
|
||||
description={repository.description}
|
||||
link={repositoryLink}
|
||||
footerLeft={footerLeft}
|
||||
footerRight={footerRight}
|
||||
const modal = (
|
||||
<HealthCheckFailureDetail
|
||||
closeFunction={() => this.setState({ showHealthCheck: false })}
|
||||
active={this.state.showHealthCheck}
|
||||
failures={repository.healthCheckFailures}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{modal}
|
||||
<CardColumn
|
||||
avatar={<RepositoryAvatar repository={repository} />}
|
||||
title={title}
|
||||
description={repository.description}
|
||||
link={repositoryLink}
|
||||
footerLeft={footerLeft}
|
||||
footerRight={footerRight}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -49,6 +49,7 @@ export { default as RepositoryEntry } from "./RepositoryEntry";
|
||||
export { default as RepositoryEntryLink } from "./RepositoryEntryLink";
|
||||
export { default as JumpToFileButton } from "./JumpToFileButton";
|
||||
export { default as CommitAuthor } from "./CommitAuthor";
|
||||
export { default as HealthCheckFailureDetail } from "./HealthCheckFailureDetail";
|
||||
|
||||
export {
|
||||
File,
|
||||
|
||||
@@ -29,6 +29,13 @@ export type NamespaceAndName = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type HealthCheckFailure = {
|
||||
id: string;
|
||||
description: string;
|
||||
summary: string;
|
||||
url: string;
|
||||
};
|
||||
|
||||
export type RepositoryBase = NamespaceAndName & {
|
||||
type: string;
|
||||
contact?: string;
|
||||
@@ -41,6 +48,8 @@ export type Repository = HalRepresentation &
|
||||
lastModified?: string;
|
||||
archived?: boolean;
|
||||
exporting?: boolean;
|
||||
healthCheckFailures?: HealthCheckFailure[];
|
||||
healthCheckRunning?: boolean;
|
||||
};
|
||||
|
||||
export type RepositoryCreation = RepositoryBase & {
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
"creationDate": "Erstellt",
|
||||
"lastModified": "Zuletzt bearbeitet",
|
||||
"archived": "archiviert",
|
||||
"exporting": "Wird exportiert"
|
||||
"exporting": "Wird exportiert",
|
||||
"healthCheckFailure": "fehlerhaft"
|
||||
},
|
||||
"validation": {
|
||||
"namespace-invalid": "Der Namespace des Repository ist ungültig",
|
||||
@@ -263,7 +264,11 @@
|
||||
"initializeRepository": "Repository initiieren",
|
||||
"dangerZone": "Umbenennen, Archivieren und Löschen",
|
||||
"createButton": "Neues Repository erstellen",
|
||||
"importButton": "Repository importieren"
|
||||
"importButton": "Repository importieren",
|
||||
"healthCheckWarning": {
|
||||
"title": "Die letzte Integritätsprüfung dieses Repositories hat Fehler festgestellt. Für weitere Details hier klicken.",
|
||||
"subtitle": "Um die Integritätsprüfung erneut auszuführen, klicken Sie den Schalter unter \"Integritätsprüfung starten\" weiter unten."
|
||||
}
|
||||
},
|
||||
"export": {
|
||||
"subtitle": "Repository exportieren",
|
||||
@@ -435,6 +440,17 @@
|
||||
"exporting": {
|
||||
"tooltip": "Nur lesender Zugriff möglich. Das Repository wird derzeit exportiert."
|
||||
},
|
||||
"healthCheckFailure": {
|
||||
"tooltip": "Dieses Repository ist fehlerhaft. Für weitere Details bitte klicken.",
|
||||
"title": "Fehler im Repository",
|
||||
"close": "Schließen"
|
||||
},
|
||||
"runHealthCheck": {
|
||||
"button": "Integritätsprüfung starten",
|
||||
"subtitle": "Integritätsprüfung",
|
||||
"descriptionNotRunning": "Starten der Integritätsprüfung dieses Repositories. Dieser Vorgang kann einige Zeit in Anspruch nehmen.",
|
||||
"descriptionRunning": "Die Integritätsprüfung für dieses Repository läuft bereits und kann nicht parallel erneut gestartet werden."
|
||||
},
|
||||
"diff": {
|
||||
"jumpToSource": "Zur Quelldatei springen",
|
||||
"jumpToTarget": "Zur vorherigen Version der Datei springen",
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
"creationDate": "Creation Date",
|
||||
"lastModified": "Last Modified",
|
||||
"archived": "archived",
|
||||
"exporting": "exporting"
|
||||
"exporting": "exporting",
|
||||
"healthCheckFailure": "erroneous"
|
||||
},
|
||||
"validation": {
|
||||
"namespace-invalid": "The repository namespace is invalid",
|
||||
@@ -263,7 +264,11 @@
|
||||
"initializeRepository": "Initialize Repository",
|
||||
"dangerZone": "Rename, Archive and Delete",
|
||||
"createButton": "Create Repository",
|
||||
"importButton": "Import Repository"
|
||||
"importButton": "Import Repository",
|
||||
"healthCheckWarning": {
|
||||
"title": "The latest health check for this repository reported failures. Click here for more details.",
|
||||
"subtitle": "To rerun the health check, click the button in the \"Health Check\" part blow."
|
||||
}
|
||||
},
|
||||
"export": {
|
||||
"subtitle": "Repository Export",
|
||||
@@ -429,12 +434,23 @@
|
||||
"cancel": "No"
|
||||
}
|
||||
},
|
||||
"runHealthCheck": {
|
||||
"button": "Run Health Checks",
|
||||
"subtitle": "Health Checks",
|
||||
"descriptionNotRunning": "Run the health checks for this repository. This may take a while.",
|
||||
"descriptionRunning": "Health checks for this repository are currently running and cannot be started again in parallel."
|
||||
},
|
||||
"archive": {
|
||||
"tooltip": "Read only. The archive cannot be changed."
|
||||
},
|
||||
"exporting": {
|
||||
"tooltip": "Read only. The repository is currently being exported."
|
||||
},
|
||||
"healthCheckFailure": {
|
||||
"tooltip": "This repository has health check failures. Click to get details.",
|
||||
"title": "Health Check Failures",
|
||||
"close": "Close"
|
||||
},
|
||||
"diff": {
|
||||
"changes": {
|
||||
"add": "added",
|
||||
|
||||
@@ -25,12 +25,14 @@ import React, { FC } from "react";
|
||||
import { Redirect, useRouteMatch } from "react-router-dom";
|
||||
import RepositoryForm from "../components/form";
|
||||
import { Repository } from "@scm-manager/ui-types";
|
||||
import { ErrorNotification, Subtitle, urls } from "@scm-manager/ui-components";
|
||||
import { ErrorNotification, Notification, Subtitle, urls } from "@scm-manager/ui-components";
|
||||
import { ExtensionPoint } from "@scm-manager/ui-extensions";
|
||||
import RepositoryDangerZone from "./RepositoryDangerZone";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import ExportRepository from "./ExportRepository";
|
||||
import { useIndexLinks, useUpdateRepository } from "@scm-manager/ui-api";
|
||||
import HealthCheckWarning from "./HealthCheckWarning";
|
||||
import RunHealthCheck from "./RunHealthCheck";
|
||||
|
||||
type Props = {
|
||||
repository: Repository;
|
||||
@@ -54,12 +56,16 @@ const EditRepo: FC<Props> = ({ repository }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<HealthCheckWarning repository={repository} />
|
||||
<Subtitle subtitle={t("repositoryForm.subtitle")} />
|
||||
<ErrorNotification error={error} />
|
||||
<RepositoryForm repository={repository} loading={isLoading} modifyRepository={update} />
|
||||
<ExtensionPoint name="repo-config.details" props={extensionProps} renderAll={true}/>
|
||||
<ExtensionPoint name="repo-config.details" props={extensionProps} renderAll={true} />
|
||||
{repository._links.exportInfo && <ExportRepository repository={repository} />}
|
||||
<ExtensionPoint name="repo-config.route" props={extensionProps} renderAll={true} />
|
||||
{(repository._links.runHealthCheck || repository.healthCheckRunning) && (
|
||||
<RunHealthCheck repository={repository} />
|
||||
)}
|
||||
<RepositoryDangerZone repository={repository} indexLinks={indexLinks} />
|
||||
</>
|
||||
);
|
||||
|
||||
62
scm-ui/ui-webapp/src/repos/containers/HealthCheckWarning.tsx
Normal file
62
scm-ui/ui-webapp/src/repos/containers/HealthCheckWarning.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
import React, { FC, useState } from "react";
|
||||
import { Notification } from "@scm-manager/ui-components";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Repository } from "@scm-manager/ui-types";
|
||||
import { HealthCheckFailureDetail } from "@scm-manager/ui-components";
|
||||
|
||||
type Props = {
|
||||
repository: Repository;
|
||||
};
|
||||
|
||||
const HealthCheckWarning: FC<Props> = ({ repository }) => {
|
||||
const [t] = useTranslation("repos");
|
||||
const [showHealthCheck, setShowHealthCheck] = useState(false);
|
||||
|
||||
if (repository.healthCheckFailures?.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const modal = (
|
||||
<HealthCheckFailureDetail
|
||||
closeFunction={() => setShowHealthCheck(false)}
|
||||
active={showHealthCheck}
|
||||
failures={repository.healthCheckFailures}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<Notification type="danger">
|
||||
{modal}
|
||||
<div className={"has-cursor-pointer"} onClick={() => setShowHealthCheck(true)}>
|
||||
<div>{t("repositoryForm.healthCheckWarning.title")}</div>
|
||||
<div className={"is-small"}>{t("repositoryForm.healthCheckWarning.subtitle")}</div>
|
||||
</div>
|
||||
</Notification>
|
||||
);
|
||||
};
|
||||
|
||||
export default HealthCheckWarning;
|
||||
@@ -76,7 +76,6 @@ const RepositoryDangerZone: FC<Props> = ({ repository, indexLinks }) => {
|
||||
if (repository?._links?.unarchive) {
|
||||
dangerZone.push(<UnarchiveRepo repository={repository} />);
|
||||
}
|
||||
|
||||
if (dangerZone.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import React from "react";
|
||||
import React, { useState } from "react";
|
||||
import { Redirect, Route, Link as RouteLink, Switch, useRouteMatch } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { binder, ExtensionPoint } from "@scm-manager/ui-extensions";
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
CustomQueryFlexWrappedColumns,
|
||||
ErrorPage,
|
||||
FileControlFactory,
|
||||
HealthCheckFailureDetail,
|
||||
JumpToFileButton,
|
||||
Loading,
|
||||
NavLink,
|
||||
@@ -68,6 +69,15 @@ const RepositoryTag = styled.span`
|
||||
font-weight: bold;
|
||||
`;
|
||||
|
||||
const RepositoryWarnTag = styled.span`
|
||||
margin-left: 0.2rem;
|
||||
background-color: #f14668;
|
||||
padding: 0.4rem;
|
||||
border-radius: 5px;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
`;
|
||||
|
||||
type UrlParams = {
|
||||
namespace: string;
|
||||
name: string;
|
||||
@@ -86,6 +96,7 @@ const RepositoryRoot = () => {
|
||||
const match = useRouteMatch<UrlParams>();
|
||||
const { isLoading, error, repository } = useRepositoryFromUrl(match);
|
||||
const indexLinks = useIndexLinks();
|
||||
const [showHealthCheck, setShowHealthCheck] = useState(false);
|
||||
|
||||
const [t] = useTranslation("repos");
|
||||
|
||||
@@ -174,6 +185,16 @@ const RepositoryRoot = () => {
|
||||
);
|
||||
}
|
||||
|
||||
if (repository.healthCheckFailures && repository.healthCheckFailures.length > 0) {
|
||||
repositoryFlags.push(
|
||||
<Tooltip message={t("healthCheckFailure.tooltip")}>
|
||||
<RepositoryWarnTag className="is-size-6" onClick={() => setShowHealthCheck(true)}>
|
||||
{t("repository.healthCheckFailure")}
|
||||
</RepositoryWarnTag>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
const titleComponent = (
|
||||
<>
|
||||
<RouteLink to={`/repos/${repository.namespace}/`} className={"has-text-dark"}>
|
||||
@@ -221,6 +242,14 @@ const RepositoryRoot = () => {
|
||||
return `${url}/code/changesets`;
|
||||
};
|
||||
|
||||
const modal = (
|
||||
<HealthCheckFailureDetail
|
||||
closeFunction={() => setShowHealthCheck(false)}
|
||||
active={showHealthCheck}
|
||||
failures={repository.healthCheckFailures}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<StateMenuContextProvider>
|
||||
<Page
|
||||
@@ -233,6 +262,7 @@ const RepositoryRoot = () => {
|
||||
</>
|
||||
}
|
||||
>
|
||||
{modal}
|
||||
<CustomQueryFlexWrappedColumns>
|
||||
<PrimaryContentColumn>
|
||||
<Switch>
|
||||
|
||||
72
scm-ui/ui-webapp/src/repos/containers/RunHealthCheck.tsx
Normal file
72
scm-ui/ui-webapp/src/repos/containers/RunHealthCheck.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import React, { FC } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Repository } from "@scm-manager/ui-types";
|
||||
import { Button, ErrorNotification, Level, Subtitle } from "@scm-manager/ui-components";
|
||||
import { useRunHealthCheck } from "@scm-manager/ui-api";
|
||||
import styled from "styled-components";
|
||||
|
||||
type Props = {
|
||||
repository: Repository;
|
||||
};
|
||||
|
||||
const MarginTopButton = styled(Button)`
|
||||
margin-top: 1rem;
|
||||
`;
|
||||
|
||||
const RunHealthCheck: FC<Props> = ({ repository }) => {
|
||||
const { isLoading, error, runHealthCheck } = useRunHealthCheck();
|
||||
const [t] = useTranslation("repos");
|
||||
|
||||
const runHealthCheckCallback = () => {
|
||||
runHealthCheck(repository);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<hr />
|
||||
<ErrorNotification error={error} />
|
||||
<Subtitle>{t("runHealthCheck.subtitle")}</Subtitle>
|
||||
<p>
|
||||
{repository.healthCheckRunning
|
||||
? t("runHealthCheck.descriptionRunning")
|
||||
: t("runHealthCheck.descriptionNotRunning")}
|
||||
</p>
|
||||
<Level
|
||||
right={
|
||||
<MarginTopButton
|
||||
color="warning"
|
||||
icon="heartbeat"
|
||||
label={t("runHealthCheck.button")}
|
||||
action={runHealthCheckCallback}
|
||||
loading={isLoading || repository.healthCheckRunning}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default RunHealthCheck;
|
||||
@@ -21,14 +21,25 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import de.otto.edison.hal.HalRepresentation;
|
||||
import de.otto.edison.hal.Links;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter @Setter
|
||||
public class HealthCheckFailureDto {
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@SuppressWarnings("java:S2160") // we do not need this for dto
|
||||
public class HealthCheckFailureDto extends HalRepresentation {
|
||||
public HealthCheckFailureDto(Links links) {
|
||||
super(links);
|
||||
}
|
||||
|
||||
private String id;
|
||||
private String description;
|
||||
private String summary;
|
||||
private String url;
|
||||
|
||||
@@ -59,6 +59,7 @@ public class RepositoryDto extends HalRepresentation implements CreateRepository
|
||||
private String type;
|
||||
private boolean archived;
|
||||
private boolean exporting;
|
||||
private boolean healthCheckRunning;
|
||||
|
||||
RepositoryDto(Links links, Embedded embedded) {
|
||||
super(links, embedded);
|
||||
|
||||
@@ -30,6 +30,7 @@ import io.swagger.v3.oas.annotations.media.ExampleObject;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.parameters.RequestBody;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import sonia.scm.repository.HealthCheckService;
|
||||
import sonia.scm.repository.NamespaceAndName;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryManager;
|
||||
@@ -61,17 +62,19 @@ public class RepositoryResource {
|
||||
private final RepositoryManager manager;
|
||||
private final SingleResourceManagerAdapter<Repository, RepositoryDto> adapter;
|
||||
private final RepositoryBasedResourceProvider resourceProvider;
|
||||
private final HealthCheckService healthCheckService;
|
||||
|
||||
@Inject
|
||||
public RepositoryResource(
|
||||
RepositoryToRepositoryDtoMapper repositoryToDtoMapper,
|
||||
RepositoryDtoToRepositoryMapper dtoToRepositoryMapper, RepositoryManager manager,
|
||||
RepositoryBasedResourceProvider resourceProvider) {
|
||||
RepositoryBasedResourceProvider resourceProvider, HealthCheckService healthCheckService) {
|
||||
this.dtoToRepositoryMapper = dtoToRepositoryMapper;
|
||||
this.manager = manager;
|
||||
this.repositoryToDtoMapper = repositoryToDtoMapper;
|
||||
this.adapter = new SingleResourceManagerAdapter<>(manager, Repository.class);
|
||||
this.resourceProvider = resourceProvider;
|
||||
this.healthCheckService = healthCheckService;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -271,6 +274,25 @@ public class RepositoryResource {
|
||||
manager.unarchive(repository);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("runHealthCheck")
|
||||
@Operation(summary = "Check health of repository", description = "Starts a full health check for the repository.", tags = "Repository")
|
||||
@ApiResponse(responseCode = "204", description = "check started")
|
||||
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
|
||||
@ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"repository:healthCheck\" privilege")
|
||||
@ApiResponse(
|
||||
responseCode = "404",
|
||||
description = "not found, no repository with the specified namespace and name available",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.ERROR_TYPE,
|
||||
schema = @Schema(implementation = ErrorDto.class)
|
||||
))
|
||||
@ApiResponse(responseCode = "500", description = "internal server error")
|
||||
public void runHealthCheck(@PathParam("namespace") String namespace, @PathParam("name") String name) {
|
||||
Repository repository = loadBy(namespace, name).get();
|
||||
healthCheckService.fullCheck(repository);
|
||||
}
|
||||
|
||||
private Repository processUpdate(RepositoryDto repositoryDto, Repository existing) {
|
||||
Repository changedRepository = dtoToRepositoryMapper.map(repositoryDto, existing.getId());
|
||||
changedRepository.setPermissions(existing.getPermissions());
|
||||
|
||||
@@ -32,10 +32,12 @@ import org.mapstruct.Mapper;
|
||||
import org.mapstruct.Mapping;
|
||||
import org.mapstruct.MappingTarget;
|
||||
import org.mapstruct.ObjectFactory;
|
||||
import sonia.scm.SCMContextProvider;
|
||||
import sonia.scm.config.ScmConfiguration;
|
||||
import sonia.scm.repository.DefaultRepositoryExportingCheck;
|
||||
import sonia.scm.repository.Feature;
|
||||
import sonia.scm.repository.HealthCheckFailure;
|
||||
import sonia.scm.repository.HealthCheckService;
|
||||
import sonia.scm.repository.NamespaceStrategy;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryPermissions;
|
||||
@@ -68,9 +70,28 @@ public abstract class RepositoryToRepositoryDtoMapper extends BaseMapper<Reposit
|
||||
private RepositoryServiceFactory serviceFactory;
|
||||
@Inject
|
||||
private Set<NamespaceStrategy> strategies;
|
||||
@Inject
|
||||
private HealthCheckService healthCheckService;
|
||||
@Inject
|
||||
private SCMContextProvider contextProvider;
|
||||
|
||||
abstract HealthCheckFailureDto toDto(HealthCheckFailure failure);
|
||||
|
||||
@ObjectFactory
|
||||
HealthCheckFailureDto createHealthCheckFailureDto(HealthCheckFailure failure) {
|
||||
String url = failure.getUrl(contextProvider.getDocumentationVersion());
|
||||
if (url == null) {
|
||||
return new HealthCheckFailureDto();
|
||||
} else {
|
||||
return new HealthCheckFailureDto(Links.linkingTo().single(link("documentation", url)).build());
|
||||
}
|
||||
}
|
||||
|
||||
@AfterMapping
|
||||
void updateHealthCheckUrlForCurrentVersion(HealthCheckFailure failure, @MappingTarget HealthCheckFailureDto dto) {
|
||||
dto.setUrl(failure.getUrl(contextProvider.getDocumentationVersion()));
|
||||
}
|
||||
|
||||
@Mapping(target = "attributes", ignore = true) // We do not map HAL attributes
|
||||
@Mapping(target = "exporting", ignore = true)
|
||||
@Override
|
||||
@@ -138,11 +159,16 @@ public abstract class RepositoryToRepositoryDtoMapper extends BaseMapper<Reposit
|
||||
linksBuilder.single(link("changesets", resourceLinks.changeset().all(repository.getNamespace(), repository.getName())));
|
||||
linksBuilder.single(link("sources", resourceLinks.source().selfWithoutRevision(repository.getNamespace(), repository.getName())));
|
||||
linksBuilder.single(link("paths", resourceLinks.repository().paths(repository.getNamespace(), repository.getName())));
|
||||
if (RepositoryPermissions.healthCheck(repository).isPermitted() && !healthCheckService.checkRunning(repository)) {
|
||||
linksBuilder.single(link("runHealthCheck", resourceLinks.repository().runHealthCheck(repository.getNamespace(), repository.getName())));
|
||||
}
|
||||
|
||||
Embedded.Builder embeddedBuilder = embeddedBuilder();
|
||||
applyEnrichers(new EdisonHalAppender(linksBuilder, embeddedBuilder), repository);
|
||||
|
||||
return new RepositoryDto(linksBuilder.build(), embeddedBuilder.build());
|
||||
RepositoryDto repositoryDto = new RepositoryDto(linksBuilder.build(), embeddedBuilder.build());
|
||||
repositoryDto.setHealthCheckRunning(healthCheckService.checkRunning(repository));
|
||||
return repositoryDto;
|
||||
}
|
||||
|
||||
private boolean isRenameNamespacePossible() {
|
||||
|
||||
@@ -410,6 +410,10 @@ class ResourceLinks {
|
||||
String paths(String namespace, String name) {
|
||||
return repositoryPathsLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("paths").parameters().method("collect").parameters("_REVISION_").href().replace("_REVISION_", "{revision}");
|
||||
}
|
||||
|
||||
String runHealthCheck(String namespace, String name) {
|
||||
return repositoryLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("runHealthCheck").parameters().href();
|
||||
}
|
||||
}
|
||||
|
||||
RepositoryCollectionLinks repositoryCollection() {
|
||||
|
||||
@@ -66,11 +66,13 @@ import sonia.scm.net.ahc.XmlContentTransformer;
|
||||
import sonia.scm.plugin.DefaultPluginManager;
|
||||
import sonia.scm.plugin.PluginLoader;
|
||||
import sonia.scm.plugin.PluginManager;
|
||||
import sonia.scm.repository.DefaultHealthCheckService;
|
||||
import sonia.scm.repository.DefaultNamespaceManager;
|
||||
import sonia.scm.repository.DefaultRepositoryManager;
|
||||
import sonia.scm.repository.DefaultRepositoryProvider;
|
||||
import sonia.scm.repository.DefaultRepositoryRoleManager;
|
||||
import sonia.scm.repository.HealthCheckContextListener;
|
||||
import sonia.scm.repository.HealthCheckService;
|
||||
import sonia.scm.repository.NamespaceManager;
|
||||
import sonia.scm.repository.NamespaceStrategy;
|
||||
import sonia.scm.repository.NamespaceStrategyProvider;
|
||||
@@ -259,6 +261,8 @@ class ScmServletModule extends ServletModule {
|
||||
bind(RootURL.class).to(DefaultRootURL.class);
|
||||
|
||||
bind(PermissionProvider.class).to(RepositoryPermissionProvider.class);
|
||||
|
||||
bind(HealthCheckService.class).to(DefaultHealthCheckService.class);
|
||||
}
|
||||
|
||||
private <T> void bind(Class<T> clazz, Class<? extends T> defaultImplementation) {
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.repository;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
public class DefaultHealthCheckService implements HealthCheckService {
|
||||
|
||||
private final HealthChecker healthChecker;
|
||||
|
||||
@Inject
|
||||
public DefaultHealthCheckService(HealthChecker healthChecker) {
|
||||
this.healthChecker = healthChecker;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fullCheck(String id) {
|
||||
RepositoryPermissions.healthCheck(id).check();
|
||||
healthChecker.fullCheck(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fullCheck(Repository repository) {
|
||||
RepositoryPermissions.healthCheck(repository).check();
|
||||
healthChecker.fullCheck(repository);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean checkRunning(String repositoryId) {
|
||||
return healthChecker.checkRunning(repositoryId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean checkRunning(Repository repository) {
|
||||
return healthChecker.checkRunning(repository.getId());
|
||||
}
|
||||
}
|
||||
@@ -77,14 +77,16 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager {
|
||||
private final Set<Type> types;
|
||||
private final Provider<NamespaceStrategy> namespaceStrategyProvider;
|
||||
private final ManagerDaoAdapter<Repository> managerDaoAdapter;
|
||||
private final RepositoryPostProcessor repositoryPostProcessor;
|
||||
|
||||
@Inject
|
||||
public DefaultRepositoryManager(SCMContextProvider contextProvider, KeyGenerator keyGenerator,
|
||||
RepositoryDAO repositoryDAO, Set<RepositoryHandler> handlerSet,
|
||||
Provider<NamespaceStrategy> namespaceStrategyProvider) {
|
||||
Provider<NamespaceStrategy> namespaceStrategyProvider, RepositoryPostProcessor repositoryPostProcessor) {
|
||||
this.keyGenerator = keyGenerator;
|
||||
this.repositoryDAO = repositoryDAO;
|
||||
this.namespaceStrategyProvider = namespaceStrategyProvider;
|
||||
this.repositoryPostProcessor = repositoryPostProcessor;
|
||||
|
||||
handlerMap = new HashMap<>();
|
||||
types = new HashSet<>();
|
||||
@@ -220,7 +222,7 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager {
|
||||
Repository repository = repositoryDAO.get(id);
|
||||
|
||||
if (repository != null) {
|
||||
repository = repository.clone();
|
||||
repository = postProcess(repository);
|
||||
}
|
||||
|
||||
return repository;
|
||||
@@ -236,7 +238,7 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager {
|
||||
|
||||
if (repository != null) {
|
||||
RepositoryPermissions.read(repository).check();
|
||||
repository = repository.clone();
|
||||
repository = postProcess(repository);
|
||||
}
|
||||
|
||||
return repository;
|
||||
@@ -289,7 +291,7 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager {
|
||||
throw new NoChangesMadeException(repository);
|
||||
}
|
||||
|
||||
Repository changedRepository = originalRepository.clone();
|
||||
Repository changedRepository = postProcess(originalRepository);
|
||||
|
||||
changedRepository.setArchived(archived);
|
||||
|
||||
@@ -314,7 +316,7 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager {
|
||||
if (handlerMap.containsKey(repository.getType())
|
||||
&& filter.test(repository)
|
||||
&& RepositoryPermissions.read().isPermitted(repository)) {
|
||||
Repository r = repository.clone();
|
||||
Repository r = postProcess(repository);
|
||||
|
||||
repositories.add(r);
|
||||
}
|
||||
@@ -342,7 +344,7 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager {
|
||||
@Override
|
||||
public void append(Collection<Repository> collection, Repository item) {
|
||||
if (RepositoryPermissions.read().isPermitted(item)) {
|
||||
collection.add(item.clone());
|
||||
collection.add(postProcess(item));
|
||||
}
|
||||
}
|
||||
}, start, limit);
|
||||
@@ -427,4 +429,10 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager {
|
||||
|
||||
return handler;
|
||||
}
|
||||
|
||||
private Repository postProcess(Repository repository) {
|
||||
Repository clone = repository.clone();
|
||||
repositoryPostProcessor.postProcess(repository);
|
||||
return clone;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
|
||||
package sonia.scm.repository;
|
||||
|
||||
//~--- non-JDK imports --------------------------------------------------------
|
||||
@@ -121,7 +121,7 @@ public class HealthCheckContextListener implements ServletContextListener
|
||||
{
|
||||
|
||||
// excute health checks for all repsitories asynchronous
|
||||
SecurityUtils.getSubject().execute(healthChecker::checkAll);
|
||||
SecurityUtils.getSubject().execute(healthChecker::lightCheckAll);
|
||||
}
|
||||
|
||||
//~--- fields -------------------------------------------------------------
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.repository;
|
||||
|
||||
public interface HealthCheckService {
|
||||
|
||||
void fullCheck(String id);
|
||||
|
||||
void fullCheck(Repository repository);
|
||||
|
||||
boolean checkRunning(String repositoryId);
|
||||
|
||||
boolean checkRunning(Repository repository);
|
||||
}
|
||||
@@ -24,17 +24,27 @@
|
||||
|
||||
package sonia.scm.repository;
|
||||
|
||||
import com.github.sdorra.ssp.PermissionActionCheck;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.inject.Inject;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.NotFoundException;
|
||||
import sonia.scm.repository.api.Command;
|
||||
import sonia.scm.repository.api.RepositoryService;
|
||||
import sonia.scm.repository.api.RepositoryServiceFactory;
|
||||
|
||||
import javax.inject.Singleton;
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
import static java.util.Collections.synchronizedCollection;
|
||||
|
||||
public final class HealthChecker {
|
||||
@Singleton
|
||||
final class HealthChecker {
|
||||
|
||||
private static final Logger logger =
|
||||
LoggerFactory.getLogger(HealthChecker.class);
|
||||
@@ -42,40 +52,68 @@ public final class HealthChecker {
|
||||
private final Set<HealthCheck> checks;
|
||||
|
||||
private final RepositoryManager repositoryManager;
|
||||
private final RepositoryServiceFactory repositoryServiceFactory;
|
||||
private final RepositoryPostProcessor repositoryPostProcessor;
|
||||
|
||||
private final Collection<String> checksRunning = synchronizedCollection(new HashSet<>());
|
||||
|
||||
private final ExecutorService healthCheckExecutor = Executors.newSingleThreadExecutor();
|
||||
|
||||
@Inject
|
||||
public HealthChecker(Set<HealthCheck> checks,
|
||||
RepositoryManager repositoryManager) {
|
||||
HealthChecker(Set<HealthCheck> checks,
|
||||
RepositoryManager repositoryManager,
|
||||
RepositoryServiceFactory repositoryServiceFactory,
|
||||
RepositoryPostProcessor repositoryPostProcessor) {
|
||||
this.checks = checks;
|
||||
this.repositoryManager = repositoryManager;
|
||||
this.repositoryServiceFactory = repositoryServiceFactory;
|
||||
this.repositoryPostProcessor = repositoryPostProcessor;
|
||||
}
|
||||
|
||||
public void check(String id){
|
||||
void lightCheck(String id) {
|
||||
RepositoryPermissions.healthCheck(id).check();
|
||||
|
||||
Repository repository = loadRepository(id);
|
||||
|
||||
doLightCheck(repository);
|
||||
}
|
||||
|
||||
void fullCheck(String id) {
|
||||
RepositoryPermissions.healthCheck(id).check();
|
||||
|
||||
Repository repository = loadRepository(id);
|
||||
|
||||
doFullCheck(repository);
|
||||
}
|
||||
|
||||
void lightCheck(Repository repository) {
|
||||
RepositoryPermissions.healthCheck(repository).check();
|
||||
|
||||
doLightCheck(repository);
|
||||
}
|
||||
|
||||
void fullCheck(Repository repository) {
|
||||
RepositoryPermissions.healthCheck(repository).check();
|
||||
|
||||
doFullCheck(repository);
|
||||
}
|
||||
|
||||
private Repository loadRepository(String id) {
|
||||
Repository repository = repositoryManager.get(id);
|
||||
|
||||
if (repository == null) {
|
||||
throw new NotFoundException(Repository.class, id);
|
||||
}
|
||||
|
||||
doCheck(repository);
|
||||
return repository;
|
||||
}
|
||||
|
||||
public void check(Repository repository)
|
||||
{
|
||||
RepositoryPermissions.healthCheck(repository).check();
|
||||
|
||||
doCheck(repository);
|
||||
}
|
||||
|
||||
public void checkAll() {
|
||||
void lightCheckAll() {
|
||||
logger.debug("check health of all repositories");
|
||||
|
||||
for (Repository repository : repositoryManager.getAll()) {
|
||||
if (RepositoryPermissions.healthCheck().isPermitted(repository)) {
|
||||
try {
|
||||
check(repository);
|
||||
lightCheck(repository);
|
||||
} catch (NotFoundException ex) {
|
||||
logger.error("health check ends with exception", ex);
|
||||
}
|
||||
@@ -87,32 +125,89 @@ public final class HealthChecker {
|
||||
}
|
||||
}
|
||||
|
||||
private void doCheck(Repository repository){
|
||||
logger.info("start health check for repository {}", repository);
|
||||
private void doLightCheck(Repository repository) {
|
||||
withLockedRepository(repository, () -> {
|
||||
HealthCheckResult result = gatherLightChecks(repository);
|
||||
|
||||
if (result.isUnhealthy()) {
|
||||
logger.warn("repository {} is unhealthy: {}", repository,
|
||||
result);
|
||||
} else {
|
||||
logger.info("repository {} is healthy", repository);
|
||||
}
|
||||
|
||||
storeResult(repository, result);
|
||||
});
|
||||
}
|
||||
|
||||
private HealthCheckResult gatherLightChecks(Repository repository) {
|
||||
logger.info("start light health check for repository {}", repository);
|
||||
|
||||
HealthCheckResult result = HealthCheckResult.healthy();
|
||||
|
||||
for (HealthCheck check : checks) {
|
||||
logger.trace("execute health check {} for repository {}",
|
||||
logger.trace("execute light health check {} for repository {}",
|
||||
check.getClass(), repository);
|
||||
result = result.merge(check.check(repository));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
if (result.isUnhealthy()) {
|
||||
logger.warn("repository {} is unhealthy: {}", repository,
|
||||
result);
|
||||
} else {
|
||||
logger.info("repository {} is healthy", repository);
|
||||
private void doFullCheck(Repository repository) {
|
||||
withLockedRepository(repository, () ->
|
||||
runInExecutorAndWait(repository, () -> {
|
||||
HealthCheckResult lightCheckResult = gatherLightChecks(repository);
|
||||
HealthCheckResult fullCheckResult = gatherFullChecks(repository);
|
||||
HealthCheckResult result = lightCheckResult.merge(fullCheckResult);
|
||||
|
||||
storeResult(repository, result);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private void withLockedRepository(Repository repository, Runnable runnable) {
|
||||
if (!checksRunning.add(repository.getId())) {
|
||||
logger.debug("check for repository {} is already running", repository);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(repository.isHealthy() && result.isHealthy())) {
|
||||
logger.trace("store health check results for repository {}",
|
||||
repository);
|
||||
repository.setHealthCheckFailures(
|
||||
ImmutableList.copyOf(result.getFailures()));
|
||||
repositoryManager.modify(repository);
|
||||
try {
|
||||
runnable.run();
|
||||
} finally {
|
||||
checksRunning.remove(repository.getId());
|
||||
}
|
||||
}
|
||||
|
||||
private void runInExecutorAndWait(Repository repository, Runnable runnable) {
|
||||
try {
|
||||
healthCheckExecutor.submit(runnable).get();
|
||||
} catch (ExecutionException e) {
|
||||
logger.warn("could not submit task for health check for repository {}", repository, e);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
private HealthCheckResult gatherFullChecks(Repository repository) {
|
||||
try (RepositoryService service = repositoryServiceFactory.create(repository)) {
|
||||
if (service.isSupported(Command.FULL_HEALTH_CHECK)) {
|
||||
return service.getFullCheckCommand().check();
|
||||
} else {
|
||||
return HealthCheckResult.healthy();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new InternalRepositoryException(repository, "error during full health check", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void storeResult(Repository repository, HealthCheckResult result) {
|
||||
if (!(repository.isHealthy() && result.isHealthy())) {
|
||||
logger.trace("store health check results for repository {}",
|
||||
repository);
|
||||
repositoryPostProcessor.setCheckResults(repository, result.getFailures());
|
||||
}
|
||||
}
|
||||
|
||||
public boolean checkRunning(String repositoryId) {
|
||||
return checksRunning.contains(repositoryId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.repository;
|
||||
|
||||
import sonia.scm.event.ScmEventBus;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static com.google.common.collect.ImmutableList.copyOf;
|
||||
import static java.util.Collections.emptyList;
|
||||
|
||||
@Singleton
|
||||
class RepositoryPostProcessor {
|
||||
|
||||
private final ScmEventBus eventBus;
|
||||
|
||||
private final Map<String, List<HealthCheckFailure>> checkResults = new HashMap<>();
|
||||
|
||||
@Inject
|
||||
RepositoryPostProcessor(ScmEventBus eventBus) {
|
||||
this.eventBus = eventBus;
|
||||
}
|
||||
|
||||
void setCheckResults(Repository repository, Collection<HealthCheckFailure> failures) {
|
||||
List<HealthCheckFailure> oldFailures = getCheckResults(repository.getId());
|
||||
List<HealthCheckFailure> copyOfFailures = copyOf(failures);
|
||||
checkResults.put(repository.getId(), copyOfFailures);
|
||||
repository.setHealthCheckFailures(copyOfFailures);
|
||||
eventBus.post(new HealthCheckEvent(repository, oldFailures, copyOfFailures));
|
||||
}
|
||||
|
||||
void postProcess(Repository repository) {
|
||||
repository.setHealthCheckFailures(getCheckResults(repository.getId()));
|
||||
}
|
||||
|
||||
private List<HealthCheckFailure> getCheckResults(String repositoryId) {
|
||||
return checkResults.getOrDefault(repositoryId, emptyList());
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,9 @@
|
||||
<permission>
|
||||
<value>repository:read,rename:*</value>
|
||||
</permission>
|
||||
<permission>
|
||||
<value>repository:read,healthCheck:*</value>
|
||||
</permission>
|
||||
<permission>
|
||||
<value>repository:*</value>
|
||||
</permission>
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
<verb>permissionWrite</verb>
|
||||
<verb read-only="true">archive</verb>
|
||||
<verb read-only="true">export</verb>
|
||||
<verb>healthCheck</verb>
|
||||
<verb>*</verb>
|
||||
</verbs>
|
||||
<roles>
|
||||
|
||||
@@ -53,6 +53,12 @@
|
||||
"displayName": "Repositories exportieren",
|
||||
"description": "Achtung: Darf alle Repositories inkl. aller Metadaten exportieren."
|
||||
}
|
||||
},
|
||||
"read,healthCheck": {
|
||||
"*": {
|
||||
"displayName": "Integritätsprüfungen ausführen",
|
||||
"description": "Darf alle Repositories sehen und Integritätsprüfungen starten."
|
||||
}
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
@@ -165,6 +171,10 @@
|
||||
"displayName": "Repository exportieren",
|
||||
"description": "Achtung: Darf das Repository inkl. aller Metadaten exportieren."
|
||||
},
|
||||
"healthCheck": {
|
||||
"displayName": "überprüfen",
|
||||
"description": "Darf das Repository überprüfen."
|
||||
},
|
||||
"*": {
|
||||
"displayName": "Alle Repository Rechte",
|
||||
"description": "Darf im Repository Kontext alles ausführen. Dies beinhaltet alle Repository Berechtigungen."
|
||||
@@ -350,6 +360,65 @@
|
||||
"description": "Der Import ist für den gegebenen Repository-Typen nicht möglich."
|
||||
}
|
||||
},
|
||||
"healthCheckFailures": {
|
||||
"detailUrl": "Hier finden sich weitere Informationen.",
|
||||
"AnOTx99ex1": {
|
||||
"summary": "Inkompatibles DB Format",
|
||||
"description": "Das Subversion DB Format ist inkompatibel mit der SVN Version, die im SCM-Manager genutzt wird."
|
||||
},
|
||||
"4IOTx8pvv1": {
|
||||
"summary": "DB/Format Datei kann nicht gelesen werden",
|
||||
"description": "Die DB/Format Datei des Repositories konnte nicht gelesen werden."
|
||||
},
|
||||
"6TOTx9RLD1": {
|
||||
"summary": "Das Repository lässt sich nicht öffnen",
|
||||
"description": "Das SVN Repository kann nicht geöffnet werden."
|
||||
},
|
||||
"A9OTx8leC1": {
|
||||
"summary": "DB/Format Datei kann nicht gefunden werden",
|
||||
"description": "Das Subversion Repository enthält keine DB/Format Datei."
|
||||
},
|
||||
"2OOTx6ta71": {
|
||||
"summary": "Repository hat keinen Typen",
|
||||
"description": "Das Repository hat keinen konfigurierten Typ."
|
||||
},
|
||||
"CqOTx7Jkq1": {
|
||||
"summary": "Kein Handler für Repository Typ",
|
||||
"description": "Es ist kein Handler für den Typen des Repositories registriert."
|
||||
},
|
||||
"AcOTx7fD51": {
|
||||
"summary": "Handler kann Verzeichnis nicht zurückgeben",
|
||||
"description": "Der Handler war nicht in der Lage, ein Verzeichnis für das Repository zurückzugeben."
|
||||
},
|
||||
"1oOTx803F1": {
|
||||
"summary": "Repository Verzeichnis existiert nicht",
|
||||
"description": "Das Repository existiert nicht. Eventuell wurde es außerhalb des SCM-Managers gelöscht."
|
||||
},
|
||||
"AKOdhQ0pw1": {
|
||||
"summary": "Kein .git oder refs Verzeichnis gefunden",
|
||||
"description": "Das Git-Repository enthält weder ein '.git' noch ein 'refs' Verzeichnis"
|
||||
},
|
||||
"FaSUYbZUR1": {
|
||||
"summary": "'hg verify' fehlgeschlagen",
|
||||
"description": "Die Prüfung 'hg verify' ist für das Repository fehlgeschlagen."
|
||||
},
|
||||
"6bOdhOXpB1": {
|
||||
"summary": "Verzeichnis .hg nicht gefunden",
|
||||
"description": "Das Mercurial Repository enthält kein .hg Verzeichnis."
|
||||
},
|
||||
"9cSV1eaVF1": {
|
||||
"summary": "Repository Verzeichnis nicht schreibbar",
|
||||
"description": "Der Systemuser hat keine Berechtigung, Dateien im Verzeichnis für das Repository zu erstellen oder zu löschen."
|
||||
},
|
||||
"6bSUg4dZ41": {
|
||||
"summary": "Metadaten Datei nicht schreibbar",
|
||||
"description": "Die Datei für die Metadaten des Repositories kann vom Systemuser nicht geschrieben werden."
|
||||
},
|
||||
"5FSV2kreE1": {
|
||||
"summary": "'svn verify' fehlgeschlagen",
|
||||
"description": "Die Prüfung 'svn verify' ist für das Repository fehlgeschlagen."
|
||||
}
|
||||
},
|
||||
"namespaceStrategies": {
|
||||
"UsernameNamespaceStrategy": "Benutzername",
|
||||
"CustomNamespaceStrategy": "Benutzerdefiniert",
|
||||
|
||||
@@ -53,6 +53,12 @@
|
||||
"displayName": "Export repositories",
|
||||
"description": "Attention: May export all repositories including all metadata"
|
||||
}
|
||||
},
|
||||
"read,healthCheck": {
|
||||
"*": {
|
||||
"displayName": "Run repository health checks",
|
||||
"description": "May see all repositories and run health checks for them"
|
||||
}
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
@@ -165,6 +171,10 @@
|
||||
"displayName": "export repository",
|
||||
"description": "Attention: May export the repository including all metadata"
|
||||
},
|
||||
"healthCheck": {
|
||||
"displayName": "run health checks",
|
||||
"description": "May run health checks for this repository"
|
||||
},
|
||||
"*": {
|
||||
"displayName": "own repository",
|
||||
"description": "May change everything for the repository (includes all other permissions)"
|
||||
@@ -350,6 +360,9 @@
|
||||
"description": "The import is not possible for the given repository type."
|
||||
}
|
||||
},
|
||||
"healthChecksFailures": {
|
||||
"detailUrl": "Find more details here."
|
||||
},
|
||||
"namespaceStrategies": {
|
||||
"UsernameNamespaceStrategy": "Username",
|
||||
"CustomNamespaceStrategy": "Custom",
|
||||
|
||||
@@ -55,6 +55,7 @@ import sonia.scm.importexport.FullScmRepositoryImporter;
|
||||
import sonia.scm.importexport.RepositoryImportExportEncryption;
|
||||
import sonia.scm.importexport.RepositoryImportLoggerFactory;
|
||||
import sonia.scm.repository.CustomNamespaceStrategy;
|
||||
import sonia.scm.repository.HealthCheckService;
|
||||
import sonia.scm.repository.NamespaceAndName;
|
||||
import sonia.scm.repository.NamespaceStrategy;
|
||||
import sonia.scm.repository.Repository;
|
||||
@@ -157,6 +158,8 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
|
||||
private RepositoryImportLoggerFactory importLoggerFactory;
|
||||
@Mock
|
||||
private ExportService exportService;
|
||||
@Mock
|
||||
private HealthCheckService healthCheckService;
|
||||
|
||||
@Captor
|
||||
private ArgumentCaptor<Predicate<Repository>> filterCaptor;
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import sonia.scm.repository.HealthCheckService;
|
||||
import sonia.scm.repository.RepositoryManager;
|
||||
|
||||
import static com.google.inject.util.Providers.of;
|
||||
@@ -48,6 +49,7 @@ abstract class RepositoryTestBase {
|
||||
RepositoryImportResource repositoryImportResource;
|
||||
RepositoryExportResource repositoryExportResource;
|
||||
RepositoryPathsResource repositoryPathsResource;
|
||||
HealthCheckService healthCheckService;
|
||||
|
||||
RepositoryRootResource getRepositoryRootResource() {
|
||||
RepositoryBasedResourceProvider repositoryBasedResourceProvider = new RepositoryBasedResourceProvider(
|
||||
@@ -70,7 +72,9 @@ abstract class RepositoryTestBase {
|
||||
repositoryToDtoMapper,
|
||||
dtoToRepositoryMapper,
|
||||
manager,
|
||||
repositoryBasedResourceProvider)),
|
||||
of(repositoryCollectionResource), of(repositoryImportResource));
|
||||
repositoryBasedResourceProvider,
|
||||
healthCheckService)),
|
||||
of(repositoryCollectionResource),
|
||||
of(repositoryImportResource));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,9 +34,11 @@ import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import sonia.scm.SCMContextProvider;
|
||||
import sonia.scm.config.ScmConfiguration;
|
||||
import sonia.scm.repository.CustomNamespaceStrategy;
|
||||
import sonia.scm.repository.HealthCheckFailure;
|
||||
import sonia.scm.repository.HealthCheckService;
|
||||
import sonia.scm.repository.NamespaceStrategy;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.api.Command;
|
||||
@@ -57,6 +59,7 @@ import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.mockito.MockitoAnnotations.initMocks;
|
||||
import static sonia.scm.repository.HealthCheckFailure.templated;
|
||||
|
||||
@SubjectAware(
|
||||
username = "trillian",
|
||||
@@ -83,6 +86,10 @@ public class RepositoryToRepositoryDtoMapperTest {
|
||||
private ScmConfiguration configuration;
|
||||
@Mock
|
||||
private Set<NamespaceStrategy> strategies;
|
||||
@Mock
|
||||
private HealthCheckService healthCheckService;
|
||||
@Mock
|
||||
private SCMContextProvider scmContextProvider;
|
||||
|
||||
@InjectMocks
|
||||
private RepositoryToRepositoryDtoMapperImpl mapper;
|
||||
@@ -311,6 +318,62 @@ public class RepositoryToRepositoryDtoMapperTest {
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldCreateRunHealthCheckLink() {
|
||||
RepositoryDto dto = mapper.map(createTestRepository());
|
||||
assertEquals(
|
||||
"http://example.com/base/v2/repositories/testspace/test/runHealthCheck",
|
||||
dto.getLinks().getLinkBy("runHealthCheck").get().getHref());
|
||||
assertFalse(dto.isHealthCheckRunning());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldNotCreateHealthCheckLinkIfCheckIsRunning() {
|
||||
Repository testRepository = createTestRepository();
|
||||
when(healthCheckService.checkRunning(testRepository)).thenReturn(true);
|
||||
RepositoryDto dto = mapper.map(testRepository);
|
||||
assertFalse(dto.getLinks().getLinkBy("runHealthCheck").isPresent());
|
||||
assertTrue(dto.isHealthCheckRunning());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldCreateCorrectLinksForHealthChecks() {
|
||||
when(scmContextProvider.getDocumentationVersion()).thenReturn("2.17.x");
|
||||
|
||||
Repository testRepository = createTestRepository();
|
||||
HealthCheckFailure failure = new HealthCheckFailure("1", "vogons", templated("http://hog/{0}/vogons"), "met vogons");
|
||||
testRepository.setHealthCheckFailures(singletonList(failure));
|
||||
|
||||
RepositoryDto dto = mapper.map(testRepository);
|
||||
|
||||
assertThat(dto.getHealthCheckFailures())
|
||||
.extracting("url")
|
||||
.containsExactly("http://hog/2.17.x/vogons");
|
||||
|
||||
assertThat(dto.getHealthCheckFailures().get(0).getLinks().getLinkBy("documentation"))
|
||||
.get()
|
||||
.extracting("href")
|
||||
.isEqualTo("http://hog/2.17.x/vogons");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldCreateNoLinksForHealthChecksWithoutUrl() {
|
||||
when(scmContextProvider.getDocumentationVersion()).thenReturn("2.17.x");
|
||||
|
||||
Repository testRepository = createTestRepository();
|
||||
HealthCheckFailure failure = new HealthCheckFailure("1", "vogons", "met vogons");
|
||||
testRepository.setHealthCheckFailures(singletonList(failure));
|
||||
|
||||
RepositoryDto dto = mapper.map(testRepository);
|
||||
|
||||
assertThat(dto.getHealthCheckFailures())
|
||||
.extracting("url")
|
||||
.containsExactly(new Object[] {null});
|
||||
|
||||
assertThat(dto.getHealthCheckFailures().get(0).getLinks().getLinkBy("documentation"))
|
||||
.isNotPresent();
|
||||
}
|
||||
|
||||
private ScmProtocol mockProtocol(String type, String protocol) {
|
||||
return new MockScmProtocol(type, protocol);
|
||||
}
|
||||
|
||||
@@ -102,6 +102,8 @@ public class DefaultRepositoryManagerPerfTest {
|
||||
|
||||
@Mock
|
||||
private AuthorizationCollector authzCollector;
|
||||
@Mock
|
||||
private RepositoryPostProcessor repositoryPostProcessor;
|
||||
|
||||
/**
|
||||
* Setup object under test.
|
||||
@@ -116,8 +118,8 @@ public class DefaultRepositoryManagerPerfTest {
|
||||
keyGenerator,
|
||||
repositoryDAO,
|
||||
handlerSet,
|
||||
Providers.of(namespaceStrategy)
|
||||
);
|
||||
Providers.of(namespaceStrategy),
|
||||
repositoryPostProcessor);
|
||||
|
||||
setUpTestRepositories();
|
||||
|
||||
|
||||
@@ -106,6 +106,7 @@ import sonia.scm.TempSCMContextProvider;
|
||||
public class DefaultRepositoryManagerTest extends ManagerTestBase<Repository> {
|
||||
|
||||
private RepositoryDAO repositoryDAO;
|
||||
private RepositoryPostProcessor postProcessor = mock(RepositoryPostProcessor.class);
|
||||
|
||||
static {
|
||||
ThreadContext.unbindSubject();
|
||||
@@ -181,6 +182,7 @@ public class DefaultRepositoryManagerTest extends ManagerTestBase<Repository> {
|
||||
heartOfGold = manager.get(id);
|
||||
assertNotNull(heartOfGold);
|
||||
assertEquals(description, heartOfGold.getDescription());
|
||||
verify(postProcessor).postProcess(argThat(repository -> repository.getId().equals(id)));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -227,6 +229,8 @@ public class DefaultRepositoryManagerTest extends ManagerTestBase<Repository> {
|
||||
assertNotSame(heartOfGold, heartReference);
|
||||
heartReference.setDescription("prototype ship");
|
||||
assertNotEquals(heartOfGold.getDescription(), heartReference.getDescription());
|
||||
verify(postProcessor).postProcess(argThat(repository -> repository.getId().equals(heartOfGold.getId())));
|
||||
verify(postProcessor).postProcess(argThat(repository -> repository.getId().equals(happyVerticalPeopleTransporter.getId())));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -551,7 +555,7 @@ public class DefaultRepositoryManagerTest extends ManagerTestBase<Repository> {
|
||||
when(namespaceStrategy.createNamespace(Mockito.any(Repository.class))).thenAnswer(invocation -> mockedNamespace);
|
||||
|
||||
return new DefaultRepositoryManager(contextProvider,
|
||||
keyGenerator, repositoryDAO, handlerSet, Providers.of(namespaceStrategy));
|
||||
keyGenerator, repositoryDAO, handlerSet, Providers.of(namespaceStrategy), postProcessor);
|
||||
}
|
||||
|
||||
private RepositoryDAO createRepositoryDaoMock() {
|
||||
@@ -618,9 +622,6 @@ public class DefaultRepositoryManagerTest extends ManagerTestBase<Repository> {
|
||||
private Repository createRepository(Repository repository) {
|
||||
manager.create(repository);
|
||||
assertNotNull(repository.getId());
|
||||
assertNotNull(manager.get(repository.getId()));
|
||||
assertTrue(repository.getCreationDate() > 0);
|
||||
|
||||
return repository;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,278 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.repository;
|
||||
|
||||
import org.apache.shiro.authz.AuthorizationException;
|
||||
import org.apache.shiro.subject.Subject;
|
||||
import org.apache.shiro.util.ThreadContext;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import sonia.scm.NotFoundException;
|
||||
import sonia.scm.repository.api.Command;
|
||||
import sonia.scm.repository.api.FullHealthCheckCommandBuilder;
|
||||
import sonia.scm.repository.api.RepositoryService;
|
||||
import sonia.scm.repository.api.RepositoryServiceFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
|
||||
import static com.google.common.collect.ImmutableSet.of;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.awaitility.Awaitility.await;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.argThat;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class HealthCheckerTest {
|
||||
|
||||
private final Repository repository = RepositoryTestData.createHeartOfGold();
|
||||
private final String repositoryId = repository.getId();
|
||||
|
||||
@Mock
|
||||
private HealthCheck healthCheck1;
|
||||
@Mock
|
||||
private HealthCheck healthCheck2;
|
||||
|
||||
@Mock
|
||||
private RepositoryManager repositoryManager;
|
||||
@Mock
|
||||
private RepositoryServiceFactory repositoryServiceFactory;
|
||||
@Mock
|
||||
private RepositoryService repositoryService;
|
||||
@Mock
|
||||
private RepositoryPostProcessor postProcessor;
|
||||
|
||||
@Mock
|
||||
private Subject subject;
|
||||
|
||||
private HealthChecker checker;
|
||||
|
||||
@BeforeEach
|
||||
void initializeChecker() {
|
||||
this.checker = new HealthChecker(of(healthCheck1, healthCheck2), repositoryManager, repositoryServiceFactory, postProcessor);
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void initSubject() {
|
||||
ThreadContext.bind(subject);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void cleanupSubject() {
|
||||
ThreadContext.unbindSubject();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFailForNotExistingRepositoryId() {
|
||||
assertThrows(NotFoundException.class, () -> checker.lightCheck("no-such-id"));
|
||||
}
|
||||
|
||||
@Nested
|
||||
class WithRepository {
|
||||
@BeforeEach
|
||||
void setUpRepository() {
|
||||
doReturn(repository).when(repositoryManager).get(repositoryId);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldComputeLightChecks() {
|
||||
when(healthCheck1.check(repository)).thenReturn(HealthCheckResult.unhealthy(createFailure("error1")));
|
||||
when(healthCheck2.check(repository)).thenReturn(HealthCheckResult.unhealthy(createFailure("error2")));
|
||||
|
||||
checker.lightCheck(repositoryId);
|
||||
|
||||
verify(postProcessor).setCheckResults(eq(repository), argThat(failures -> {
|
||||
assertThat(failures)
|
||||
.hasSize(2)
|
||||
.extracting("id").containsExactly("error1", "error2");
|
||||
return true;
|
||||
}));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldLockWhileLightCheckIsRunning() throws InterruptedException {
|
||||
CountDownLatch waitUntilSecondCheckHasRun = new CountDownLatch(1);
|
||||
CountDownLatch waitForFirstCheckStarted = new CountDownLatch(1);
|
||||
|
||||
when(healthCheck1.check(repository)).thenReturn(HealthCheckResult.healthy());
|
||||
when(healthCheck2.check(repository)).thenAnswer(invocation -> {
|
||||
waitForFirstCheckStarted.countDown();
|
||||
waitUntilSecondCheckHasRun.await();
|
||||
return HealthCheckResult.healthy();
|
||||
});
|
||||
|
||||
new Thread(() -> checker.lightCheck(repositoryId)).start();
|
||||
|
||||
waitForFirstCheckStarted.await();
|
||||
await().until(() -> {
|
||||
checker.lightCheck(repositoryId);
|
||||
return true;
|
||||
});
|
||||
|
||||
waitUntilSecondCheckHasRun.countDown();
|
||||
|
||||
verify(healthCheck1).check(repository);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldShowRunningCheck() throws InterruptedException {
|
||||
CountDownLatch waitUntilVerification = new CountDownLatch(1);
|
||||
CountDownLatch waitForFirstCheckStarted = new CountDownLatch(1);
|
||||
|
||||
assertThat(checker.checkRunning(repositoryId)).isFalse();
|
||||
|
||||
when(healthCheck1.check(repository)).thenReturn(HealthCheckResult.healthy());
|
||||
when(healthCheck2.check(repository)).thenAnswer(invocation -> {
|
||||
waitForFirstCheckStarted.countDown();
|
||||
waitUntilVerification.await();
|
||||
return HealthCheckResult.healthy();
|
||||
});
|
||||
|
||||
new Thread(() -> checker.lightCheck(repositoryId)).start();
|
||||
|
||||
waitForFirstCheckStarted.await();
|
||||
|
||||
assertThat(checker.checkRunning(repositoryId)).isTrue();
|
||||
|
||||
waitUntilVerification.countDown();
|
||||
|
||||
await().until(() -> !checker.checkRunning(repositoryId));
|
||||
}
|
||||
|
||||
@Nested
|
||||
class ForFullChecks {
|
||||
|
||||
@Mock
|
||||
private FullHealthCheckCommandBuilder fullHealthCheckCommand;
|
||||
|
||||
@BeforeEach
|
||||
void setUpRepository() {
|
||||
when(repositoryServiceFactory.create(repository)).thenReturn(repositoryService);
|
||||
lenient().when(repositoryService.getFullCheckCommand()).thenReturn(fullHealthCheckCommand);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldComputeLightChecksForFullChecks() {
|
||||
when(healthCheck1.check(repository)).thenReturn(HealthCheckResult.healthy());
|
||||
when(healthCheck2.check(repository)).thenReturn(HealthCheckResult.unhealthy(createFailure("error")));
|
||||
|
||||
checker.fullCheck(repositoryId);
|
||||
|
||||
verify(postProcessor).setCheckResults(eq(repository), argThat(failures -> {
|
||||
assertThat(failures)
|
||||
.hasSize(1)
|
||||
.extracting("id").containsExactly("error");
|
||||
return true;
|
||||
}));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldLockWhileFullCheckIsRunning() throws InterruptedException {
|
||||
CountDownLatch waitUntilSecondCheckHasRun = new CountDownLatch(1);
|
||||
CountDownLatch waitForFirstCheckStarted = new CountDownLatch(1);
|
||||
|
||||
when(healthCheck1.check(repository)).thenReturn(HealthCheckResult.healthy());
|
||||
when(healthCheck2.check(repository)).thenAnswer(invocation -> {
|
||||
waitForFirstCheckStarted.countDown();
|
||||
waitUntilSecondCheckHasRun.await();
|
||||
return HealthCheckResult.healthy();
|
||||
});
|
||||
|
||||
new Thread(() -> checker.fullCheck(repositoryId)).start();
|
||||
|
||||
waitForFirstCheckStarted.await();
|
||||
await().until(() -> {
|
||||
checker.fullCheck(repositoryId);
|
||||
return true;
|
||||
});
|
||||
|
||||
waitUntilSecondCheckHasRun.countDown();
|
||||
|
||||
verify(healthCheck1).check(repository);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldComputeFullChecks() throws IOException {
|
||||
when(healthCheck1.check(repository)).thenReturn(HealthCheckResult.healthy());
|
||||
when(healthCheck2.check(repository)).thenReturn(HealthCheckResult.healthy());
|
||||
when(repositoryService.isSupported(Command.FULL_HEALTH_CHECK)).thenReturn(true);
|
||||
when(fullHealthCheckCommand.check()).thenReturn(HealthCheckResult.unhealthy(createFailure("error")));
|
||||
|
||||
checker.fullCheck(repositoryId);
|
||||
|
||||
verify(postProcessor).setCheckResults(eq(repository), argThat(failures -> {
|
||||
assertThat(failures)
|
||||
.hasSize(1)
|
||||
.extracting("id").containsExactly("error");
|
||||
return true;
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class WithoutPermission {
|
||||
|
||||
@BeforeEach
|
||||
void setMissingPermission() {
|
||||
doThrow(AuthorizationException.class).when(subject).checkPermission("repository:healthCheck:" + repositoryId);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFailToRunLightChecksWithoutPermissionForId() {
|
||||
assertThrows(AuthorizationException.class, () -> checker.lightCheck(repositoryId));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFailToRunLightChecksWithoutPermissionForRepository() {
|
||||
assertThrows(AuthorizationException.class, () -> checker.lightCheck(repository));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFailToRunFullChecksWithoutPermissionForId() {
|
||||
assertThrows(AuthorizationException.class, () -> checker.fullCheck(repositoryId));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFailToRunFullChecksWithoutPermissionForRepository() {
|
||||
assertThrows(AuthorizationException.class, () -> checker.fullCheck(repository));
|
||||
}
|
||||
}
|
||||
|
||||
private HealthCheckFailure createFailure(String text) {
|
||||
return new HealthCheckFailure(text, text, text);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.repository;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import sonia.scm.event.ScmEventBus;
|
||||
|
||||
import static java.util.Collections.singleton;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.argThat;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class RepositoryPostProcessorTest {
|
||||
|
||||
@Mock
|
||||
private ScmEventBus eventBus;
|
||||
|
||||
@InjectMocks
|
||||
RepositoryPostProcessor repositoryPostProcessor;
|
||||
|
||||
@Test
|
||||
void shouldSetHealthChecksForRepository() {
|
||||
Repository repository = RepositoryTestData.createHeartOfGold();
|
||||
repositoryPostProcessor.setCheckResults(repository.clone(), singleton(new HealthCheckFailure("HOG", "improbable", "This is not very probable")));
|
||||
|
||||
repositoryPostProcessor.postProcess(repository);
|
||||
|
||||
assertThat(repository.getHealthCheckFailures())
|
||||
.extracting("id")
|
||||
.containsExactly("HOG");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSetEmptyListOfHealthChecksWhenNoResultsExist() {
|
||||
Repository repository = RepositoryTestData.createHeartOfGold();
|
||||
|
||||
repositoryPostProcessor.postProcess(repository);
|
||||
|
||||
assertThat(repository.getHealthCheckFailures())
|
||||
.isNotNull()
|
||||
.isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSetHealthChecksForRepositoryInSetter() {
|
||||
Repository repository = RepositoryTestData.createHeartOfGold();
|
||||
repositoryPostProcessor.setCheckResults(repository, singleton(new HealthCheckFailure("HOG", "improbable", "This is not very probable")));
|
||||
|
||||
assertThat(repository.getHealthCheckFailures())
|
||||
.extracting("id")
|
||||
.containsExactly("HOG");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldTriggerHealthCheckEventForNewFailure() {
|
||||
Repository repository = RepositoryTestData.createHeartOfGold();
|
||||
repositoryPostProcessor.setCheckResults(repository, singleton(new HealthCheckFailure("HOG", "improbable", "This is not very probable")));
|
||||
|
||||
verify(eventBus).post(argThat(event -> {
|
||||
HealthCheckEvent healthCheckEvent = (HealthCheckEvent) event;
|
||||
assertThat(healthCheckEvent.getRepository())
|
||||
.isEqualTo(repository);
|
||||
assertThat(healthCheckEvent.getPreviousFailures())
|
||||
.isEmpty();
|
||||
assertThat(((HealthCheckEvent) event).getCurrentFailures())
|
||||
.extracting("id")
|
||||
.containsExactly("HOG");
|
||||
return true;
|
||||
}));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldTriggerHealthCheckEventForDifferentFailure() {
|
||||
Repository repository = RepositoryTestData.createHeartOfGold();
|
||||
repositoryPostProcessor.setCheckResults(repository, singleton(new HealthCheckFailure("HOG", "improbable", "This is not very probable")));
|
||||
repositoryPostProcessor.setCheckResults(repository, singleton(new HealthCheckFailure("VOG", "vogons", "Erased by Vogons")));
|
||||
|
||||
verify(eventBus).post(argThat(event -> {
|
||||
HealthCheckEvent healthCheckEvent = (HealthCheckEvent) event;
|
||||
if (healthCheckEvent.getPreviousFailures().isEmpty()) {
|
||||
return false; // ignore event from first checks
|
||||
}
|
||||
assertThat((healthCheckEvent).getRepository())
|
||||
.isEqualTo(repository);
|
||||
assertThat((healthCheckEvent).getPreviousFailures())
|
||||
.extracting("id")
|
||||
.containsExactly("HOG");
|
||||
assertThat((healthCheckEvent).getCurrentFailures())
|
||||
.extracting("id")
|
||||
.containsExactly("VOG");
|
||||
return true;
|
||||
}));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user