diff --git a/README.md b/README.md index dc4a980d4..ffe2b0e34 100644 --- a/README.md +++ b/README.md @@ -123,6 +123,25 @@ meta: interval: 5 # Publishing interval in seconds, default value is 5 ``` +## Single repository on port + +Artipie repositories may run on separate ports if configured. +This feature may be especially useful for Docker repository, +as it's API is not well suited to serve multiple repositories on single port. + +To run repository on its own port +`port` parameter should be specified in repository configuration YAML as follows: + +```yaml +repo: + type: + port: 54321 + ... +``` + +*NOTE: Artipie scans repositories for port configuration only on start, +so server requires restart in order to apply changes made in runtime.* + ## Artipie REST API Artipie provides a set of APIs to manage repositories and users. The current APIs are fully documented [here](./REST_API.md). diff --git a/examples/docker/README.md b/examples/docker/README.md index b811f5476..ae4d2f15c 100644 --- a/examples/docker/README.md +++ b/examples/docker/README.md @@ -52,6 +52,28 @@ docker pull localhost:8080/my-docker/myfirstimage #### Advanced options +##### Docker on port + +We may also assign a port for the repository +to access the image by name without using `my-docker` prefix. +To do that we add `host: 8081` parameter to existing `my-docker.yaml`: + +```yaml +repo: + host: 8081 + type: docker + storage: + type: fs + path: /var/artipie/data +``` + +Now we may pull image `localhost:8080/my-docker/myfirstimage` +we pushed before as `localhost:8081/myfirstimage`: + +```bash +docker pull localhost:8081/myfirstimage +``` + ##### Security Docker registry has to be protected by HTTPS and should have no prefix in path. diff --git a/src/test/java/com/artipie/ArtipieServer.java b/src/test/java/com/artipie/ArtipieServer.java index 9fb0a480c..ed72ab3a7 100644 --- a/src/test/java/com/artipie/ArtipieServer.java +++ b/src/test/java/com/artipie/ArtipieServer.java @@ -57,6 +57,11 @@ public class ArtipieServer { */ public static final User CAROL = new User("carol", "LetMeIn"); + /** + * Credentials file name. + */ + public static final String CREDENTIALS_FILE = "_credentials.yml"; + /** * All users. */ @@ -64,11 +69,6 @@ public class ArtipieServer { ArtipieServer.ALICE, ArtipieServer.BOB, ArtipieServer.CAROL ); - /** - * Credentials file name. - */ - private static final String CREDENTIALS_FILE = "_credentials.yml"; - /** * Root path. */ diff --git a/src/test/java/com/artipie/FilesRepoITCase.java b/src/test/java/com/artipie/FilesRepoITCase.java index bffe3181c..4dacf7a7b 100644 --- a/src/test/java/com/artipie/FilesRepoITCase.java +++ b/src/test/java/com/artipie/FilesRepoITCase.java @@ -24,6 +24,8 @@ package com.artipie; import com.amihaiemil.eoyaml.Yaml; +import com.amihaiemil.eoyaml.YamlMapping; +import com.amihaiemil.eoyaml.YamlMappingBuilder; import com.artipie.asto.Key; import com.artipie.asto.Storage; import com.artipie.asto.blocking.BlockingStorage; @@ -31,17 +33,21 @@ import com.artipie.asto.test.TestResource; import com.jcabi.log.Logger; import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; import java.util.regex.Pattern; import org.hamcrest.MatcherAssert; import org.hamcrest.core.IsEqual; import org.hamcrest.core.StringContains; import org.hamcrest.text.MatchesPattern; import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledOnOs; import org.junit.jupiter.api.condition.OS; import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.testcontainers.Testcontainers; import org.testcontainers.containers.GenericContainer; @@ -50,7 +56,10 @@ * @since 0.11 * @checkstyle ClassDataAbstractionCouplingCheck (500 lines) */ -@SuppressWarnings("PMD.AvoidDuplicateLiterals") +@SuppressWarnings({ + "PMD.AvoidDuplicateLiterals", + "PMD.TooManyMethods" +}) @EnabledOnOs({OS.LINUX, OS.MAC}) final class FilesRepoITCase { @@ -81,29 +90,13 @@ final class FilesRepoITCase { */ private int port; - @BeforeEach - void init() throws Exception { - this.storage = new FileStorage(this.tmp); - this.server = new ArtipieServer(this.tmp, "my-file", this.config()); - this.port = this.server.start(); - this.server.start(); - Testcontainers.exposeHostPorts(this.port); - this.cntn = new GenericContainer<>("centos:centos8") - .withCommand("tail", "-f", "/dev/null") - .withWorkingDirectory("/home/") - .withFileSystemBind(this.tmp.toString(), "/home"); - this.cntn.start(); - this.exec("yum", "-y", "install", "curl"); - } - - @Test - void curlGetShouldReceiveFile() throws Exception { - final String url = "http://host.testcontainers.internal:%d/my-file/file-repo/curl.txt"; - this.addFilesToStorage( - "file-repo", new Key.From("repos", "my-file", "file-repo") - ); + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void curlGetShouldReceiveFile(final boolean anonymous) throws Exception { + this.init(this.config(anonymous)); + this.addFilesToStorage("file-repo", new Key.From("repos", "my-file", "file-repo")); MatcherAssert.assertThat( - this.exec("curl", "-i", "-X", "GET", String.format(url, this.port)), + this.curl("GET", this.userOpt(anonymous)), new MatchesPattern( Pattern.compile( // @checkstyle LineLengthCheck (1 line) @@ -113,12 +106,13 @@ void curlGetShouldReceiveFile() throws Exception { ); } - @Test - void curlPutShouldSaveFile() throws Exception { - final String url = "http://host.testcontainers.internal:%d/my-file/file-repo/curl.txt"; + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void curlPutShouldSaveFile(final boolean anonymous) throws Exception { + this.init(this.config(anonymous)); MatcherAssert.assertThat( "curl PUT does work properly", - this.exec("curl", "-i", "-X", "PUT", String.format(url, this.port)), + this.curl("PUT", this.userOpt(anonymous)), new StringContains("HTTP/1.1 201 Created") ); MatcherAssert.assertThat( @@ -130,35 +124,118 @@ void curlPutShouldSaveFile() throws Exception { ); } + @ParameterizedTest + @ValueSource(strings = {"PUT", "GET"}) + void curlPutAndGetShouldFailWithUnauthorized(final String req) throws Exception { + this.init(this.config(false)); + MatcherAssert.assertThat( + this.curl( + req, Optional.of( + new ArtipieServer.User( + ArtipieServer.ALICE.name(), + String.format("bad%s", ArtipieServer.ALICE.password()) + ) + ) + ), + new StringContains("HTTP/1.1 401 Unauthorized") + ); + } + + @ParameterizedTest + @ValueSource(strings = {"PUT", "GET"}) + void curlPutAndGetShouldFailWithForbidden(final String req) throws Exception { + this.init(this.config(false)); + MatcherAssert.assertThat( + this.curl(req, Optional.of(ArtipieServer.BOB)), + new StringContains("HTTP/1.1 403 Forbidden") + ); + } + @AfterEach - void release() throws Exception { + void tearDown() { this.server.stop(); this.cntn.stop(); } - private String exec(final String... command) throws Exception { - Logger.debug(this, "Command:\n%s", String.join(" ", command)); - return this.cntn.execInContainer(command).getStdout(); + private String curl(final String action, + final Optional user) throws Exception { + final String url = "http://host.testcontainers.internal:%d/my-file/file-repo/curl.txt"; + final List cmdlst = new ArrayList<>( + Arrays.asList( + "curl", "-i", "-X", action, String.format(url, this.port) + ) + ); + user.ifPresent( + usr -> { + cmdlst.add("--user"); + cmdlst.add(String.format("%s:%s", usr.name(), usr.password())); + } + ); + final String[] cmdarr = cmdlst.toArray(new String[0]); + Logger.debug(this, "Command:\n%s", String.join(" ", cmdlst)); + return this.cntn.execInContainer(cmdarr).getStdout(); + } + + private void init(final String config) throws Exception { + this.storage = new FileStorage(this.tmp); + this.server = new ArtipieServer(this.tmp, "my-file", config); + this.port = this.server.start(); + this.server.start(); + Testcontainers.exposeHostPorts(this.port); + this.cntn = new GenericContainer<>("centos:centos8") + .withCommand("tail", "-f", "/dev/null") + .withWorkingDirectory("/home/") + .withFileSystemBind(this.tmp.toString(), "/home"); + this.cntn.start(); + Logger.debug(this, "Command:\nyum -y install curl"); + this.cntn.execInContainer("yum", "-y", "install", "curl"); + } + + private String config(final boolean anonymous) { + YamlMappingBuilder yaml = Yaml.createYamlMappingBuilder() + .add("type", "file") + .add( + "storage", + Yaml.createYamlMappingBuilder() + .add("type", "fs") + .add("path", this.tmp.resolve("repos").toString()) + .build() + ); + if (!anonymous) { + yaml = yaml.add( + "credentials", + Yaml.createYamlMappingBuilder() + .add("type", "file") + .add("path", ArtipieServer.CREDENTIALS_FILE) + .build() + ) + .add( + "permissions", + this.perms() + ); + } + return Yaml.createYamlMappingBuilder() + .add( + "repo", yaml.build() + ).build().toString(); } - private String config() { - return Yaml.createYamlMappingBuilder().add( - "repo", - Yaml.createYamlMappingBuilder() - .add("type", "file") - .add( - "storage", - Yaml.createYamlMappingBuilder() - .add("type", "fs") - .add("path", this.tmp.resolve("repos").toString()) - .build() - ) - .build() - ).build().toString(); + private YamlMapping perms() { + return Yaml.createYamlMappingBuilder() + .add( + ArtipieServer.ALICE.name(), + Yaml.createYamlSequenceBuilder() + .add("write") + .add("download") + .build() + ) + .add( + ArtipieServer.BOB.name(), + Yaml.createYamlSequenceBuilder().build() + ).build(); } - private void addFilesToStorage(final String resource, final Key key) - throws InterruptedException { + private void addFilesToStorage(final String resource, final Key key) { final Storage resources = new FileStorage( new TestResource(resource).asPath() ); @@ -170,4 +247,14 @@ private void addFilesToStorage(final String resource, final Key key) ); } } + + private Optional userOpt(final boolean anonymous) { + final Optional user; + if (anonymous) { + user = Optional.empty(); + } else { + user = Optional.of(ArtipieServer.ALICE); + } + return user; + } }