add gitlab section
This commit is contained in:
parent
e96e220869
commit
0d5f9eff7e
32
gitlab/cicd/common-workflows.yml
Normal file
32
gitlab/cicd/common-workflows.yml
Normal file
@ -0,0 +1,32 @@
|
||||
workflow:
|
||||
rules:
|
||||
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
||||
when: never
|
||||
# NO DEPLOYMENT ON CLASSICS BRANCH
|
||||
- if: '$CI_COMMIT_REF_PROTECTED == "false"'
|
||||
variables:
|
||||
ENVIRONMENT: "null"
|
||||
ENVIRONMENT_SHORT: "null"
|
||||
IMAGE_TAG: $CI_COMMIT_REF_SLUG-$CI_COMMIT_SHORT_SHA
|
||||
CONTAINER_IMAGE: $CI_REGISTRY_IMAGE:$IMAGE_TAG
|
||||
# TESTING - develop only
|
||||
- if: '$CI_COMMIT_TAG == null && $CI_COMMIT_REF_NAME == "develop" && $CI_COMMIT_REF_PROTECTED == "true"'
|
||||
variables:
|
||||
ENVIRONMENT: testing
|
||||
ENVIRONMENT_SHORT: tst
|
||||
IMAGE_TAG: $CI_COMMIT_REF_SLUG-$CI_COMMIT_SHORT_SHA
|
||||
CONTAINER_IMAGE: $CI_REGISTRY_IMAGE:$IMAGE_TAG
|
||||
# STAGING - main/master only
|
||||
- if: '($CI_COMMIT_REF_NAME == "main" || $CI_COMMIT_REF_NAME == "master") && $CI_COMMIT_REF_PROTECTED == "true"'
|
||||
variables:
|
||||
ENVIRONMENT: staging
|
||||
ENVIRONMENT_SHORT: stg
|
||||
IMAGE_TAG: $CI_COMMIT_REF_SLUG-$CI_COMMIT_SHORT_SHA
|
||||
CONTAINER_IMAGE: $CI_REGISTRY_IMAGE:$IMAGE_TAG
|
||||
# PRODUCTION - tags only
|
||||
- if: '$CI_COMMIT_TAG && $CI_COMMIT_REF_PROTECTED == "true"'
|
||||
variables:
|
||||
ENVIRONMENT: production
|
||||
ENVIRONMENT_SHORT: prd
|
||||
IMAGE_TAG: $CI_COMMIT_TAG
|
||||
CONTAINER_IMAGE: $CI_REGISTRY_IMAGE:$IMAGE_TAG
|
||||
108
gitlab/cicd/docker-k8s-utils.yml
Normal file
108
gitlab/cicd/docker-k8s-utils.yml
Normal file
@ -0,0 +1,108 @@
|
||||
stages:
|
||||
- init
|
||||
- build
|
||||
- scan
|
||||
- deploy
|
||||
|
||||
# ###########################################
|
||||
# Configuration section
|
||||
# ###########################################
|
||||
.helminit: &helminit
|
||||
before_script:
|
||||
- echo "Adding Helm repository..."
|
||||
- helm repo add --username $CI_REGISTRY_USER --password $CI_JOB_TOKEN helm-charts $CI_API_V4_URL/projects/645/packages/helm/stable
|
||||
- helm repo update
|
||||
- echo "Validating Helm dependencies..."
|
||||
- helm dependency update ./helm
|
||||
|
||||
# ###########################################
|
||||
# Build section
|
||||
# ###########################################
|
||||
.build_template: &build_template
|
||||
image:
|
||||
name: moby/buildkit:v0.21.0
|
||||
entrypoint: [""]
|
||||
variables:
|
||||
DOCKER_BUILDKIT: 1
|
||||
before_script:
|
||||
- echo "Preparing BuildKit environment..."
|
||||
- mkdir -p /root/.docker
|
||||
- echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_REGISTRY_PASSWORD\"}}}" > /root/.docker/config.json
|
||||
- echo "Initializing build arguments..."
|
||||
- |
|
||||
for VARNAME in $BUILD_ARGS; do
|
||||
VALUE=$(eval echo \$$VARNAME)
|
||||
BUILDPARAMS="$BUILDPARAMS --opt build-arg:$VARNAME=$VALUE"
|
||||
done
|
||||
echo "Build parameters: $BUILDPARAMS"
|
||||
script:
|
||||
- echo "Validating build context..."
|
||||
- ls -la $CI_PROJECT_DIR
|
||||
- echo "Starting BuildKit build with the following settings:"
|
||||
- |
|
||||
buildctl-daemonless.sh build --frontend dockerfile.v0 \
|
||||
--local context=. \
|
||||
--local dockerfile=. \
|
||||
--output type=image,name=${BUILD_IMAGE_DESTINATION},push=true \
|
||||
--export-cache type=registry,ref=${CI_REGISTRY_IMAGE}/cache,image-manifest=true \
|
||||
--import-cache type=registry,ref=${CI_REGISTRY_IMAGE}/cache \
|
||||
--opt target=${BUILD_TARGET} \
|
||||
${BUILDPARAMS}
|
||||
|
||||
.build:image:
|
||||
<<: *build_template
|
||||
variables:
|
||||
BUILD_IMAGE_DESTINATION: $CONTAINER_IMAGE
|
||||
BUILD_TARGET:
|
||||
BUILD_ARGS:
|
||||
|
||||
# ###########################################
|
||||
# Deploy section
|
||||
# ###########################################
|
||||
.deploy:k8s:
|
||||
image: alpine/helm:3.21.0
|
||||
variables:
|
||||
HELM_CUSTOM_ARGS: ""
|
||||
NAMESPACE: tests
|
||||
HPA_REPLICAS: 1
|
||||
HPA_MAXREPLICAS: 1
|
||||
HELM_NAME: $CI_PROJECT_NAME
|
||||
INJECT_ENVVARS_GITLAB: "false"
|
||||
VALUES_ENVVARS_PATH: symfonyLib.phpfpm.envVars.
|
||||
before_script:
|
||||
- !reference [".helminit", before_script]
|
||||
- |
|
||||
if [[ "$INJECT_ENVVARS_GITLAB" == "true" ]]; then
|
||||
echo "Injecting environment variables into Helm Chart..."
|
||||
for VARNAME in $(env); do
|
||||
if [[ $(echo $VARNAME | egrep '^ENV_') ]]; then
|
||||
NAME=$(echo "$VARNAME" | cut -d"=" -f1 | sed "s/ENV_/$VALUES_ENVVARS_PATH/")
|
||||
VAR=$(echo "$VARNAME" | cut -d"=" -f2-)
|
||||
echo -e $NAME
|
||||
ENVVARS="${ENVVARS} --set $NAME=$VAR"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
script:
|
||||
- echo "Validating Helm chart..."
|
||||
- helm lint ./helm # Validation du chart Helm avant déploiement
|
||||
- echo "Deploying to $ENVIRONMENT k8s cluster in $NAMESPACE namespace..."
|
||||
- |
|
||||
helm upgrade --install --namespace $NAMESPACE -f ./helm/values.yaml $HELM_CUSTOM_ARGS \
|
||||
--set global.app.env="$ENVIRONMENT_SHORT" \
|
||||
--set global.replica.replicaCount="$HPA_REPLICAS" \
|
||||
--set global.replica.maxReplicaCount="$HPA_MAXREPLICAS" \
|
||||
--set global.app.version="$IMAGE_TAG" \
|
||||
--set global.namespace="$NAMESPACE" \
|
||||
--set global.app.revision="$CI_COMMIT_SHORT_SHA" \
|
||||
--set symfonyLib.phpfpm.image="$CI_REGISTRY_IMAGE" \
|
||||
--set pythonLib.python.image="$CI_REGISTRY_IMAGE" \
|
||||
$ENVVARS $HELM_NAME ./helm
|
||||
- if [ $? -eq 0 ]; then touch success; fi
|
||||
after_script:
|
||||
- |
|
||||
if [ -f 'success' ] && [ "$ENVIRONMENT" == 'production' ]; then
|
||||
echo 'Sending notification to Teams webhook...'
|
||||
apk add curl
|
||||
curl -H 'Content-Type: application/json' -d "{\"text\": \"[prd] [job/$CI_JOB_NAME] [$CI_PROJECT_NAME] [$CI_COMMIT_REF_NAME] [$CI_PIPELINE_URL] completed!\"}" $TEAMS_WEBHOOK
|
||||
fi
|
||||
141
gitlab/cicd/go.yml
Normal file
141
gitlab/cicd/go.yml
Normal file
@ -0,0 +1,141 @@
|
||||
include:
|
||||
- project: $TEMPLATES_PROJECT_NAME
|
||||
file: 'docker-k8s-utils.yml'
|
||||
ref: $TEMPLATES_DEFAULT_BRANCH
|
||||
- project: $TEMPLATES_PROJECT_NAME
|
||||
file: 'utils.yml'
|
||||
ref: $TEMPLATES_DEFAULT_BRANCH
|
||||
|
||||
workflow:
|
||||
rules:
|
||||
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
||||
when: never
|
||||
# NO DEPLOYMENT
|
||||
- if: '$CI_COMMIT_TAG == null'
|
||||
variables:
|
||||
IMAGE_TAG: $CI_COMMIT_REF_SLUG-$CI_COMMIT_SHORT_SHA
|
||||
CONTAINER_IMAGE: $CI_REGISTRY_IMAGE:$IMAGE_TAG
|
||||
changes:
|
||||
- app/*
|
||||
- app/*/*
|
||||
- Dockerfile
|
||||
- .gitlab-ci.yml
|
||||
# STAGING/PRODUCTION - tags only
|
||||
- if: '$CI_COMMIT_TAG =~ /(alpha)|(beta)|(rc)/'
|
||||
variables:
|
||||
IMAGE_TAG: $CI_COMMIT_TAG
|
||||
CONTAINER_IMAGE: $CI_REGISTRY_IMAGE:$IMAGE_TAG
|
||||
ENVIRONMENT: 'staging'
|
||||
ENVIRONMENT_SHORT: 'stg'
|
||||
- if: '$CI_COMMIT_TAG'
|
||||
variables:
|
||||
IMAGE_TAG: $CI_COMMIT_TAG
|
||||
CONTAINER_IMAGE: $CI_REGISTRY_IMAGE:$IMAGE_TAG
|
||||
ENVIRONMENT: 'production'
|
||||
ENVIRONMENT_SHORT: 'prd'
|
||||
|
||||
variables:
|
||||
SRC_ROOT_PATH: "./app"
|
||||
VAULT_ADDR: https://vault.example.com
|
||||
|
||||
stages:
|
||||
- lint
|
||||
- build
|
||||
- test
|
||||
- release
|
||||
|
||||
variables:checker:
|
||||
extends: .variables:checker
|
||||
stage: .pre
|
||||
variables:
|
||||
VARS_TO_CHECK_APP: ""
|
||||
VARS_TO_CHECK_SYSTEM: SRC_ROOT_PATH CONTAINER_IMAGE
|
||||
|
||||
# ###########################################
|
||||
# Lint
|
||||
# ###########################################
|
||||
|
||||
linter:
|
||||
image: golangci/golangci-lint:v2.0.2-alpine
|
||||
rules:
|
||||
- changes:
|
||||
- .gitlab-ci.yml
|
||||
- app/*
|
||||
- app/*/*
|
||||
when: always
|
||||
stage: lint
|
||||
before_script:
|
||||
- cd $SRC_ROOT_PATH
|
||||
script:
|
||||
- go mod tidy
|
||||
- golangci-lint run --output.code-climate.path gl-code-quality-report.json --path-prefix /app/
|
||||
artifacts:
|
||||
reports:
|
||||
codequality: $SRC_ROOT_PATH/gl-code-quality-report.json
|
||||
build:application_test:
|
||||
extends: .build:image
|
||||
stage: build
|
||||
variables:
|
||||
BUILD_TARGET: dev
|
||||
BUILD_IMAGE_DESTINATION: ${CONTAINER_IMAGE}_dev
|
||||
BUILD_ARGS: ""
|
||||
|
||||
#######################################
|
||||
# TEST
|
||||
#######################################
|
||||
|
||||
go:tests:
|
||||
image: ${CONTAINER_IMAGE}_dev
|
||||
stage: test
|
||||
before_script:
|
||||
- cd $SRC_ROOT_PATH
|
||||
- go mod tidy
|
||||
- go install github.com/jstemmer/go-junit-report@latest
|
||||
- go install github.com/boumenot/gocover-cobertura@latest
|
||||
script:
|
||||
- echo "Execute tests"
|
||||
- go test ./... -v | go-junit-report > report-gotest.xml
|
||||
- echo "Compute coverage"
|
||||
- go test ./... -v -coverprofile=coverage.txt -covermode count
|
||||
- gocover-cobertura < coverage.txt > report-coverage.xml
|
||||
coverage: '/^coverage: \d+.\d+% of statements/'
|
||||
artifacts:
|
||||
reports:
|
||||
junit: $SRC_ROOT_PATH/report-gotest.xml
|
||||
coverage_report:
|
||||
coverage_format: cobertura
|
||||
path: $SRC_ROOT_PATH/report-coverage.xml
|
||||
|
||||
#######################################
|
||||
# BUILD
|
||||
# #####################################
|
||||
|
||||
build:image:
|
||||
extends: .build:image
|
||||
stage: build
|
||||
rules:
|
||||
- if: '$CI_COMMIT_TAG'
|
||||
when: on_success
|
||||
variables:
|
||||
BUILD_TARGET: prod
|
||||
BUILD_IMAGE_DESTINATION: $CONTAINER_IMAGE
|
||||
BUILD_ARGS: ""
|
||||
|
||||
# ###########################################
|
||||
# Release
|
||||
# ###########################################
|
||||
release:image:
|
||||
image: hashicorp/vault:1.19.3
|
||||
stage: release
|
||||
environment: $ENVIRONMENT
|
||||
before_script:
|
||||
- echo "Authentification Vault"
|
||||
- export VAULT_TOKEN=$(vault write -field=token auth/approle/login role_id=$VAULT_ROLE_ID secret_id=$VAULT_SECRET_ID)
|
||||
script:
|
||||
- echo "Push last tag on Vault"
|
||||
- vault kv put app/<myApp>/$ENVIRONMENT_SHORT/image_latest name=$CONTAINER_IMAGE
|
||||
needs:
|
||||
- build:image
|
||||
rules:
|
||||
- if: '$CI_COMMIT_TAG'
|
||||
when: on_success
|
||||
179
gitlab/cicd/php-symfony.yml
Normal file
179
gitlab/cicd/php-symfony.yml
Normal file
@ -0,0 +1,179 @@
|
||||
# ###########################################
|
||||
# Security section
|
||||
# ###########################################
|
||||
.security:advisories:
|
||||
image: <imageUrl>/docker/local-php-security-checker:1.0.0
|
||||
allow_failure: true
|
||||
script:
|
||||
- cd $SRC_ROOT_PATH
|
||||
- local-php-security-checker --format=junit src > ./report-security-checker.xml
|
||||
artifacts:
|
||||
reports:
|
||||
junit: $SRC_ROOT_PATH/report-security-checker.xml
|
||||
|
||||
# ###########################################
|
||||
# Qualimetry section
|
||||
# ###########################################
|
||||
.php:lint:
|
||||
image: jakzal/phpqa:php${PHP_VERSION}${JAKZAL_PHP_VERSION}-alpine
|
||||
script:
|
||||
- cd $SRC_ROOT_PATH
|
||||
- parallel-lint . --exclude vendor
|
||||
|
||||
.php:checkstyle:
|
||||
image: jakzal/phpqa:php${PHP_VERSION}${JAKZAL_PHP_VERSION}-alpine
|
||||
script:
|
||||
- cd $SRC_ROOT_PATH
|
||||
- phpcs --extensions=php -n --standard=PSR12 src tests --report=junit > ./report-phpcs.xml
|
||||
artifacts:
|
||||
reports:
|
||||
junit: $SRC_ROOT_PATH/report-phpcs.xml
|
||||
|
||||
.php:stan:
|
||||
image: jakzal/phpqa:php${PHP_VERSION}${JAKZAL_PHP_VERSION}-alpine
|
||||
variables:
|
||||
STAN_LEVEL: 5
|
||||
script:
|
||||
- echo ${STAN_LEVEL}
|
||||
- cd $SRC_ROOT_PATH
|
||||
- phpstan analyse src --level=$STAN_LEVEL --no-progress --error-format=junit > ./report-phpstan.xml
|
||||
artifacts:
|
||||
reports:
|
||||
junit: $SRC_ROOT_PATH/report-phpstan.xml
|
||||
|
||||
# ###########################################
|
||||
# Tests section
|
||||
# ###########################################
|
||||
.test:unit:
|
||||
image: $CONTAINER_IMAGE
|
||||
variables:
|
||||
APP_ENV: test
|
||||
XDEBUG_MODE: coverage
|
||||
before_script:
|
||||
- install-php-extensions xdebug-stable
|
||||
- cp ./docker/env/.env.test $SRC_ROOT_PATH/.env
|
||||
- cd $SRC_ROOT_PATH
|
||||
- composer install --prefer-dist --no-progress --no-interaction
|
||||
- bin/console cache:warmup --env=test
|
||||
script:
|
||||
- vendor/bin/simple-phpunit --coverage-text --colors=never --log-junit ./report-phpunit.xml
|
||||
coverage: '/^\s*Lines:\s*\d+.\d+\%/'
|
||||
artifacts:
|
||||
reports:
|
||||
junit: $SRC_ROOT_PATH/report-phpunit.xml
|
||||
|
||||
.test:phpunit:
|
||||
image: ${CONTAINER_IMAGE}_dev
|
||||
variables:
|
||||
APP_ENV: test
|
||||
XDEBUG_MODE: coverage
|
||||
GIT_STRATEGY: none
|
||||
SRC_ROOT_PATH: /app
|
||||
before_script:
|
||||
- cd $SRC_ROOT_PATH
|
||||
# Composer setup
|
||||
- composer install --prefer-dist --no-progress --no-interaction
|
||||
- composer dump-autoload --classmap-authoritative
|
||||
- composer dump-env test
|
||||
- composer run-script post-install-cmd
|
||||
script:
|
||||
- cd $SRC_ROOT_PATH
|
||||
- ls -la
|
||||
- vendor/bin/simple-phpunit --coverage-text --colors=never --log-junit="${CI_PROJECT_DIR}"/report-phpunit.xml
|
||||
coverage: '/^\s*Lines:\s*\d+.\d+\%/'
|
||||
artifacts:
|
||||
reports:
|
||||
junit: ./report-phpunit.xml
|
||||
|
||||
.test:phpunit:standalone:
|
||||
image: ${CONTAINER_IMAGE}_dev
|
||||
variables:
|
||||
APP_ENV: test
|
||||
XDEBUG_MODE: coverage
|
||||
GIT_STRATEGY: none
|
||||
SRC_ROOT_PATH: /app
|
||||
script:
|
||||
- cd $SRC_ROOT_PATH
|
||||
- vendor/bin/phpunit --coverage-text --colors=never --log-junit="${CI_PROJECT_DIR}"/report-phpunit.xml
|
||||
coverage: '/^\s*Lines:\s*\d+.\d+\%/'
|
||||
artifacts:
|
||||
reports:
|
||||
junit: ./report-phpunit.xml
|
||||
|
||||
.test:codeception:
|
||||
image: ${CONTAINER_IMAGE}_dev
|
||||
variables:
|
||||
APP_ENV: test
|
||||
XDEBUG_MODE: coverage
|
||||
GIT_STRATEGY: none
|
||||
SRC_ROOT_PATH: /app
|
||||
before_script:
|
||||
- cd $SRC_ROOT_PATH
|
||||
- sed -i 's#http://web#http://127.0.0.1:80#g' $SRC_ROOT_PATH/tests/api.suite.yml
|
||||
# Composer setup
|
||||
- composer install --prefer-dist --no-progress --no-interaction
|
||||
- composer dump-autoload --classmap-authoritative
|
||||
- composer dump-env test
|
||||
- composer run-script post-install-cmd
|
||||
script:
|
||||
- cd $SRC_ROOT_PATH && symfony local:server:start --port=80 --no-tls -d
|
||||
- $SRC_ROOT_PATH/vendor/bin/codecept run --xml --coverage --coverage-text --no-colors && true; exit_code=$?
|
||||
- symfony local:server:stop
|
||||
- exit $exit_code
|
||||
after_script:
|
||||
# Copy directory to CI dir for artifacts
|
||||
- cp -r $SRC_ROOT_PATH/tests $CI_PROJECT_DIR/
|
||||
coverage: '/^\s*Lines:\s*\d+.\d+\%/'
|
||||
artifacts:
|
||||
expire_in: 1 day
|
||||
when: always
|
||||
paths:
|
||||
- ./tests/_build/output/
|
||||
reports:
|
||||
junit: ./tests/_build/output/report.xml
|
||||
|
||||
.test:codeception:noweb:
|
||||
extends: .test:codeception
|
||||
before_script:
|
||||
- cd $SRC_ROOT_PATH
|
||||
# Composer setup
|
||||
- composer install --prefer-dist --no-progress --no-interaction
|
||||
- composer dump-autoload --classmap-authoritative
|
||||
- composer dump-env test
|
||||
- composer run-script post-install-cmd
|
||||
script:
|
||||
- cd $SRC_ROOT_PATH
|
||||
- $SRC_ROOT_PATH/vendor/bin/codecept run --xml --coverage --coverage-text --no-colors && true; exit_code=$?
|
||||
- exit $exit_code
|
||||
|
||||
.test:codeception5:
|
||||
image: ${CONTAINER_IMAGE}_dev
|
||||
variables:
|
||||
APP_ENV: test
|
||||
XDEBUG_MODE: coverage
|
||||
GIT_STRATEGY: none
|
||||
SRC_ROOT_PATH: /app
|
||||
before_script:
|
||||
- cd $SRC_ROOT_PATH
|
||||
- sed -i 's#http://web#http://127.0.0.1:80#g' $SRC_ROOT_PATH/tests/Api.suite.yml
|
||||
# Composer setup
|
||||
- composer install --prefer-dist --no-progress --no-interaction
|
||||
- composer dump-autoload --classmap-authoritative
|
||||
- composer dump-env test
|
||||
- composer run-script post-install-cmd
|
||||
script:
|
||||
- cd $SRC_ROOT_PATH && symfony local:server:start --port=80 --no-tls -d
|
||||
- $SRC_ROOT_PATH/vendor/bin/codecept run --xml --coverage --coverage-text --no-colors && true; exit_code=$?
|
||||
- symfony local:server:stop
|
||||
- exit $exit_code
|
||||
after_script:
|
||||
# Copy directory to CI dir for artifacts
|
||||
- cp -r $SRC_ROOT_PATH/tests $CI_PROJECT_DIR/
|
||||
coverage: '/^\s*Lines:\s*\d+.\d+\%/'
|
||||
artifacts:
|
||||
expire_in: 1 day
|
||||
when: always
|
||||
paths:
|
||||
- ./tests/_output/
|
||||
reports:
|
||||
junit: ./tests/_output/report.xml
|
||||
29
gitlab/cicd/utils.yml
Normal file
29
gitlab/cicd/utils.yml
Normal file
@ -0,0 +1,29 @@
|
||||
# Check that variables exists
|
||||
.variables:checker:
|
||||
variables:
|
||||
VARS_TO_CHECK_SYSTEM: SRC_ROOT_PATH CONTAINER_IMAGE ENVIRONMENT ENVIRONMENT_SHORT
|
||||
VARS_TO_CHECK_APP:
|
||||
VARS_TO_CHECK: $VARS_TO_CHECK_SYSTEM $VARS_TO_CHECK_APP
|
||||
script:
|
||||
- echo "Checking of the existence of some variables..."
|
||||
- |
|
||||
checkVariables () {
|
||||
TXT_RED="\e[91m" && TXT_GREEN="\e[92m" && TXT_CLEAR="\e[0m"
|
||||
inError=0
|
||||
|
||||
for var in "$@"
|
||||
do
|
||||
echo -ne "\$${var} : "
|
||||
if [ -z "${!var}" ]; then
|
||||
echo -e "\xE2\x9D\x8C ${TXT_RED}not defined${TXT_CLEAR}";
|
||||
inError=1;
|
||||
else
|
||||
echo -e "\xE2\x9C\x94 ${TXT_GREEN}defined${TXT_CLEAR}";
|
||||
fi
|
||||
done
|
||||
|
||||
return $inError
|
||||
}
|
||||
|
||||
checkVariables $VARS_TO_CHECK
|
||||
exit $?
|
||||
Loading…
x
Reference in New Issue
Block a user