CLI Support for repository actions (#1987)

To make SCM-Manager more accessible and to make it easier using scripts against the server, we created a command line interface. This command line interface can be used to perform the default actions like create, modify and delete repositories. It is also very flexible and can be extended by plugins.

The CLI already supports internationalization, help texts, input validation, loose and table-like templates and nested subcommands. Check the cli guidelines to learn how add new cli commands.

Co-authored-by: Sebastian Sdorra <sebastian.sdorra@cloudogu.com>
This commit is contained in:
Eduard Heimbuch
2022-04-04 12:02:16 +02:00
committed by GitHub
parent 07afe4b439
commit 162dd6ad0a
90 changed files with 5303 additions and 21 deletions

View File

@@ -0,0 +1,95 @@
/*
* 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.api.v2.resources;
import lombok.Data;
import org.apache.shiro.SecurityUtils;
import sonia.scm.cli.CliProcessor;
import sonia.scm.cli.JsonStreamingCliContext;
import sonia.scm.security.ApiKeyService;
import javax.inject.Inject;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.StreamingOutput;
import java.util.List;
@Path("v2/cli")
public class CliResource {
private final CliProcessor processor;
private final ApiKeyService service;
@Inject
public CliResource(CliProcessor processor, ApiKeyService service) {
this.processor = processor;
this.service = service;
}
@POST
@Path("exec")
public StreamingOutput exec(@QueryParam("args") List<String> args, @Context HttpServletRequest request) {
return outputStream -> {
try (JsonStreamingCliContext context = new JsonStreamingCliContext(request.getLocale(), request.getInputStream(), outputStream)) {
int exitCode = processor.execute(context, args.toArray(new String[0]));
context.writeExit(exitCode);
}
};
}
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Path("login")
public Response login(CliAuthenticationDto auth) {
String username = SecurityUtils.getSubject().getPrincipal().toString();
ApiKeyService.CreationResult newKey = service.createNewKey(username, auth.getApiKey(), "*");
return Response.ok(newKey.getToken()).build();
}
@DELETE
@Path("logout/{apiKey}")
public Response logout(@PathParam("apiKey") String apiKeyName) {
String username = SecurityUtils.getSubject().getPrincipal().toString();
service.getKeys(username)
.stream()
.filter(apiKey -> apiKey.getDisplayName().equals(apiKeyName))
.findFirst()
.ifPresent(apiKey -> service.remove(username, apiKey.getId()));
return Response.noContent().build();
}
@Data
static class CliAuthenticationDto {
private String apiKey;
}
}

View File

@@ -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.cli;
import picocli.CommandLine;
import sonia.scm.ExceptionWithContext;
import java.io.PrintWriter;
public class CliExceptionHandler implements CommandLine.IExecutionExceptionHandler {
@Override
public int handleExecutionException(Exception ex, CommandLine commandLine, CommandLine.ParseResult parseResult) {
if (ex instanceof CliExitException) {
return ((CliExitException) ex).getExitCode();
}
PrintWriter stdErr = commandLine.getErr();
stdErr.print("Execution error");
if (ex instanceof ExceptionWithContext) {
String code = ((ExceptionWithContext) ex).getCode();
stdErr.print(" " + code);
}
stdErr.print(": ");
stdErr.println(ex.getMessage());
stdErr.println();
commandLine.usage(stdErr);
return ExitCode.SERVER_ERROR;
}
}

View File

@@ -0,0 +1,37 @@
/*
* 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.cli;
import lombok.Getter;
@Getter
public class CliExitException extends RuntimeException {
private final int exitCode;
public CliExitException(int exitCode) {
this.exitCode = exitCode;
}
}

View File

@@ -0,0 +1,81 @@
/*
* 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.cli;
import com.google.inject.Injector;
import picocli.AutoComplete;
import picocli.CommandLine;
import javax.inject.Inject;
import java.util.ResourceBundle;
public class CliProcessor {
private final CommandRegistry registry;
private final Injector injector;
private final CommandLine.Model.CommandSpec usageHelp;
@Inject
public CliProcessor(CommandRegistry registry, Injector injector) {
this.registry = registry;
this.injector = injector;
this.usageHelp = new CommandLine(HelpMixin.class).getCommandSpec();
}
public int execute(CliContext context, String... args) {
CommandFactory factory = new CommandFactory(injector, context);
CommandLine cli = new CommandLine(ScmManagerCommand.class, factory);
cli.getCommandSpec().addMixin("help", usageHelp);
cli.setResourceBundle(ResourceBundle.getBundle("sonia.scm.cli.i18n", context.getLocale()));
for (RegisteredCommandNode c : registry.createCommandTree()) {
CommandLine commandline = createCommandline(context, factory, c);
cli.getCommandSpec().addSubcommand(c.getName(), commandline);
}
cli.addSubcommand(AutoComplete.GenerateCompletion.class);
cli.setErr(context.getStderr());
cli.setOut(context.getStdout());
cli.setExecutionExceptionHandler(new CliExceptionHandler());
return cli.execute(args);
}
private CommandLine createCommandline(CliContext context, CommandFactory factory, RegisteredCommandNode command) {
CommandLine commandLine = new CommandLine(command.getCommand(), factory);
commandLine.getCommandSpec().addMixin("help", usageHelp);
ResourceBundle resourceBundle = commandLine.getCommandSpec().resourceBundle();
if (resourceBundle != null) {
String resourceBundleBaseName = resourceBundle.getBaseBundleName();
commandLine.setResourceBundle(ResourceBundle.getBundle(resourceBundleBaseName, context.getLocale()));
}
for (RegisteredCommandNode child : command.getChildren()) {
if (!commandLine.getCommandSpec().subcommands().containsKey(child.getName())) {
CommandLine childCommandLine = createCommandline(context, factory, child);
commandLine.getCommandSpec().addSubcommand(child.getName(), childCommandLine);
}
}
return commandLine;
}
}

View File

@@ -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.cli;
import com.google.inject.AbstractModule;
import com.google.inject.Injector;
import picocli.CommandLine;
public class CommandFactory implements CommandLine.IFactory {
private final Injector injector;
public CommandFactory(Injector injector, CliContext context) {
this.injector = injector.createChildInjector(new CliContextModule(context));
}
@Override
public <K> K create(Class<K> cls) throws Exception {
return injector.getInstance(cls);
}
static class CliContextModule extends AbstractModule {
private final CliContext context;
private CliContextModule(CliContext context) {
this.context = context;
}
@Override
protected void configure() {
bind(CliContext.class).toInstance(context);
}
}
}

View 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.
*/
package sonia.scm.cli;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
@Singleton
public class CommandRegistry {
private final RegisteredCommandCollector commandCollector;
@Inject
public CommandRegistry(RegisteredCommandCollector commandCollector) {
this.commandCollector = commandCollector;
}
public Set<RegisteredCommandNode> createCommandTree() {
Set<RegisteredCommandNode> rootCommands = new HashSet<>();
Set<RegisteredCommand> registeredCommands = commandCollector.collect();
Map<Class<?>, RegisteredCommandNode> commandNodes = new HashMap<>();
for (RegisteredCommand command : registeredCommands) {
commandNodes.put(command.getCommand(), new RegisteredCommandNode(command.getName(), command.getCommand()));
}
for (RegisteredCommand command : registeredCommands) {
RegisteredCommandNode node = commandNodes.get(command.getCommand());
if (command.getParent() == null) {
rootCommands.add(node);
} else {
RegisteredCommandNode parentNode = commandNodes.get(command.getParent());
if (parentNode != null) {
parentNode.getChildren().add(node);
} else {
throw new NonExistingParentCommandException("parent command of " + command.getName() + " does not exist");
}
}
}
return rootCommands;
}
}

View File

@@ -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.cli;
import picocli.CommandLine;
public class HelpMixin {
@CommandLine.Option(names = {"--help", "-h"}, usageHelp = true, descriptionKey = "scm.help.usage.description.0")
private boolean usageHelp;
}

View File

@@ -0,0 +1,158 @@
/*
* 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.cli;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.util.MinimalPrettyPrinter;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.Writer;
import java.util.Arrays;
import java.util.Locale;
public class JsonStreamingCliContext implements CliContext, AutoCloseable {
private static final Logger LOG = LoggerFactory.getLogger(JsonStreamingCliContext.class);
private final Locale locale;
private final InputStream stdin;
private final PrintWriter stdout;
private final PrintWriter stderr;
private final JsonGenerator jsonGenerator;
@SuppressWarnings("java:S2095") // generator is closed in the close method
public JsonStreamingCliContext(Locale locale, InputStream stdin, OutputStream output) throws IOException {
this.locale = locale;
this.stdin = stdin;
this.jsonGenerator = mapper.createGenerator(output).setPrettyPrinter(new MinimalPrettyPrinter(""));
jsonGenerator.writeStartArray();
StringingOutputWriter out = new StringingOutputWriter(jsonGenerator, "out");
StringingOutputWriter err = new StringingOutputWriter(jsonGenerator, "err");
out.setOther(err);
err.setOther(out);
this.stdout = new PrintWriter(out);
this.stderr = new PrintWriter(err);
}
@Override
public PrintWriter getStdout() {
return stdout;
}
@Override
public PrintWriter getStderr() {
return stderr;
}
@Override
public InputStream getStdin() {
return stdin;
}
public void writeExit(int exitcode) throws IOException {
stdout.flush();
stderr.flush();
jsonGenerator.writeStartObject();
jsonGenerator.writeNumberField("exit", exitcode);
jsonGenerator.writeEndObject();
}
@Override
public void exit(int exitCode) {
throw new CliExitException(exitCode);
}
@Override
public Locale getLocale() {
return locale;
}
private static final ObjectMapper mapper = new ObjectMapper();
@Override
public void close() {
try {
stdout.close();
stderr.close();
jsonGenerator.writeEndArray();
jsonGenerator.close();
} catch (IOException e) {
LOG.error("Could not close cli output streams", e);
}
try {
stdin.close();
} catch (IOException e) {
LOG.error("Could not close cli input stream", e);
}
}
public static class StringingOutputWriter extends Writer {
private final JsonGenerator jsonGenerator;
private final String name;
private StringBuilder buffer = new StringBuilder();
private StringingOutputWriter other;
private StringingOutputWriter(JsonGenerator jsonGenerator, String name) {
this.jsonGenerator = jsonGenerator;
this.name = name;
}
public void setOther(StringingOutputWriter other) {
this.other = other;
}
@Override
public void write(char[] cbuf, int off, int len) throws IOException {
other.flush();
buffer.append(Arrays.copyOfRange(cbuf, off, len));
}
@Override
public void flush() throws IOException {
String content = buffer.toString();
if (content.length() > 0) {
buffer = new StringBuilder();
jsonGenerator.writeStartObject();
jsonGenerator.writeStringField(name, content);
jsonGenerator.writeEndObject();
}
}
@Override
public void close() throws IOException {
this.flush();
}
}
}

View File

@@ -0,0 +1,30 @@
/*
* 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.cli;
import picocli.CommandLine;
@CommandLine.Command(name = "logout")
public class LogoutCommand {}

View File

@@ -0,0 +1,35 @@
/*
* 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.cli;
/**
* Exception is thrown if a command is registered with parent which does not exist.
* @since 2.33.0
*/
public class NonExistingParentCommandException extends CliException {
public NonExistingParentCommandException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,45 @@
/*
* 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.cli;
import picocli.CommandLine;
import javax.inject.Inject;
@CommandLine.Command(name = "ping", hidden = true)
public class PingCommand implements Runnable {
private final CliContext context;
@Inject
public PingCommand(CliContext context) {
this.context = context;
}
@Override
public void run() {
context.getStdout().println("PONG");
}
}

View File

@@ -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.cli;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
class RegisteredCommand {
private String name;
private Class<?> command;
private Class<?> parent;
}

View File

@@ -0,0 +1,84 @@
/*
* 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.cli;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.plugin.InstalledPlugin;
import sonia.scm.plugin.PluginLoader;
import sonia.scm.plugin.ScmModule;
import javax.inject.Inject;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.stream.Collectors;
public class RegisteredCommandCollector {
private static final Logger LOG = LoggerFactory.getLogger(RegisteredCommandCollector.class);
private final PluginLoader pluginLoader;
@Inject
public RegisteredCommandCollector(PluginLoader pluginLoader) {
this.pluginLoader = pluginLoader;
}
public Set<RegisteredCommand> collect() {
Set<RegisteredCommand> cmds = new HashSet<>();
findCommands(pluginLoader.getUberClassLoader(), cmds, pluginLoader.getInstalledModules());
findCommands(pluginLoader.getUberClassLoader(), cmds, pluginLoader.getInstalledPlugins().stream().map(InstalledPlugin::getDescriptor).collect(Collectors.toList()));
return Collections.unmodifiableSet(cmds);
}
private void findCommands(ClassLoader classLoader, Set<RegisteredCommand> commands, Iterable<? extends ScmModule> modules) {
modules.forEach(m -> m.getCliCommands().forEach(c -> {
Class<?> command = createCommand(classLoader, c.getClazz());
if (command != null && command != ScmManagerCommand.class) {
commands.add(new RegisteredCommand(c.getName(), command, getParent(command)));
}
}));
}
private Class<?> getParent(Class<?> command) {
ParentCommand parentAnnotation = command.getAnnotation(ParentCommand.class);
if (parentAnnotation != null) {
return parentAnnotation.value();
}
return null;
}
private Class<?> createCommand(ClassLoader classLoader, String clazz) {
try {
return classLoader.loadClass(clazz);
} catch (ClassNotFoundException e) {
LOG.error("Could not find command class: {}", clazz, e);
return null;
}
}
}

View File

@@ -0,0 +1,42 @@
/*
* 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.cli;
import lombok.Getter;
import java.util.ArrayList;
import java.util.List;
@Getter
public class RegisteredCommandNode {
private final String name;
private final Class<?> command;
private final List<RegisteredCommandNode> children = new ArrayList<>();
public RegisteredCommandNode(String name, Class<?> command) {
this.name = name;
this.command = command;
}
}

View File

@@ -0,0 +1,30 @@
/*
* 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.cli;
import picocli.CommandLine;
@CommandLine.Command(name = "scm")
public class ScmManagerCommand {}

View File

@@ -37,6 +37,7 @@ import sonia.scm.filter.WebElementModule;
import sonia.scm.plugin.ExtensionProcessor;
import sonia.scm.plugin.PluginLoader;
import sonia.scm.repository.ExecutorModule;
import sonia.scm.validation.ValidationModule;
import javax.servlet.ServletContext;
import java.util.ArrayList;
@@ -62,6 +63,7 @@ public class ApplicationModuleProvider implements ModuleProvider {
private List<Module> createModules(ClassOverrides overrides) {
List<Module> moduleList = new ArrayList<>();
moduleList.add(new ValidationModule());
moduleList.add(new ResteasyModule());
moduleList.add(ShiroWebModule.guiceFilterModule());
moduleList.add(new WebElementModule(pluginLoader));

View File

@@ -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.cli;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
public class RepositoryCommandDto {
private String name;
private String namespace;
private String type;
private String contact;
private String description;
private String creationDate;
private String lastModified;
private String url;
}

View File

@@ -0,0 +1,116 @@
/*
* 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.cli;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableMap;
import picocli.CommandLine;
import sonia.scm.cli.CommandValidator;
import sonia.scm.cli.ParentCommand;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryInitializer;
import sonia.scm.repository.RepositoryManager;
import sonia.scm.repository.RepositoryName;
import sonia.scm.repository.RepositoryTypeConstraint;
import javax.inject.Inject;
import javax.validation.constraints.Email;
@CommandLine.Command(name = "create")
@ParentCommand(value = RepositoryCommand.class)
public class RepositoryCreateCommand implements Runnable {
@CommandLine.Mixin
private final RepositoryTemplateRenderer templateRenderer;
@CommandLine.Mixin
private final CommandValidator validator;
private final RepositoryManager manager;
private final RepositoryInitializer repositoryInitializer;
@RepositoryTypeConstraint
@CommandLine.Parameters(descriptionKey = "scm.repo.create.type")
private String type;
@RepositoryName(namespace = RepositoryName.Namespace.OPTIONAL)
@CommandLine.Parameters(descriptionKey = "scm.repo.create.repository", paramLabel = "name")
private String repository;
@CommandLine.Option(names = {"--description", "-d"}, descriptionKey = "scm.repo.create.desc")
private String description;
@Email
@CommandLine.Option(names = {"--contact", "-c"})
private String contact;
@CommandLine.Option(names = {"--init", "-i"}, descriptionKey = "scm.repo.create.init")
private boolean init;
@Inject
public RepositoryCreateCommand(RepositoryTemplateRenderer templateRenderer,
CommandValidator validator,
RepositoryManager manager,
RepositoryInitializer repositoryInitializer) {
this.templateRenderer = templateRenderer;
this.validator = validator;
this.manager = manager;
this.repositoryInitializer = repositoryInitializer;
}
@Override
public void run() {
validator.validate();
Repository newRepo = new Repository();
String[] splitRepoName = repository.split("/");
if (splitRepoName.length == 2) {
newRepo.setNamespace(splitRepoName[0]);
newRepo.setName(splitRepoName[1]);
} else {
newRepo.setName(repository);
}
newRepo.setType(type);
newRepo.setDescription(description);
newRepo.setContact(contact);
Repository createdRepo = manager.create(newRepo);
if (init) {
repositoryInitializer.initialize(createdRepo, ImmutableMap.of());
}
templateRenderer.render(createdRepo);
}
@VisibleForTesting
void setType(String type) {
this.type = type;
}
@VisibleForTesting
void setRepository(String repository) {
this.repository = repository;
}
@VisibleForTesting
void setInit(boolean init) {
this.init = init;
}
}

View File

@@ -0,0 +1,85 @@
/*
* 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.cli;
import com.google.common.annotations.VisibleForTesting;
import picocli.CommandLine;
import sonia.scm.cli.ParentCommand;
import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryManager;
import javax.inject.Inject;
import java.util.Collections;
@CommandLine.Command(name = "delete", aliases = "rm")
@ParentCommand(RepositoryCommand.class)
public class RepositoryDeleteCommand implements Runnable {
private static final String PROMPT_TEMPLATE = "{{i18n.repoDeletePrompt}}";
@CommandLine.Parameters(descriptionKey = "scm.repo.delete.repository", paramLabel = "namespace/name")
private String repository;
@CommandLine.Option(names = {"--yes", "-y"}, descriptionKey = "scm.repo.delete.prompt")
private boolean shouldDelete;
@CommandLine.Mixin
private final RepositoryTemplateRenderer templateRenderer;
private final RepositoryManager manager;
@Inject
public RepositoryDeleteCommand(RepositoryManager manager, RepositoryTemplateRenderer templateRenderer) {
this.manager = manager;
this.templateRenderer = templateRenderer;
}
@Override
public void run() {
if (!shouldDelete) {
templateRenderer.renderToStderr(PROMPT_TEMPLATE, Collections.emptyMap());
return;
}
String[] splitRepo = repository.split("/");
if (splitRepo.length == 2) {
Repository repo = manager.get(new NamespaceAndName(splitRepo[0], splitRepo[1]));
if (repo != null) {
manager.delete(repo);
}
} else {
templateRenderer.renderInvalidInputError();
}
}
@VisibleForTesting
void setRepository(String repository) {
this.repository = repository;
}
@VisibleForTesting
void setShouldDelete(boolean shouldDelete) {
this.shouldDelete = shouldDelete;
}
}

View 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.
*/
package sonia.scm.repository.cli;
import com.cronutils.utils.VisibleForTesting;
import picocli.CommandLine;
import sonia.scm.cli.ParentCommand;
import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryManager;
import javax.inject.Inject;
@ParentCommand(value = RepositoryCommand.class)
@CommandLine.Command(name = "get")
public class RepositoryGetCommand implements Runnable {
@CommandLine.Parameters(paramLabel = "namespace/name", index = "0")
private String repository;
@CommandLine.Mixin
private final RepositoryTemplateRenderer templateRenderer;
private final RepositoryManager manager;
@Inject
RepositoryGetCommand(RepositoryTemplateRenderer templateRenderer, RepositoryManager manager) {
this.templateRenderer = templateRenderer;
this.manager = manager;
}
@VisibleForTesting
public void setRepository(String repository) {
this.repository = repository;
}
@Override
public void run() {
String[] splitRepo = repository.split("/");
if (splitRepo.length == 2) {
Repository repo = manager.get(new NamespaceAndName(splitRepo[0], splitRepo[1]));
if (repo != null) {
templateRenderer.render(repo);
} else {
templateRenderer.renderNotFoundError();
}
} else {
templateRenderer.renderInvalidInputError();
}
}
}

View File

@@ -0,0 +1,89 @@
/*
* 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.cli;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableMap;
import picocli.CommandLine;
import sonia.scm.cli.ParentCommand;
import sonia.scm.cli.Table;
import sonia.scm.cli.TemplateRenderer;
import sonia.scm.repository.RepositoryManager;
import javax.inject.Inject;
import java.util.Collection;
import java.util.stream.Collectors;
@ParentCommand(value = RepositoryCommand.class)
@CommandLine.Command(name = "list", aliases = "ls")
public class RepositoryListCommand implements Runnable {
@CommandLine.Mixin
private final TemplateRenderer templateRenderer;
private final RepositoryManager manager;
private final RepositoryToRepositoryCommandDtoMapper mapper;
@CommandLine.Option(names = {"--short", "-s"})
private boolean useShortTemplate;
private static final String TABLE_TEMPLATE = String.join("\n",
"{{#rows}}",
"{{#cols}}{{#row.first}}{{#upper}}{{value}}{{/upper}}{{/row.first}}{{^row.first}}{{value}}{{/row.first}}{{^last}} {{/last}}{{/cols}}",
"{{/rows}}"
);
private static final String SHORT_TEMPLATE = String.join("\n",
"{{#repos}}",
"{{namespace}}/{{name}}",
"{{/repos}}"
);
@Inject
public RepositoryListCommand(RepositoryManager manager, TemplateRenderer templateRenderer, RepositoryToRepositoryCommandDtoMapper mapper) {
this.manager = manager;
this.templateRenderer = templateRenderer;
this.mapper = mapper;
}
@Override
public void run() {
Collection<RepositoryCommandDto> dtos = manager.getAll().stream().map(mapper::map).collect(Collectors.toList());
if (useShortTemplate) {
templateRenderer.renderToStdout(SHORT_TEMPLATE, ImmutableMap.of("repos", dtos));
} else {
Table table = templateRenderer.createTable();
table.addHeader("repoName", "repoType", "repoUrl");
for (RepositoryCommandDto dto : dtos) {
table.addRow(dto.getNamespace() + "/" + dto.getName(), dto.getType(), dto.getUrl());
}
templateRenderer.renderToStdout(TABLE_TEMPLATE, ImmutableMap.of("rows", table, "repos", dtos));
}
}
@VisibleForTesting
void setShortTemplate(boolean useShortTemplate) {
this.useShortTemplate = useShortTemplate;
}
}

View File

@@ -0,0 +1,95 @@
/*
* 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.cli;
import com.google.common.annotations.VisibleForTesting;
import picocli.CommandLine;
import sonia.scm.cli.CommandValidator;
import sonia.scm.cli.ParentCommand;
import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryManager;
import javax.inject.Inject;
import javax.validation.constraints.Email;
@ParentCommand(value = RepositoryCommand.class)
@CommandLine.Command(name = "modify")
public class RepositoryModifyCommand implements Runnable {
@CommandLine.Mixin
private final RepositoryTemplateRenderer templateRenderer;
@CommandLine.Mixin
private final CommandValidator validator;
private final RepositoryManager manager;
@CommandLine.Parameters(paramLabel = "namespace/name", index = "0", descriptionKey = "scm.repo.modify.repository")
private String repository;
@CommandLine.Option(names = {"--description", "-d"}, descriptionKey = "scm.repo.create.desc")
private String description;
@Email
@CommandLine.Option(names = {"--contact", "-c"})
private String contact;
@Inject
RepositoryModifyCommand(RepositoryTemplateRenderer templateRenderer, CommandValidator validator, RepositoryManager manager) {
this.templateRenderer = templateRenderer;
this.validator = validator;
this.manager = manager;
}
@VisibleForTesting
public void setRepository(String repository) {
this.repository = repository;
}
@Override
public void run() {
validator.validate();
String[] splitRepo = repository.split("/");
if (splitRepo.length == 2) {
Repository repo = manager.get(new NamespaceAndName(splitRepo[0], splitRepo[1]));
if (repo != null) {
if (contact != null) {
repo.setContact(contact);
}
if (description != null) {
repo.setDescription(description);
}
manager.modify(repo);
templateRenderer.render(repo);
} else {
templateRenderer.renderNotFoundError();
}
} else {
templateRenderer.renderInvalidInputError();
}
}
}

View File

@@ -0,0 +1,86 @@
/*
* 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.cli;
import com.google.common.collect.ImmutableMap;
import sonia.scm.cli.CliContext;
import sonia.scm.cli.ExitCode;
import sonia.scm.cli.Table;
import sonia.scm.cli.TemplateRenderer;
import sonia.scm.repository.Repository;
import sonia.scm.template.TemplateEngineFactory;
import javax.inject.Inject;
import java.util.Collections;
public class RepositoryTemplateRenderer extends TemplateRenderer {
private static final String DETAILS_TABLE_TEMPLATE = String.join("\n",
"{{#rows}}",
"{{#cols}}{{value}}{{/cols}}",
"{{/rows}}"
);
private static final String INVALID_INPUT_TEMPLATE = "{{i18n.repoInvalidInput}}";
private static final String NOT_FOUND_TEMPLATE = "{{i18n.repoNotFound}}";
private final CliContext context;
private final RepositoryToRepositoryCommandDtoMapper mapper;
@Inject
RepositoryTemplateRenderer(CliContext context, TemplateEngineFactory templateEngineFactory, RepositoryToRepositoryCommandDtoMapper mapper) {
super(context, templateEngineFactory);
this.context = context;
this.mapper = mapper;
}
public void render(Repository repository) {
Table table = createTable();
RepositoryCommandDto dto = mapper.map(repository);
table.addLabelValueRow("repoNamespace", dto.getNamespace());
table.addLabelValueRow("repoName", dto.getName());
table.addLabelValueRow("repoType", dto.getType());
table.addLabelValueRow("repoContact", dto.getContact());
table.addLabelValueRow("repoCreationDate", dto.getCreationDate());
table.addLabelValueRow("repoLastModified", dto.getLastModified());
table.addLabelValueRow("repoUrl", dto.getUrl());
table.addLabelValueRow("repoDescription", dto.getDescription());
renderToStdout(DETAILS_TABLE_TEMPLATE, ImmutableMap.of("rows", table, "repo", dto));
}
public void renderInvalidInputError() {
renderToStderr(INVALID_INPUT_TEMPLATE, Collections.emptyMap());
context.exit(ExitCode.USAGE);
}
public void renderNotFoundError() {
renderToStderr(NOT_FOUND_TEMPLATE, Collections.emptyMap());
context.exit(ExitCode.NOT_FOUND);
}
public void renderException(Exception exception) {
renderDefaultError(exception);
context.exit(ExitCode.SERVER_ERROR);
}
}

View 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.
*/
package sonia.scm.repository.cli;
import com.google.common.annotations.VisibleForTesting;
import org.mapstruct.Mapper;
import org.mapstruct.ObjectFactory;
import sonia.scm.repository.Repository;
import sonia.scm.repository.api.RepositoryService;
import sonia.scm.repository.api.RepositoryServiceFactory;
import sonia.scm.repository.api.ScmProtocol;
import javax.inject.Inject;
import java.time.Instant;
import java.time.format.DateTimeFormatter;
import java.util.Optional;
@Mapper
public abstract class RepositoryToRepositoryCommandDtoMapper {
@Inject
private RepositoryServiceFactory serviceFactory;
public abstract RepositoryCommandDto map(Repository modelObject);
@ObjectFactory
RepositoryCommandDto createDto(Repository repository) {
RepositoryCommandDto dto = new RepositoryCommandDto();
try (RepositoryService service = serviceFactory.create(repository)) {
Optional<ScmProtocol> protocolUrl = service.getSupportedProtocols().filter(p -> p.getType().equals("http")).findFirst();
protocolUrl.ifPresent(scmProtocol -> dto.setUrl(scmProtocol.getUrl()));
}
return dto;
}
String mapTimestampToISODate(Long timestamp) {
if (timestamp != null) {
return DateTimeFormatter.ISO_INSTANT.format(Instant.ofEpochMilli(timestamp));
}
return null;
}
@VisibleForTesting
void setServiceFactory(RepositoryServiceFactory serviceFactory) {
this.serviceFactory = serviceFactory;
}
}

View File

@@ -90,14 +90,18 @@ public class ApiKeyRealm extends AuthenticatingRealm {
}
private AuthenticationInfo buildAuthenticationInfo(AuthenticationToken token, ApiKeyService.CheckResult check) {
RepositoryRole repositoryRole = determineRole(check);
Scope scope = createScope(repositoryRole);
LOG.debug("login for user {} with api key limited to role {}", check.getUser(), check.getPermissionRole());
return helper
DAORealmHelper.AuthenticationInfoBuilder builder = helper
.authenticationInfoBuilder(check.getUser())
.withSessionId(getPrincipal(token))
.withScope(scope)
.build();
.withSessionId(getPrincipal(token));
if (!check.getPermissionRole().equals("*")) {
RepositoryRole repositoryRole = determineRole(check);
Scope scope = createScope(repositoryRole);
LOG.debug("login for user {} with api key limited to role {}", check.getUser(), check.getPermissionRole());
builder = builder.withScope(scope);
}
return builder.build();
}
private String getPassword(AuthenticationToken token) {

View File

@@ -0,0 +1,49 @@
/*
* 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.validation;
import javax.inject.Inject;
import javax.inject.Provider;
import javax.validation.ConstraintValidatorFactory;
import javax.validation.Validation;
import javax.validation.Validator;
public class DefaultValidatorProvider implements Provider<Validator> {
private final ConstraintValidatorFactory constraintValidatorFactory;
@Inject
public DefaultValidatorProvider(ConstraintValidatorFactory constraintValidatorFactory) {
this.constraintValidatorFactory = constraintValidatorFactory;
}
@Override
public Validator get() {
return Validation.buildDefaultValidatorFactory()
.usingContext()
.constraintValidatorFactory(constraintValidatorFactory)
.getValidator();
}
}

View File

@@ -0,0 +1,51 @@
/*
* 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.validation;
import com.google.inject.Injector;
import javax.inject.Inject;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorFactory;
public class GuiceConstraintValidatorFactory implements ConstraintValidatorFactory {
private final Injector injector;
@Inject
public GuiceConstraintValidatorFactory(Injector injector) {
this.injector = injector;
}
@Override
public <T extends ConstraintValidator<?, ?>> T getInstance(Class<T> key) {
return injector.getInstance(key);
}
@Override
public void releaseInstance(ConstraintValidator<?, ?> instance) {
// do nothing
}
}

View File

@@ -0,0 +1,78 @@
/*
* 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.validation;
import org.jboss.resteasy.plugins.validation.GeneralValidatorImpl;
import org.jboss.resteasy.spi.HttpRequest;
import javax.validation.ConstraintValidatorFactory;
import javax.validation.MessageInterpolator;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import javax.validation.executable.ExecutableType;
import java.util.List;
import java.util.Locale;
import java.util.Set;
public class ResteasyValidator extends GeneralValidatorImpl {
private final ValidatorFactory validatorFactory;
private final ConstraintValidatorFactory constraintValidatorFactory;
ResteasyValidator(ValidatorFactory validatorFactory, ConstraintValidatorFactory constraintValidatorFactory, boolean isExecutableValidationEnabled, Set<ExecutableType> defaultValidatedExecutableTypes) {
super(Validation.buildDefaultValidatorFactory(), isExecutableValidationEnabled, defaultValidatedExecutableTypes);
this.validatorFactory = validatorFactory;
this.constraintValidatorFactory = constraintValidatorFactory;
}
@Override
protected Validator getValidator(HttpRequest request) {
Validator v = Validator.class.cast(request.getAttribute(Validator.class.getName()));
if (v == null) {
Locale locale = getLocaleFrom(request);
v = createValidator(locale);
request.setAttribute(Validator.class.getName(), v);
}
return v;
}
private Validator createValidator(Locale locale) {
MessageInterpolator interpolator = new LocaleSpecificMessageInterpolator(validatorFactory.getMessageInterpolator(), locale);
return validatorFactory.usingContext()
.constraintValidatorFactory(constraintValidatorFactory)
.messageInterpolator(interpolator)
.getValidator();
}
private Locale getLocaleFrom(HttpRequest request) {
if (request == null) {
return null;
}
List<Locale> locales = request.getHttpHeaders().getAcceptableLanguages();
return locales == null || locales.isEmpty() ? Locale.ENGLISH : locales.get(0);
}
}

View File

@@ -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.validation;
import org.jboss.resteasy.spi.validation.GeneralValidator;
import javax.inject.Inject;
import javax.validation.BootstrapConfiguration;
import javax.validation.Configuration;
import javax.validation.ConstraintValidatorFactory;
import javax.validation.Validation;
import javax.ws.rs.ext.ContextResolver;
import javax.ws.rs.ext.Provider;
@Provider
public class ResteasyValidatorContextResolver implements ContextResolver<GeneralValidator> {
private final ConstraintValidatorFactory constraintValidatorFactory;
@Inject
public ResteasyValidatorContextResolver(ConstraintValidatorFactory constraintValidatorFactory) {
this.constraintValidatorFactory = constraintValidatorFactory;
}
@Override
public GeneralValidator getContext(Class<?> type) {
Configuration<?> configuration = Validation.byDefaultProvider().configure();
BootstrapConfiguration bootstrapConfiguration = configuration.getBootstrapConfiguration();
return new ResteasyValidator(
configuration.buildValidatorFactory(),
constraintValidatorFactory,
bootstrapConfiguration.isExecutableValidationEnabled(),
bootstrapConfiguration.getDefaultValidatedExecutableTypes()
);
}
}

View File

@@ -0,0 +1,39 @@
/*
* 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.validation;
import com.google.inject.AbstractModule;
import javax.validation.ConstraintValidatorFactory;
import javax.validation.Validator;
public class ValidationModule extends AbstractModule {
@Override
protected void configure() {
bind(ConstraintValidatorFactory.class).to(GuiceConstraintValidatorFactory.class);
bind(Validator.class).toProvider(DefaultValidatorProvider.class);
}
}