mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Build: Unified dockerfile for all builds (#59173)
* unified dockerfile for all builds * update builder * include docker commands and output in logs * quiet docker build * quiet gsutil
This commit is contained in:
38
Dockerfile
38
Dockerfile
@@ -1,16 +1,12 @@
|
|||||||
######################## IMPORTANT ########################
|
# syntax=docker/dockerfile:1
|
||||||
#
|
|
||||||
# There are 2 Dockerfiles which must be kept in sync:
|
|
||||||
#
|
|
||||||
# - Dockerfile
|
|
||||||
# - packaging/docker/Dockerfile
|
|
||||||
#
|
|
||||||
###########################################################
|
|
||||||
|
|
||||||
ARG BASE_IMAGE=alpine:3.15
|
ARG BASE_IMAGE=alpine:3.15
|
||||||
ARG JS_IMAGE=node:16-alpine3.15
|
ARG JS_IMAGE=node:16-alpine3.15
|
||||||
ARG GO_IMAGE=golang:1.19.3-alpine3.15
|
ARG GO_IMAGE=golang:1.19.3-alpine3.15
|
||||||
|
|
||||||
|
ARG GO_SRC=go-builder
|
||||||
|
ARG JS_SRC=js-builder
|
||||||
|
|
||||||
FROM ${JS_IMAGE} as js-builder
|
FROM ${JS_IMAGE} as js-builder
|
||||||
|
|
||||||
ENV NODE_OPTIONS=--max_old_space_size=8000
|
ENV NODE_OPTIONS=--max_old_space_size=8000
|
||||||
@@ -56,9 +52,25 @@ COPY public/app/plugins public/app/plugins
|
|||||||
COPY public/api-spec.json public/api-spec.json
|
COPY public/api-spec.json public/api-spec.json
|
||||||
COPY pkg pkg
|
COPY pkg pkg
|
||||||
COPY scripts scripts
|
COPY scripts scripts
|
||||||
|
COPY conf conf
|
||||||
|
|
||||||
RUN make build-go
|
RUN make build-go
|
||||||
|
|
||||||
|
FROM ${BASE_IMAGE} as tgz-builder
|
||||||
|
|
||||||
|
WORKDIR /tmp/grafana
|
||||||
|
|
||||||
|
ARG GRAFANA_TGZ="grafana-latest.linux-x64-musl.tar.gz"
|
||||||
|
|
||||||
|
COPY ${GRAFANA_TGZ} /tmp/grafana.tar.gz
|
||||||
|
|
||||||
|
# add -v to make tar print every file it extracts
|
||||||
|
RUN tar x -z -f /tmp/grafana.tar.gz --strip-components=1
|
||||||
|
|
||||||
|
# helpers for COPY --from
|
||||||
|
FROM ${GO_SRC} as go-src
|
||||||
|
FROM ${JS_SRC} as js-src
|
||||||
|
|
||||||
# Final stage
|
# Final stage
|
||||||
FROM ${BASE_IMAGE}
|
FROM ${BASE_IMAGE}
|
||||||
|
|
||||||
@@ -106,7 +118,7 @@ RUN if grep -i -q alpine /etc/issue && [ `arch` = "x86_64" ]; then \
|
|||||||
rm -f /etc/ld.so.cache; \
|
rm -f /etc/ld.so.cache; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
COPY conf ./conf
|
COPY --from=go-src /tmp/grafana/conf ./conf
|
||||||
|
|
||||||
RUN if [ ! $(getent group "$GF_GID") ]; then \
|
RUN if [ ! $(getent group "$GF_GID") ]; then \
|
||||||
if grep -i -q alpine /etc/issue; then \
|
if grep -i -q alpine /etc/issue; then \
|
||||||
@@ -136,12 +148,14 @@ RUN if [ ! $(getent group "$GF_GID") ]; then \
|
|||||||
chown -R "grafana:$GF_GID_NAME" "$GF_PATHS_DATA" "$GF_PATHS_HOME/.aws" "$GF_PATHS_LOGS" "$GF_PATHS_PLUGINS" "$GF_PATHS_PROVISIONING" && \
|
chown -R "grafana:$GF_GID_NAME" "$GF_PATHS_DATA" "$GF_PATHS_HOME/.aws" "$GF_PATHS_LOGS" "$GF_PATHS_PLUGINS" "$GF_PATHS_PROVISIONING" && \
|
||||||
chmod -R 777 "$GF_PATHS_DATA" "$GF_PATHS_HOME/.aws" "$GF_PATHS_LOGS" "$GF_PATHS_PLUGINS" "$GF_PATHS_PROVISIONING"
|
chmod -R 777 "$GF_PATHS_DATA" "$GF_PATHS_HOME/.aws" "$GF_PATHS_LOGS" "$GF_PATHS_PLUGINS" "$GF_PATHS_PROVISIONING"
|
||||||
|
|
||||||
COPY --from=go-builder /tmp/grafana/bin/*/grafana-server /tmp/grafana/bin/*/grafana-cli /tmp/grafana/bin/*/grafana ./bin/
|
COPY --from=go-src /tmp/grafana/bin/grafana* /tmp/grafana/bin/*/grafana* ./bin/
|
||||||
COPY --from=js-builder /tmp/grafana/public ./public
|
COPY --from=js-src /tmp/grafana/public ./public
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|
||||||
COPY ./packaging/docker/run.sh /run.sh
|
ARG RUN_SH=./packaging/docker/run.sh
|
||||||
|
|
||||||
|
COPY ${RUN_SH} /run.sh
|
||||||
|
|
||||||
USER "$GF_UID"
|
USER "$GF_UID"
|
||||||
ENTRYPOINT [ "/run.sh" ]
|
ENTRYPOINT [ "/run.sh" ]
|
||||||
|
|||||||
2
Makefile
2
Makefile
@@ -158,11 +158,13 @@ shellcheck: $(SH_FILES) ## Run checks for shell scripts.
|
|||||||
|
|
||||||
build-docker-full: ## Build Docker image for development.
|
build-docker-full: ## Build Docker image for development.
|
||||||
@echo "build docker container"
|
@echo "build docker container"
|
||||||
|
DOCKER_BUILDKIT=1 \
|
||||||
docker build \
|
docker build \
|
||||||
--tag grafana/grafana:dev .
|
--tag grafana/grafana:dev .
|
||||||
|
|
||||||
build-docker-full-ubuntu: ## Build Docker image based on Ubuntu for development.
|
build-docker-full-ubuntu: ## Build Docker image based on Ubuntu for development.
|
||||||
@echo "build docker container"
|
@echo "build docker container"
|
||||||
|
DOCKER_BUILDKIT=1 \
|
||||||
docker build \
|
docker build \
|
||||||
--build-arg BASE_IMAGE=ubuntu:20.04 \
|
--build-arg BASE_IMAGE=ubuntu:20.04 \
|
||||||
--build-arg GO_IMAGE=golang:1.19.3 \
|
--build-arg GO_IMAGE=golang:1.19.3 \
|
||||||
|
|||||||
@@ -1,113 +0,0 @@
|
|||||||
######################## IMPORTANT ########################
|
|
||||||
#
|
|
||||||
# There are 2 Dockerfiles which must be kept in sync:
|
|
||||||
#
|
|
||||||
# - Dockerfile
|
|
||||||
# - packaging/docker/Dockerfile
|
|
||||||
#
|
|
||||||
###########################################################
|
|
||||||
|
|
||||||
ARG BASE_IMAGE=alpine:3.15
|
|
||||||
|
|
||||||
FROM ${BASE_IMAGE} as tgz-builder
|
|
||||||
|
|
||||||
WORKDIR /tmp/grafana
|
|
||||||
|
|
||||||
ARG GRAFANA_TGZ="grafana-latest.linux-x64-musl.tar.gz"
|
|
||||||
|
|
||||||
# Make sure we have Gnu tar
|
|
||||||
RUN if grep -i -q alpine /etc/issue; then \
|
|
||||||
apk add --no-cache tar; \
|
|
||||||
fi
|
|
||||||
|
|
||||||
COPY ${GRAFANA_TGZ} /tmp/grafana.tar.gz
|
|
||||||
|
|
||||||
# Change to tar xfzv to make tar print every file it extracts
|
|
||||||
RUN tar xzf /tmp/grafana.tar.gz --strip-components=1 -C /tmp/grafana
|
|
||||||
|
|
||||||
# Final stage
|
|
||||||
FROM ${BASE_IMAGE}
|
|
||||||
|
|
||||||
LABEL maintainer="Grafana Labs <hello@grafana.com>"
|
|
||||||
|
|
||||||
ARG GF_UID="472"
|
|
||||||
ARG GF_GID="0"
|
|
||||||
|
|
||||||
ENV PATH="/usr/share/grafana/bin:$PATH" \
|
|
||||||
GF_PATHS_CONFIG="/etc/grafana/grafana.ini" \
|
|
||||||
GF_PATHS_DATA="/var/lib/grafana" \
|
|
||||||
GF_PATHS_HOME="/usr/share/grafana" \
|
|
||||||
GF_PATHS_LOGS="/var/log/grafana" \
|
|
||||||
GF_PATHS_PLUGINS="/var/lib/grafana/plugins" \
|
|
||||||
GF_PATHS_PROVISIONING="/etc/grafana/provisioning"
|
|
||||||
|
|
||||||
WORKDIR $GF_PATHS_HOME
|
|
||||||
|
|
||||||
# Install dependencies
|
|
||||||
RUN if grep -i -q alpine /etc/issue; then \
|
|
||||||
apk add --no-cache ca-certificates bash tzdata musl-utils && \
|
|
||||||
apk info -vv | sort; \
|
|
||||||
elif grep -i -q ubuntu /etc/issue; then \
|
|
||||||
DEBIAN_FRONTEND=noninteractive && \
|
|
||||||
apt-get update && \
|
|
||||||
apt-get install -y ca-certificates curl tzdata && \
|
|
||||||
apt-get autoremove -y && \
|
|
||||||
rm -rf /var/lib/apt/lists/*; \
|
|
||||||
else \
|
|
||||||
echo 'ERROR: Unsupported base image' && /bin/false; \
|
|
||||||
fi
|
|
||||||
|
|
||||||
# glibc support for alpine x86_64 only
|
|
||||||
RUN if grep -i -q alpine /etc/issue && [ `arch` = "x86_64" ]; then \
|
|
||||||
wget https://github.com/sgerrand/alpine-pkg-glibc/releases/download/2.35-r0/glibc-2.35-r0.apk \
|
|
||||||
-O /tmp/glibc-2.35-r0.apk && \
|
|
||||||
wget https://github.com/sgerrand/alpine-pkg-glibc/releases/download/2.35-r0/glibc-bin-2.35-r0.apk \
|
|
||||||
-O /tmp/glibc-bin-2.35-r0.apk && \
|
|
||||||
apk add --no-cache --allow-untrusted /tmp/glibc-2.35-r0.apk /tmp/glibc-bin-2.35-r0.apk && \
|
|
||||||
rm -f /lib64/ld-linux-x86-64.so.2 && \
|
|
||||||
ln -s /usr/glibc-compat/lib64/ld-linux-x86-64.so.2 /lib64/ld-linux-x86-64.so.2 && \
|
|
||||||
rm -f /tmp/glibc-2.35-r0.apk && \
|
|
||||||
rm -f /tmp/glibc-bin-2.35-r0.apk && \
|
|
||||||
rm -f /lib/ld-linux-x86-64.so.2 && \
|
|
||||||
rm -f /etc/ld.so.cache; \
|
|
||||||
fi
|
|
||||||
|
|
||||||
COPY --from=tgz-builder /tmp/grafana/conf ./conf
|
|
||||||
|
|
||||||
RUN if [ ! $(getent group "$GF_GID") ]; then \
|
|
||||||
if grep -i -q alpine /etc/issue; then \
|
|
||||||
addgroup -S -g $GF_GID grafana; \
|
|
||||||
else \
|
|
||||||
addgroup --system --gid $GF_GID grafana; \
|
|
||||||
fi; \
|
|
||||||
fi && \
|
|
||||||
GF_GID_NAME=$(getent group $GF_GID | cut -d':' -f1) && \
|
|
||||||
mkdir -p "$GF_PATHS_HOME/.aws" && \
|
|
||||||
if grep -i -q alpine /etc/issue; then \
|
|
||||||
adduser -S -u $GF_UID -G "$GF_GID_NAME" grafana; \
|
|
||||||
else \
|
|
||||||
adduser --system --uid $GF_UID --ingroup "$GF_GID_NAME" grafana; \
|
|
||||||
fi && \
|
|
||||||
mkdir -p "$GF_PATHS_PROVISIONING/datasources" \
|
|
||||||
"$GF_PATHS_PROVISIONING/dashboards" \
|
|
||||||
"$GF_PATHS_PROVISIONING/notifiers" \
|
|
||||||
"$GF_PATHS_PROVISIONING/plugins" \
|
|
||||||
"$GF_PATHS_PROVISIONING/access-control" \
|
|
||||||
"$GF_PATHS_PROVISIONING/alerting" \
|
|
||||||
"$GF_PATHS_LOGS" \
|
|
||||||
"$GF_PATHS_PLUGINS" \
|
|
||||||
"$GF_PATHS_DATA" && \
|
|
||||||
cp conf/sample.ini "$GF_PATHS_CONFIG" && \
|
|
||||||
cp conf/ldap.toml /etc/grafana/ldap.toml && \
|
|
||||||
chown -R "grafana:$GF_GID_NAME" "$GF_PATHS_DATA" "$GF_PATHS_HOME/.aws" "$GF_PATHS_LOGS" "$GF_PATHS_PLUGINS" "$GF_PATHS_PROVISIONING" && \
|
|
||||||
chmod -R 777 "$GF_PATHS_DATA" "$GF_PATHS_HOME/.aws" "$GF_PATHS_LOGS" "$GF_PATHS_PLUGINS" "$GF_PATHS_PROVISIONING"
|
|
||||||
|
|
||||||
COPY --from=tgz-builder /tmp/grafana/bin/grafana-server /tmp/grafana/bin/grafana-cli ./bin/
|
|
||||||
COPY --from=tgz-builder /tmp/grafana/public ./public
|
|
||||||
|
|
||||||
EXPOSE 3000
|
|
||||||
|
|
||||||
COPY ./run.sh /run.sh
|
|
||||||
|
|
||||||
USER "$GF_UID"
|
|
||||||
ENTRYPOINT [ "/run.sh" ]
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
set -e
|
|
||||||
|
|
||||||
UBUNTU_BASE=0
|
|
||||||
TAG_SUFFIX=""
|
|
||||||
|
|
||||||
while [ "$1" != "" ]; do
|
|
||||||
case "$1" in
|
|
||||||
"--ubuntu")
|
|
||||||
UBUNTU_BASE=1
|
|
||||||
TAG_SUFFIX="-ubuntu"
|
|
||||||
echo "Ubuntu base image enabled"
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
* )
|
|
||||||
# unknown param causes args to be passed through to $@
|
|
||||||
break
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
_raw_grafana_tag=$1
|
|
||||||
_docker_repo=${2:-grafana/grafana-enterprise}
|
|
||||||
|
|
||||||
if echo "$_raw_grafana_tag" | grep -q "^v"; then
|
|
||||||
_grafana_tag=$(echo "${_raw_grafana_tag}" | cut -d "v" -f 2)
|
|
||||||
elif echo "$_raw_grafana_tag" | grep -q "^main-"; then
|
|
||||||
_grafana_tag="main"
|
|
||||||
else
|
|
||||||
_grafana_tag="${_raw_grafana_tag}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Building and deploying ${_docker_repo}:${_grafana_tag}${TAG_SUFFIX}"
|
|
||||||
|
|
||||||
docker build \
|
|
||||||
--tag "${_docker_repo}:${_grafana_tag}${TAG_SUFFIX}" \
|
|
||||||
--no-cache=true \
|
|
||||||
.
|
|
||||||
|
|
||||||
docker push "${_docker_repo}:${_grafana_tag}${TAG_SUFFIX}"
|
|
||||||
|
|
||||||
if echo "$_raw_grafana_tag" | grep -q "^v" && echo "$_raw_grafana_tag" | grep -qv "beta"; then
|
|
||||||
docker tag "${_docker_repo}:${_grafana_tag}${TAG_SUFFIX}" "${_docker_repo}:latest${TAG_SUFFIX}"
|
|
||||||
docker push "${_docker_repo}:latest${TAG_SUFFIX}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
|
|
||||||
if echo "${_raw_grafana_tag}" | grep -q "^main-" && [ ${UBUNTU_BASE} = "1" ]; then
|
|
||||||
docker tag "${_docker_repo}:${_grafana_tag}${TAG_SUFFIX}" "grafana/grafana-enterprise-dev:${_raw_grafana_tag}"
|
|
||||||
docker push "grafana/grafana-enterprise-dev:${_raw_grafana_tag}"
|
|
||||||
fi
|
|
||||||
@@ -68,11 +68,16 @@ docker_build () {
|
|||||||
grafana_tgz=${GRAFANA_TGZ:-"grafana-latest.linux-${arch}${libc}.tar.gz"}
|
grafana_tgz=${GRAFANA_TGZ:-"grafana-latest.linux-${arch}${libc}.tar.gz"}
|
||||||
tag="${_docker_repo}${repo_arch}:${_grafana_version}${TAG_SUFFIX}"
|
tag="${_docker_repo}${repo_arch}:${_grafana_version}${TAG_SUFFIX}"
|
||||||
|
|
||||||
|
DOCKER_BUILDKIT=1 \
|
||||||
docker build \
|
docker build \
|
||||||
--build-arg BASE_IMAGE=${base_image} \
|
--build-arg BASE_IMAGE=${base_image} \
|
||||||
--build-arg GRAFANA_TGZ=${grafana_tgz} \
|
--build-arg GRAFANA_TGZ=${grafana_tgz} \
|
||||||
|
--build-arg GO_SRC=tgz-builder \
|
||||||
|
--build-arg JS_SRC=tgz-builder \
|
||||||
|
--build-arg RUN_SH=./run.sh \
|
||||||
--tag "${tag}" \
|
--tag "${tag}" \
|
||||||
--no-cache=true \
|
--no-cache=true \
|
||||||
|
--file ../../Dockerfile \
|
||||||
.
|
.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -116,33 +116,49 @@ func BuildImage(version string, arch config.Architecture, grafanaDir string, use
|
|||||||
tags = append(tags, tag)
|
tags = append(tags, tag)
|
||||||
|
|
||||||
args := []string{
|
args := []string{
|
||||||
"build", "--build-arg", fmt.Sprintf("BASE_IMAGE=%s", baseImage),
|
"build",
|
||||||
"--build-arg", fmt.Sprintf("GRAFANA_TGZ=%s", archive), "--tag", tag, "--no-cache",
|
"-q",
|
||||||
|
"--build-arg", fmt.Sprintf("BASE_IMAGE=%s", baseImage),
|
||||||
|
"--build-arg", fmt.Sprintf("GRAFANA_TGZ=%s", archive),
|
||||||
|
"--build-arg", "GO_SRC=tgz-builder",
|
||||||
|
"--build-arg", "JS_SRC=tgz-builder",
|
||||||
|
"--build-arg", "RUN_SH=./run.sh",
|
||||||
|
"--tag", tag,
|
||||||
|
"--no-cache",
|
||||||
|
"--file", "../../Dockerfile",
|
||||||
".",
|
".",
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("Running Docker: docker %s", strings.Join(args, " "))
|
|
||||||
//nolint:gosec
|
//nolint:gosec
|
||||||
cmd := exec.Command("docker", args...)
|
cmd := exec.Command("docker", args...)
|
||||||
cmd.Dir = buildDir
|
cmd.Dir = buildDir
|
||||||
cmd.Env = append(os.Environ(), "DOCKER_CLI_EXPERIMENTAL=enabled")
|
cmd.Stdout = os.Stdout
|
||||||
if output, err := cmd.CombinedOutput(); err != nil {
|
cmd.Stderr = os.Stderr
|
||||||
return []string{}, fmt.Errorf("building Docker image failed: %w\n%s", err, output)
|
cmd.Env = append(os.Environ(), "DOCKER_CLI_EXPERIMENTAL=enabled", "DOCKER_BUILDKIT=1")
|
||||||
|
log.Printf("Running Docker: DOCKER_CLI_EXPERIMENTAL=enabled DOCKER_BUILDKIT=1 %s", cmd)
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
return []string{}, fmt.Errorf("building Docker image failed: %w", err)
|
||||||
}
|
}
|
||||||
if shouldSave {
|
if shouldSave {
|
||||||
imageFile := fmt.Sprintf("%s-%s%s-%s.img", imageFileBase, version, tagSuffix, arch)
|
imageFile := fmt.Sprintf("%s-%s%s-%s.img", imageFileBase, version, tagSuffix, arch)
|
||||||
//nolint:gosec
|
//nolint:gosec
|
||||||
cmd = exec.Command("docker", "save", tag, "-o", imageFile)
|
cmd = exec.Command("docker", "save", tag, "-o", imageFile)
|
||||||
cmd.Dir = buildDir
|
cmd.Dir = buildDir
|
||||||
if output, err := cmd.CombinedOutput(); err != nil {
|
cmd.Stdout = os.Stdout
|
||||||
return []string{}, fmt.Errorf("saving Docker image failed: %w\n%s", err, output)
|
cmd.Stderr = os.Stderr
|
||||||
|
log.Printf("Running Docker: %s", cmd)
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
return []string{}, fmt.Errorf("saving Docker image failed: %w", err)
|
||||||
}
|
}
|
||||||
gcsURL := fmt.Sprintf("gs://grafana-prerelease/artifacts/docker/%s/%s", version, imageFile)
|
gcsURL := fmt.Sprintf("gs://grafana-prerelease/artifacts/docker/%s/%s", version, imageFile)
|
||||||
//nolint:gosec
|
//nolint:gosec
|
||||||
cmd = exec.Command("gsutil", "cp", imageFile, gcsURL)
|
cmd = exec.Command("gsutil", "-q", "cp", imageFile, gcsURL)
|
||||||
cmd.Dir = buildDir
|
cmd.Dir = buildDir
|
||||||
if output, err := cmd.CombinedOutput(); err != nil {
|
cmd.Stdout = os.Stdout
|
||||||
return []string{}, fmt.Errorf("storing Docker image failed: %w\n%s", err, output)
|
cmd.Stderr = os.Stderr
|
||||||
|
log.Printf("Running gsutil: %s", cmd)
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
return []string{}, fmt.Errorf("storing Docker image failed: %w", err)
|
||||||
}
|
}
|
||||||
log.Printf("Docker image %s stored to grafana-prerelease GCS bucket", imageFile)
|
log.Printf("Docker image %s stored to grafana-prerelease GCS bucket", imageFile)
|
||||||
}
|
}
|
||||||
@@ -152,8 +168,11 @@ func BuildImage(version string, arch config.Architecture, grafanaDir string, use
|
|||||||
//nolint:gosec
|
//nolint:gosec
|
||||||
cmd = exec.Command("docker", "tag", tag, additionalTag)
|
cmd = exec.Command("docker", "tag", tag, additionalTag)
|
||||||
cmd.Dir = buildDir
|
cmd.Dir = buildDir
|
||||||
if output, err := cmd.CombinedOutput(); err != nil {
|
cmd.Stdout = os.Stdout
|
||||||
return []string{}, fmt.Errorf("tagging Docker image failed: %w\n%s", err, output)
|
cmd.Stderr = os.Stderr
|
||||||
|
log.Printf("Running Docker: %s", cmd)
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
return []string{}, fmt.Errorf("tagging Docker image failed: %w", err)
|
||||||
}
|
}
|
||||||
tags = append(tags, additionalTag)
|
tags = append(tags, additionalTag)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user