Add application directory layer to ephemeral builder for Podman support
Update `EphemeralBuilder` so that it adds an additional layer that containing an empty application (aka workspace) directory owned by the build user. Prior to this commit, the directory was only bound. This could cause issues on Podman where, unlike Docker, the bound directory is owned by `root`. Fixes gh-45233
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2012-2024 the original author or authors.
|
* Copyright 2012-2025 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@@ -29,6 +29,7 @@ import org.springframework.boot.buildpack.platform.docker.configuration.DockerCo
|
|||||||
import org.springframework.boot.buildpack.platform.docker.configuration.ResolvedDockerHost;
|
import org.springframework.boot.buildpack.platform.docker.configuration.ResolvedDockerHost;
|
||||||
import org.springframework.boot.buildpack.platform.docker.transport.DockerEngineException;
|
import org.springframework.boot.buildpack.platform.docker.transport.DockerEngineException;
|
||||||
import org.springframework.boot.buildpack.platform.docker.type.Image;
|
import org.springframework.boot.buildpack.platform.docker.type.Image;
|
||||||
|
import org.springframework.boot.buildpack.platform.docker.type.ImageArchive;
|
||||||
import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
|
import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
|
||||||
import org.springframework.boot.buildpack.platform.io.IOBiConsumer;
|
import org.springframework.boot.buildpack.platform.io.IOBiConsumer;
|
||||||
import org.springframework.boot.buildpack.platform.io.TarArchive;
|
import org.springframework.boot.buildpack.platform.io.TarArchive;
|
||||||
@@ -110,16 +111,10 @@ public class Builder {
|
|||||||
Buildpacks buildpacks = getBuildpacks(request, imageFetcher, builderMetadata, buildpackLayersMetadata);
|
Buildpacks buildpacks = getBuildpacks(request, imageFetcher, builderMetadata, buildpackLayersMetadata);
|
||||||
EphemeralBuilder ephemeralBuilder = new EphemeralBuilder(buildOwner, builderImage, request.getName(),
|
EphemeralBuilder ephemeralBuilder = new EphemeralBuilder(buildOwner, builderImage, request.getName(),
|
||||||
builderMetadata, request.getCreator(), request.getEnv(), buildpacks);
|
builderMetadata, request.getCreator(), request.getEnv(), buildpacks);
|
||||||
this.docker.image().load(ephemeralBuilder.getArchive(), UpdateListener.none());
|
executeLifecycle(request, ephemeralBuilder);
|
||||||
try {
|
tagImage(request.getName(), request.getTags());
|
||||||
executeLifecycle(request, ephemeralBuilder);
|
if (request.isPublish()) {
|
||||||
tagImage(request.getName(), request.getTags());
|
pushImages(request.getName(), request.getTags());
|
||||||
if (request.isPublish()) {
|
|
||||||
pushImages(request.getName(), request.getTags());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
this.docker.image().remove(ephemeralBuilder.getName(), true);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,13 +152,25 @@ public class Builder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void executeLifecycle(BuildRequest request, EphemeralBuilder builder) throws IOException {
|
private void executeLifecycle(BuildRequest request, EphemeralBuilder builder) throws IOException {
|
||||||
ResolvedDockerHost dockerHost = null;
|
try (Lifecycle lifecycle = new Lifecycle(this.log, this.docker, getDockerHost(), request, builder)) {
|
||||||
if (this.dockerConfiguration != null && this.dockerConfiguration.isBindHostToBuilder()) {
|
executeLifecycle(builder, lifecycle);
|
||||||
dockerHost = ResolvedDockerHost.from(this.dockerConfiguration.getHost());
|
|
||||||
}
|
}
|
||||||
try (Lifecycle lifecycle = new Lifecycle(this.log, this.docker, dockerHost, request, builder)) {
|
}
|
||||||
|
|
||||||
|
private void executeLifecycle(EphemeralBuilder builder, Lifecycle lifecycle) throws IOException {
|
||||||
|
ImageArchive archive = builder.getArchive(lifecycle.getApplicationDirectory());
|
||||||
|
this.docker.image().load(archive, UpdateListener.none());
|
||||||
|
try {
|
||||||
lifecycle.execute();
|
lifecycle.execute();
|
||||||
}
|
}
|
||||||
|
finally {
|
||||||
|
this.docker.image().remove(builder.getName(), true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ResolvedDockerHost getDockerHost() {
|
||||||
|
boolean bindHostToBuilder = this.dockerConfiguration != null && this.dockerConfiguration.isBindHostToBuilder();
|
||||||
|
return (bindHostToBuilder) ? ResolvedDockerHost.from(this.dockerConfiguration.getHost()) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void tagImage(ImageReference sourceReference, List<ImageReference> tags) throws IOException {
|
private void tagImage(ImageReference sourceReference, List<ImageReference> tags) throws IOException {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2012-2024 the original author or authors.
|
* Copyright 2012-2025 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@@ -21,11 +21,14 @@ import java.util.Map;
|
|||||||
|
|
||||||
import org.springframework.boot.buildpack.platform.docker.type.Image;
|
import org.springframework.boot.buildpack.platform.docker.type.Image;
|
||||||
import org.springframework.boot.buildpack.platform.docker.type.ImageArchive;
|
import org.springframework.boot.buildpack.platform.docker.type.ImageArchive;
|
||||||
|
import org.springframework.boot.buildpack.platform.docker.type.ImageArchive.Update;
|
||||||
import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
|
import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
|
||||||
import org.springframework.boot.buildpack.platform.docker.type.Layer;
|
import org.springframework.boot.buildpack.platform.docker.type.Layer;
|
||||||
import org.springframework.boot.buildpack.platform.io.Content;
|
import org.springframework.boot.buildpack.platform.io.Content;
|
||||||
|
import org.springframework.boot.buildpack.platform.io.IOConsumer;
|
||||||
import org.springframework.boot.buildpack.platform.io.Owner;
|
import org.springframework.boot.buildpack.platform.io.Owner;
|
||||||
import org.springframework.util.CollectionUtils;
|
import org.springframework.util.CollectionUtils;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A short-lived builder that is created for each {@link Lifecycle} run.
|
* A short-lived builder that is created for each {@link Lifecycle} run.
|
||||||
@@ -37,13 +40,17 @@ class EphemeralBuilder {
|
|||||||
|
|
||||||
static final String BUILDER_FOR_LABEL_NAME = "org.springframework.boot.builderFor";
|
static final String BUILDER_FOR_LABEL_NAME = "org.springframework.boot.builderFor";
|
||||||
|
|
||||||
|
private ImageReference name;
|
||||||
|
|
||||||
private final BuildOwner buildOwner;
|
private final BuildOwner buildOwner;
|
||||||
|
|
||||||
|
private final Creator creator;
|
||||||
|
|
||||||
private final BuilderMetadata builderMetadata;
|
private final BuilderMetadata builderMetadata;
|
||||||
|
|
||||||
private final ImageArchive archive;
|
private final Image builderImage;
|
||||||
|
|
||||||
private final Creator creator;
|
private final IOConsumer<Update> archiveUpdate;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new {@link EphemeralBuilder} instance.
|
* Create a new {@link EphemeralBuilder} instance.
|
||||||
@@ -54,26 +61,25 @@ class EphemeralBuilder {
|
|||||||
* @param creator the builder creator
|
* @param creator the builder creator
|
||||||
* @param env the builder env
|
* @param env the builder env
|
||||||
* @param buildpacks an optional set of buildpacks to apply
|
* @param buildpacks an optional set of buildpacks to apply
|
||||||
* @throws IOException on IO error
|
|
||||||
*/
|
*/
|
||||||
EphemeralBuilder(BuildOwner buildOwner, Image builderImage, ImageReference targetImage,
|
EphemeralBuilder(BuildOwner buildOwner, Image builderImage, ImageReference targetImage,
|
||||||
BuilderMetadata builderMetadata, Creator creator, Map<String, String> env, Buildpacks buildpacks)
|
BuilderMetadata builderMetadata, Creator creator, Map<String, String> env, Buildpacks buildpacks) {
|
||||||
throws IOException {
|
this.name = ImageReference.random("pack.local/builder/").inTaggedForm();
|
||||||
ImageReference name = ImageReference.random("pack.local/builder/").inTaggedForm();
|
|
||||||
this.buildOwner = buildOwner;
|
this.buildOwner = buildOwner;
|
||||||
this.creator = creator;
|
this.creator = creator;
|
||||||
this.builderMetadata = builderMetadata.copy(this::updateMetadata);
|
this.builderMetadata = builderMetadata.copy(this::updateMetadata);
|
||||||
this.archive = ImageArchive.from(builderImage, (update) -> {
|
this.builderImage = builderImage;
|
||||||
|
this.archiveUpdate = (update) -> {
|
||||||
update.withUpdatedConfig(this.builderMetadata::attachTo);
|
update.withUpdatedConfig(this.builderMetadata::attachTo);
|
||||||
update.withUpdatedConfig((config) -> config.withLabel(BUILDER_FOR_LABEL_NAME, targetImage.toString()));
|
update.withUpdatedConfig((config) -> config.withLabel(BUILDER_FOR_LABEL_NAME, targetImage.toString()));
|
||||||
update.withTag(name);
|
update.withTag(this.name);
|
||||||
if (!CollectionUtils.isEmpty(env)) {
|
if (!CollectionUtils.isEmpty(env)) {
|
||||||
update.withNewLayer(getEnvLayer(env));
|
update.withNewLayer(getEnvLayer(env));
|
||||||
}
|
}
|
||||||
if (buildpacks != null) {
|
if (buildpacks != null) {
|
||||||
buildpacks.apply(update::withNewLayer);
|
buildpacks.apply(update::withNewLayer);
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateMetadata(BuilderMetadata.Update update) {
|
private void updateMetadata(BuilderMetadata.Update update) {
|
||||||
@@ -95,7 +101,7 @@ class EphemeralBuilder {
|
|||||||
* @return the ephemeral builder name
|
* @return the ephemeral builder name
|
||||||
*/
|
*/
|
||||||
ImageReference getName() {
|
ImageReference getName() {
|
||||||
return this.archive.getTag();
|
return this.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -116,15 +122,26 @@ class EphemeralBuilder {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the contents of ephemeral builder for passing to Docker.
|
* Return the contents of ephemeral builder for passing to Docker.
|
||||||
|
* @param applicationDirectory the application directory
|
||||||
* @return the ephemeral builder archive
|
* @return the ephemeral builder archive
|
||||||
|
* @throws IOException on IO error
|
||||||
*/
|
*/
|
||||||
ImageArchive getArchive() {
|
ImageArchive getArchive(String applicationDirectory) throws IOException {
|
||||||
return this.archive;
|
return ImageArchive.from(this.builderImage, (update) -> {
|
||||||
|
this.archiveUpdate.accept(update);
|
||||||
|
if (StringUtils.hasLength(applicationDirectory)) {
|
||||||
|
update.withNewLayer(applicationDirectoryLayer(applicationDirectory));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private Layer applicationDirectoryLayer(String applicationDirectory) throws IOException {
|
||||||
|
return Layer.of((layout) -> layout.directory(applicationDirectory, this.buildOwner));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return this.archive.getTag().toString();
|
return this.name.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2012-2024 the original author or authors.
|
* Copyright 2012-2025 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@@ -116,6 +116,10 @@ class Lifecycle implements Closeable {
|
|||||||
this.securityOptions = getSecurityOptions(request);
|
this.securityOptions = getSecurityOptions(request);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String getApplicationDirectory() {
|
||||||
|
return this.applicationDirectory;
|
||||||
|
}
|
||||||
|
|
||||||
private Cache getBuildCache(BuildRequest request) {
|
private Cache getBuildCache(BuildRequest request) {
|
||||||
if (request.getBuildCache() != null) {
|
if (request.getBuildCache() != null) {
|
||||||
return request.getBuildCache();
|
return request.getBuildCache();
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2012-2023 the original author or authors.
|
* Copyright 2012-2025 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@@ -88,7 +88,7 @@ class EphemeralBuilderTests extends AbstractJsonTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void getNameHasRandomName() throws Exception {
|
void getNameHasRandomName() {
|
||||||
EphemeralBuilder b1 = new EphemeralBuilder(this.owner, this.image, this.targetImage, this.metadata,
|
EphemeralBuilder b1 = new EphemeralBuilder(this.owner, this.image, this.targetImage, this.metadata,
|
||||||
this.creator, this.env, this.buildpacks);
|
this.creator, this.env, this.buildpacks);
|
||||||
EphemeralBuilder b2 = new EphemeralBuilder(this.owner, this.image, this.targetImage, this.metadata,
|
EphemeralBuilder b2 = new EphemeralBuilder(this.owner, this.image, this.targetImage, this.metadata,
|
||||||
@@ -101,7 +101,7 @@ class EphemeralBuilderTests extends AbstractJsonTests {
|
|||||||
void getArchiveHasCreatedByConfig() throws Exception {
|
void getArchiveHasCreatedByConfig() throws Exception {
|
||||||
EphemeralBuilder builder = new EphemeralBuilder(this.owner, this.image, this.targetImage, this.metadata,
|
EphemeralBuilder builder = new EphemeralBuilder(this.owner, this.image, this.targetImage, this.metadata,
|
||||||
this.creator, this.env, this.buildpacks);
|
this.creator, this.env, this.buildpacks);
|
||||||
ImageConfig config = builder.getArchive().getImageConfig();
|
ImageConfig config = builder.getArchive(null).getImageConfig();
|
||||||
BuilderMetadata ephemeralMetadata = BuilderMetadata.fromImageConfig(config);
|
BuilderMetadata ephemeralMetadata = BuilderMetadata.fromImageConfig(config);
|
||||||
assertThat(ephemeralMetadata.getCreatedBy().getName()).isEqualTo("Spring Boot");
|
assertThat(ephemeralMetadata.getCreatedBy().getName()).isEqualTo("Spring Boot");
|
||||||
assertThat(ephemeralMetadata.getCreatedBy().getVersion()).isEqualTo("dev");
|
assertThat(ephemeralMetadata.getCreatedBy().getVersion()).isEqualTo("dev");
|
||||||
@@ -111,7 +111,7 @@ class EphemeralBuilderTests extends AbstractJsonTests {
|
|||||||
void getArchiveHasTag() throws Exception {
|
void getArchiveHasTag() throws Exception {
|
||||||
EphemeralBuilder builder = new EphemeralBuilder(this.owner, this.image, this.targetImage, this.metadata,
|
EphemeralBuilder builder = new EphemeralBuilder(this.owner, this.image, this.targetImage, this.metadata,
|
||||||
this.creator, this.env, this.buildpacks);
|
this.creator, this.env, this.buildpacks);
|
||||||
ImageReference tag = builder.getArchive().getTag();
|
ImageReference tag = builder.getArchive(null).getTag();
|
||||||
assertThat(tag.toString()).startsWith("pack.local/builder/").endsWith(":latest");
|
assertThat(tag.toString()).startsWith("pack.local/builder/").endsWith(":latest");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,7 +119,7 @@ class EphemeralBuilderTests extends AbstractJsonTests {
|
|||||||
void getArchiveHasFixedCreatedDate() throws Exception {
|
void getArchiveHasFixedCreatedDate() throws Exception {
|
||||||
EphemeralBuilder builder = new EphemeralBuilder(this.owner, this.image, this.targetImage, this.metadata,
|
EphemeralBuilder builder = new EphemeralBuilder(this.owner, this.image, this.targetImage, this.metadata,
|
||||||
this.creator, this.env, this.buildpacks);
|
this.creator, this.env, this.buildpacks);
|
||||||
Instant createInstant = builder.getArchive().getCreateDate();
|
Instant createInstant = builder.getArchive(null).getCreateDate();
|
||||||
OffsetDateTime createDateTime = OffsetDateTime.ofInstant(createInstant, ZoneId.of("UTC"));
|
OffsetDateTime createDateTime = OffsetDateTime.ofInstant(createInstant, ZoneId.of("UTC"));
|
||||||
assertThat(createDateTime.getYear()).isEqualTo(1980);
|
assertThat(createDateTime.getYear()).isEqualTo(1980);
|
||||||
assertThat(createDateTime.getMonthValue()).isOne();
|
assertThat(createDateTime.getMonthValue()).isOne();
|
||||||
@@ -133,7 +133,7 @@ class EphemeralBuilderTests extends AbstractJsonTests {
|
|||||||
void getArchiveContainsEnvLayer() throws Exception {
|
void getArchiveContainsEnvLayer() throws Exception {
|
||||||
EphemeralBuilder builder = new EphemeralBuilder(this.owner, this.image, this.targetImage, this.metadata,
|
EphemeralBuilder builder = new EphemeralBuilder(this.owner, this.image, this.targetImage, this.metadata,
|
||||||
this.creator, this.env, this.buildpacks);
|
this.creator, this.env, this.buildpacks);
|
||||||
File directory = unpack(getLayer(builder.getArchive(), EXISTING_IMAGE_LAYER_COUNT), "env");
|
File directory = unpack(getLayer(builder.getArchive(null), EXISTING_IMAGE_LAYER_COUNT), "env");
|
||||||
assertThat(new File(directory, "platform/env/spring")).usingCharset(StandardCharsets.UTF_8).hasContent("boot");
|
assertThat(new File(directory, "platform/env/spring")).usingCharset(StandardCharsets.UTF_8).hasContent("boot");
|
||||||
assertThat(new File(directory, "platform/env/empty")).usingCharset(StandardCharsets.UTF_8).hasContent("");
|
assertThat(new File(directory, "platform/env/empty")).usingCharset(StandardCharsets.UTF_8).hasContent("");
|
||||||
}
|
}
|
||||||
@@ -142,7 +142,7 @@ class EphemeralBuilderTests extends AbstractJsonTests {
|
|||||||
void getArchiveHasBuilderForLabel() throws Exception {
|
void getArchiveHasBuilderForLabel() throws Exception {
|
||||||
EphemeralBuilder builder = new EphemeralBuilder(this.owner, this.image, this.targetImage, this.metadata,
|
EphemeralBuilder builder = new EphemeralBuilder(this.owner, this.image, this.targetImage, this.metadata,
|
||||||
this.creator, this.env, this.buildpacks);
|
this.creator, this.env, this.buildpacks);
|
||||||
ImageConfig config = builder.getArchive().getImageConfig();
|
ImageConfig config = builder.getArchive(null).getImageConfig();
|
||||||
assertThat(config.getLabels())
|
assertThat(config.getLabels())
|
||||||
.contains(entry(EphemeralBuilder.BUILDER_FOR_LABEL_NAME, this.targetImage.toString()));
|
.contains(entry(EphemeralBuilder.BUILDER_FOR_LABEL_NAME, this.targetImage.toString()));
|
||||||
}
|
}
|
||||||
@@ -162,13 +162,21 @@ class EphemeralBuilderTests extends AbstractJsonTests {
|
|||||||
"/cnb/buildpacks/example_buildpack2/0.0.2/buildpack.toml");
|
"/cnb/buildpacks/example_buildpack2/0.0.2/buildpack.toml");
|
||||||
assertBuildpackLayerContent(builder, EXISTING_IMAGE_LAYER_COUNT + 2,
|
assertBuildpackLayerContent(builder, EXISTING_IMAGE_LAYER_COUNT + 2,
|
||||||
"/cnb/buildpacks/example_buildpack3/0.0.3/buildpack.toml");
|
"/cnb/buildpacks/example_buildpack3/0.0.3/buildpack.toml");
|
||||||
File orderDirectory = unpack(getLayer(builder.getArchive(), EXISTING_IMAGE_LAYER_COUNT + 3), "order");
|
File orderDirectory = unpack(getLayer(builder.getArchive(null), EXISTING_IMAGE_LAYER_COUNT + 3), "order");
|
||||||
assertThat(new File(orderDirectory, "cnb/order.toml")).usingCharset(StandardCharsets.UTF_8)
|
assertThat(new File(orderDirectory, "cnb/order.toml")).usingCharset(StandardCharsets.UTF_8)
|
||||||
.hasContent(content("order.toml"));
|
.hasContent(content("order.toml"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getArchiveHasApplicationDirectoryLayer() throws Exception {
|
||||||
|
EphemeralBuilder builder = new EphemeralBuilder(this.owner, this.image, this.targetImage, this.metadata,
|
||||||
|
this.creator, this.env, this.buildpacks);
|
||||||
|
File directory = unpack(getLayer(builder.getArchive("/myapp"), EXISTING_IMAGE_LAYER_COUNT + 1), "appdir");
|
||||||
|
assertThat(new File(directory, "myapp")).isDirectory();
|
||||||
|
}
|
||||||
|
|
||||||
private void assertBuildpackLayerContent(EphemeralBuilder builder, int index, String s) throws Exception {
|
private void assertBuildpackLayerContent(EphemeralBuilder builder, int index, String s) throws Exception {
|
||||||
File buildpackDirectory = unpack(getLayer(builder.getArchive(), index), "buildpack");
|
File buildpackDirectory = unpack(getLayer(builder.getArchive(null), index), "buildpack");
|
||||||
assertThat(new File(buildpackDirectory, s)).usingCharset(StandardCharsets.UTF_8).hasContent("[test]");
|
assertThat(new File(buildpackDirectory, s)).usingCharset(StandardCharsets.UTF_8).hasContent("[test]");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user