mirror of
https://github.com/clastix/kamaji.git
synced 2026-02-23 22:34:01 +00:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b5a7ff6e6c | ||
|
|
9f937a1eec | ||
|
|
0ae3659949 | ||
|
|
736fbf0505 | ||
|
|
8dc0672718 | ||
|
|
27f598fbfc | ||
|
|
d3603c7187 | ||
|
|
83797fc0b3 | ||
|
|
517a4a3458 | ||
|
|
649cf0c852 | ||
|
|
741090f4e6 | ||
|
|
6e8a86d975 | ||
|
|
21b01fae9d | ||
|
|
a0cd4591a9 | ||
|
|
f757c5a5aa | ||
|
|
b15a764381 | ||
|
|
8d3dcdf467 |
2
Makefile
2
Makefile
@@ -3,7 +3,7 @@
|
||||
# To re-generate a bundle for another specific version without changing the standard setup, you can:
|
||||
# - use the VERSION as arg of the bundle target (e.g make bundle VERSION=0.0.2)
|
||||
# - use environment variables to overwrite this value (e.g export VERSION=0.0.2)
|
||||
VERSION ?= 0.2.1
|
||||
VERSION ?= 0.2.3
|
||||
|
||||
# CHANNELS define the bundle channels used in the bundle.
|
||||
# Add a new line here if you would like to change its default config. (E.g CHANNELS = "candidate,fast,stable")
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
<img src="https://img.shields.io/github/go-mod/go-version/clastix/kamaji"/>
|
||||
<a href="https://github.com/clastix/kamaji/releases">
|
||||
<img src="https://img.shields.io/github/v/release/clastix/kamaji"/>
|
||||
<img src="https://goreportcard.com/badge/github.com/clastix/kamaji">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
apiVersion: v2
|
||||
appVersion: v0.2.1
|
||||
appVersion: v0.2.3
|
||||
description: Kamaji is a tool aimed to build and operate a Managed Kubernetes Service
|
||||
with a fraction of the operational burden. With Kamaji, you can deploy and operate
|
||||
hundreds of Kubernetes clusters as a hyper-scaler.
|
||||
@@ -17,7 +17,7 @@ name: kamaji
|
||||
sources:
|
||||
- https://github.com/clastix/kamaji
|
||||
type: application
|
||||
version: 0.11.3
|
||||
version: 0.11.5
|
||||
annotations:
|
||||
catalog.cattle.io/certified: partner
|
||||
catalog.cattle.io/release-name: kamaji
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# kamaji
|
||||
|
||||
  
|
||||
  
|
||||
|
||||
Kamaji is a tool aimed to build and operate a Managed Kubernetes Service with a fraction of the operational burden. With Kamaji, you can deploy and operate hundreds of Kubernetes clusters as a hyper-scaler.
|
||||
|
||||
|
||||
@@ -2304,7 +2304,7 @@ spec:
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: spec.serviceAccountName
|
||||
image: clastix/kamaji:v0.2.1
|
||||
image: clastix/kamaji:v0.2.3
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
|
||||
@@ -13,4 +13,4 @@ kind: Kustomization
|
||||
images:
|
||||
- name: controller
|
||||
newName: clastix/kamaji
|
||||
newTag: v0.2.1
|
||||
newTag: v0.2.3
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
apiVersion: kamaji.clastix.io/v1alpha1
|
||||
kind: TenantControlPlane
|
||||
metadata:
|
||||
name: test
|
||||
name: k8s-126
|
||||
spec:
|
||||
controlPlane:
|
||||
deployment:
|
||||
replicas: 1
|
||||
replicas: 2
|
||||
service:
|
||||
serviceType: LoadBalancer
|
||||
kubernetes:
|
||||
version: "v1.25.4"
|
||||
version: "v1.26.0"
|
||||
kubelet:
|
||||
cgroupfs: cgroupfs
|
||||
admissionControllers:
|
||||
- ResourceQuota
|
||||
- LimitRanger
|
||||
cgroupfs: systemd
|
||||
networkProfile:
|
||||
port: 6443
|
||||
addons:
|
||||
coreDNS: {}
|
||||
kubeProxy: {}
|
||||
konnectivity:
|
||||
server:
|
||||
port: 8132
|
||||
|
||||
@@ -41,11 +41,7 @@ Please, split changes into several and documented small commits: this will help
|
||||
## Code convention
|
||||
|
||||
Kamaji is written in Golang. The changes must follow the Pull Request method where a _GitHub Action_ will
|
||||
check the `golangci-lint`, so ensure your changes respect the coding standard.
|
||||
|
||||
### golint
|
||||
|
||||
You can easily check them issuing the _Make_ recipe `golint`.
|
||||
check the `golangci-lint`, so ensure your changes respect the coding standard. You can easily check them issuing the _Make_ recipe `golint`.
|
||||
|
||||
```
|
||||
# make golint
|
||||
@@ -54,10 +50,10 @@ golangci-lint run -c .golangci.yml
|
||||
|
||||
> Enabled linters and related options are defined in the [.golanci.yml file](https://github.com/clastix/Kamaji/blob/master/.golangci.yml)
|
||||
|
||||
Please, add a new single line at end of any file as the current coding style.
|
||||
|
||||
## Finding contributions to work on
|
||||
Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the
|
||||
default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted'
|
||||
and 'good first issue' issues are a great place to start.
|
||||
Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' and 'good first issue' issues are a great place to start.
|
||||
|
||||
## Design Docs
|
||||
|
||||
@@ -74,10 +70,31 @@ When filing an issue, please check existing open, or recently closed, issues to
|
||||
* Any modifications you've made relevant to the bug
|
||||
* Anything unusual about your environment or deployment
|
||||
|
||||
## Miscellanea
|
||||
## Governance
|
||||
|
||||
This document lays out the guidelines under which the Kamaji project will be governed.
|
||||
The goal is to make sure that the roles and responsibilities are well defined and clarify how decisions are made.
|
||||
|
||||
### Roles
|
||||
|
||||
In the context of Kamaji project, we consider the following roles:
|
||||
|
||||
* __Users__: everyone using Kamaji, typically willing to provide feedback by proposing features and/or filing issues.
|
||||
|
||||
* __Contributors__: everyone contributing code, documentation, examples, tests, and participating in feature proposals as well as design discussions.
|
||||
|
||||
* __Maintainers__: are responsible for engaging with and assisting contributors to iterate on the contributions until it reaches acceptable quality. Maintainers can decide whether the contributions can be accepted into the project or rejected.
|
||||
|
||||
### Release Management
|
||||
|
||||
The release process will be governed by Maintainers.
|
||||
|
||||
### Roadmap Planning
|
||||
|
||||
Maintainers will share roadmap and release versions as milestones in GitHub [project's page](https://github.com/clastix/kamaji).
|
||||
|
||||
Please, add a new single line at end of any file as the current coding style.
|
||||
|
||||
## Licensing
|
||||
|
||||
See the [LICENSE](https://github.com/clastix/Kamaji/blob/master/LICENSE) file for our project's licensing. We can ask you to confirm the licensing of your contribution.
|
||||
See the [LICENSE](https://github.com/clastix/Kamaji/blob/master/LICENSE) file for our project's licensing. We can ask you to confirm the licensing of your contribution.
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
# Governance
|
||||
|
||||
This document lays out the guidelines under which the Kamaji project will be governed.
|
||||
The goal is to make sure that the roles and responsibilities are well defined and clarify how decisions are made.
|
||||
|
||||
## Roles
|
||||
|
||||
In the context of Kamaji project, we consider the following roles:
|
||||
|
||||
* __Users__: everyone using Kamaji, typically willing to provide feedback by proposing features and/or filing issues.
|
||||
|
||||
* __Contributors__: everyone contributing code, documentation, examples, tests, and participating in feature proposals as well as design discussions.
|
||||
|
||||
* __Maintainers__: are responsible for engaging with and assisting contributors to iterate on the contributions until it reaches acceptable quality. Maintainers can decide whether the contributions can be accepted into the project or rejected.
|
||||
|
||||
## Release Management
|
||||
|
||||
The release process will be governed by Maintainers.
|
||||
|
||||
## Roadmap Planning
|
||||
|
||||
Maintainers will share roadmap and release versions as milestones in GitHub [project's page](https://github.com/clastix/kamaji).
|
||||
@@ -1,2 +0,0 @@
|
||||
# Guidelines
|
||||
Guidelines for community contributions.
|
||||
@@ -1,4 +1,4 @@
|
||||
# Getting started
|
||||
# Getting started with Tenant Control Planes
|
||||
|
||||
This document explains how to deploy a minimal Kamaji setup on [KinD](https://kind.sigs.k8s.io/) for development scopes. Please refer to the [Kamaji documentation](concepts.md) for understanding all the terms used in this guide, as for example: `admin cluster`, `tenant cluster`, and `tenant control plane`.
|
||||
|
||||
34
docs/content/getting-started/install.md
Normal file
34
docs/content/getting-started/install.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# Install Kamaji
|
||||
|
||||
## Quickstart
|
||||
|
||||
### Pre-requisites
|
||||
|
||||
- [Helm](https://helm.sh/docs/intro/install/)
|
||||
- Kubernetes cluster
|
||||
|
||||
### Install cert-manager
|
||||
|
||||
```shell
|
||||
helm repo add jetstack https://charts.jetstack.io
|
||||
helm repo update
|
||||
helm install \
|
||||
cert-manager jetstack/cert-manager \
|
||||
--namespace cert-manager \
|
||||
--create-namespace \
|
||||
--version v1.11.0 \
|
||||
--set installCRDs=true
|
||||
```
|
||||
|
||||
### Install Kamaji with default datastore
|
||||
|
||||
```
|
||||
helm repo add clastix https://clastix.github.io/charts
|
||||
helm repo update
|
||||
helm install kamaji clastix/kamaji -n kamaji-system --create-namespace
|
||||
```
|
||||
|
||||
Now you're ready to go with Kamaji!
|
||||
|
||||
Please follow the documentation to start playing with it.
|
||||
|
||||
73
docs/content/guides/backup-and-restore.md
Normal file
73
docs/content/guides/backup-and-restore.md
Normal file
@@ -0,0 +1,73 @@
|
||||
# Backup and restore
|
||||
|
||||
As mentioned in the introduction, Kamaji “tenant clusters” are just regular pods scheduled on top of a choosn admin cluster; as such, you can take advantage of the same backup and restore methods that you would use to maintain the standard workload.
|
||||
|
||||
This guide will assist you in how to backup and restore TCP resources on the admin cluster using [Velero](https://tanzu.vmware.com/developer/guides/what-is-velero/).
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before proceeding with the next steps, we assume that the following prerequisites are met:
|
||||
|
||||
- Working admin cluster
|
||||
- Working datastore resource
|
||||
- Working TCP resource
|
||||
- Velero binary installed on the operator VM
|
||||
- Velero installed on the admin cluster
|
||||
- Configured BackupStorageLocation for Velero
|
||||
|
||||
## Backup step
|
||||
|
||||
This example shows how to backup and restore a Tenant Control Plane called `tenant-00` and related resources using the `--include-namespaces` tag. Assume the Tenant Control Plane is deployed into the `tenant-00` namespace:
|
||||
|
||||
```
|
||||
velero backup create tenant-00 --include-namespaces tenant-00
|
||||
```
|
||||
|
||||
then, verify the backup job status:
|
||||
|
||||
```
|
||||
velero backup get tenant-00
|
||||
NAME STATUS ERRORS WARNINGS CREATED EXPIRES STORAGE LOCATION SELECTOR
|
||||
tenant-00 Completed 0 0 2023-02-23 17:45:13 +0100 CET 27d cloudian <none>
|
||||
```
|
||||
|
||||
in case of problems, you can get more information by running:
|
||||
|
||||
```
|
||||
velero backup describe tenant-00
|
||||
```
|
||||
|
||||
## Restore step
|
||||
|
||||
>_WARNING_: this procedure will restore just the TCP resource.
|
||||
In the event that the related datastore has been lost, you MUST restore it BEFORE continue; to do this, refer to the backup and restore strategy of the datastore of your choice.
|
||||
|
||||
---
|
||||
|
||||
To restore just the desired TCP, simply execute:
|
||||
|
||||
```
|
||||
velero restore create tenant-00 \
|
||||
--from-backup tenant-00 \
|
||||
--include-resources tcp,secret \
|
||||
--status-include-resources tcp
|
||||
```
|
||||
|
||||
verify the restore job status:
|
||||
|
||||
```
|
||||
velero restore get
|
||||
|
||||
NAME BACKUP STATUS STARTED COMPLETED ERRORS WARNINGS CREATED SELECTOR
|
||||
tenant-00 tenant-00 Completed 2023-02-24 12:31:39 +0100 CET 2023-02-24 12:31:40 +0100 CET 0 0 2023-02-24 12:31:39 +0100 CET <none>
|
||||
```
|
||||
|
||||
In a bunch of seconds, the Kamaji controller will reconcile the TCP and its status will pass from Ready, to NotReady and, finally, Ready again:
|
||||
|
||||
```
|
||||
kubectl get tcp -A
|
||||
|
||||
NAMESPACE NAME VERSION STATUS CONTROL-PLANE ENDPOINT KUBECONFIG DATASTORE AGE
|
||||
tenant-00 solar-energy v1.25.6 Ready 192.168.1.251:8443 solar-energy-admin-kubeconfig dedicated 6m
|
||||
[...]
|
||||
```
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
In Kamaji, there are different components that might require independent versioning and support level:
|
||||
|
||||
|Kamaji|Admin Cluster (min)|Admin Cluster (max)|Tenant Cluster (min)|Tenant Cluster (max)|Konnectivity|Tenant etcd |
|
||||
|------|-------------------|-------------------|--------------------|--------------------|------------|------------|
|
||||
|0.0.1 |1.22.0 |1.24.0 |1.21.0 |1.23.5 |0.0.31 |3.5.4 |
|
||||
|0.1.0 |1.22.0 |1.25.0 |1.21.0 |1.25.0 |0.0.32 |3.5.4 |
|
||||
|0.2.0 |1.22.0 |1.26.0 |1.21.0 |1.26.0 |0.0.32 |3.5.6 |
|
||||
|Kamaji |Admin Cluster| Tenant Cluster |
|
||||
|-------|-------------|----------------------|
|
||||
| v0.0 | v1.22+ | [v1.21.0 .. v1.23.5] |
|
||||
| v0.1 | v1.22+ | [v1.21.0 .. v1.25.0] |
|
||||
| v0.2 | v1.22+ | [v1.21.0 .. v1.27.0] |
|
||||
|
||||
|
||||
@@ -38,7 +38,9 @@ theme:
|
||||
# Generate navigation bar
|
||||
nav:
|
||||
- 'Kamaji': index.md
|
||||
- 'Getting started': getting-started.md
|
||||
- 'Getting started':
|
||||
- getting-started/install.md
|
||||
- getting-started/getting-started.md
|
||||
- 'Concepts': concepts.md
|
||||
- 'Guides':
|
||||
- guides/index.md
|
||||
@@ -49,6 +51,7 @@ nav:
|
||||
- guides/kamaji-gitops-flux.md
|
||||
- guides/upgrade.md
|
||||
- guides/datastore-migration.md
|
||||
- guides/backup-and-restore.md
|
||||
- 'Use Cases': use-cases.md
|
||||
- 'Reference':
|
||||
- reference/index.md
|
||||
@@ -57,7 +60,5 @@ nav:
|
||||
- reference/conformance.md
|
||||
- reference/versioning.md
|
||||
- reference/api.md
|
||||
- 'Contribute':
|
||||
- contribute/index.md
|
||||
- contribute/guidelines.md
|
||||
- contribute/governance.md
|
||||
- 'Contribute': contribute.md
|
||||
|
||||
|
||||
@@ -25,13 +25,30 @@ type KubernetesIngressResource struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
func (r *KubernetesIngressResource) ShouldStatusBeUpdated(_ context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) bool {
|
||||
return !(tenantControlPlane.Status.Kubernetes.Ingress.Name == r.resource.GetName() &&
|
||||
tenantControlPlane.Status.Kubernetes.Ingress.Namespace == r.resource.GetNamespace())
|
||||
func (r *KubernetesIngressResource) ShouldStatusBeUpdated(_ context.Context, tcp *kamajiv1alpha1.TenantControlPlane) bool {
|
||||
switch {
|
||||
case tcp.Spec.ControlPlane.Ingress == nil && tcp.Status.Kubernetes.Ingress == nil:
|
||||
// No update in case of no ingress in spec, neither in status.
|
||||
return false
|
||||
case tcp.Spec.ControlPlane.Ingress != nil && tcp.Status.Kubernetes.Ingress == nil,
|
||||
// Must be updated when TCP is using an Ingress, and status is not tracking it
|
||||
// or
|
||||
// Must be updated when the status is referring to an Ingress, although spec doesn't.
|
||||
tcp.Spec.ControlPlane.Ingress == nil && tcp.Status.Kubernetes.Ingress != nil:
|
||||
return true
|
||||
case len(r.resource.Status.LoadBalancer.Ingress) > 0 && tcp.Status.Kubernetes.Ingress == nil || tcp.Status.Kubernetes.Ingress.LoadBalancer.Ingress == nil:
|
||||
// Must be updated since missing the Ingress status
|
||||
return true
|
||||
case r.resource.Status.LoadBalancer.Ingress[0].IP != tcp.Status.Kubernetes.Ingress.LoadBalancer.Ingress[0].IP:
|
||||
// Must bne updated, Ingress load balancer IP is slightly different
|
||||
return true
|
||||
default:
|
||||
return tcp.Status.Kubernetes.Ingress.Name != r.resource.GetName() || tcp.Status.Kubernetes.Ingress.Namespace != r.resource.GetNamespace()
|
||||
}
|
||||
}
|
||||
|
||||
func (r *KubernetesIngressResource) ShouldCleanup(tenantControlPlane *kamajiv1alpha1.TenantControlPlane) bool {
|
||||
return tenantControlPlane.Spec.ControlPlane.Ingress == nil
|
||||
func (r *KubernetesIngressResource) ShouldCleanup(tcp *kamajiv1alpha1.TenantControlPlane) bool {
|
||||
return tcp.Spec.ControlPlane.Ingress == nil
|
||||
}
|
||||
|
||||
func (r *KubernetesIngressResource) CleanUp(ctx context.Context, _ *kamajiv1alpha1.TenantControlPlane) (bool, error) {
|
||||
@@ -52,14 +69,16 @@ func (r *KubernetesIngressResource) CleanUp(ctx context.Context, _ *kamajiv1alph
|
||||
|
||||
func (r *KubernetesIngressResource) UpdateTenantControlPlaneStatus(_ context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) error {
|
||||
if tenantControlPlane.Spec.ControlPlane.Ingress != nil {
|
||||
tenantControlPlane.Status.Kubernetes.Ingress.IngressStatus = r.resource.Status
|
||||
tenantControlPlane.Status.Kubernetes.Ingress.Name = r.resource.GetName()
|
||||
tenantControlPlane.Status.Kubernetes.Ingress.Namespace = r.resource.GetNamespace()
|
||||
tenantControlPlane.Status.Kubernetes.Ingress = &kamajiv1alpha1.KubernetesIngressStatus{
|
||||
IngressStatus: r.resource.Status,
|
||||
Name: r.resource.GetName(),
|
||||
Namespace: r.resource.GetNamespace(),
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
tenantControlPlane.Status.Kubernetes.Ingress = &kamajiv1alpha1.KubernetesIngressStatus{}
|
||||
tenantControlPlane.Status.Kubernetes.Ingress = nil
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -37,8 +37,15 @@ type KubeconfigResource struct {
|
||||
TmpDirectory string
|
||||
}
|
||||
|
||||
func (r *KubeconfigResource) ShouldStatusBeUpdated(context.Context, *kamajiv1alpha1.TenantControlPlane) bool {
|
||||
return false
|
||||
func (r *KubeconfigResource) ShouldStatusBeUpdated(_ context.Context, tcp *kamajiv1alpha1.TenantControlPlane) bool {
|
||||
// an update is required only in case of missing status checksum, or name:
|
||||
// this data is required by the following resource handlers.
|
||||
status, err := r.getKubeconfigStatus(tcp)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return len(status.Checksum) == 0 || len(status.SecretName) == 0
|
||||
}
|
||||
|
||||
func (r *KubeconfigResource) ShouldCleanup(*kamajiv1alpha1.TenantControlPlane) bool {
|
||||
@@ -151,27 +158,29 @@ func (r *KubeconfigResource) mutate(ctx context.Context, tenantControlPlane *kam
|
||||
|
||||
return err
|
||||
}
|
||||
// A new kubeconfig must be generated when one of the following cases is occurring:
|
||||
// 1. the status checksum is different from the computed one
|
||||
// 2. the resource UID is empty, meaning it's a new resource (tl;dr; a first reconciliation)
|
||||
//
|
||||
// And finally, we're checking if the kubeconfig is valid: if not, generating a new one.
|
||||
if (status.Checksum != checksum || len(r.resource.UID) == 0) && !kubeadm.IsKubeconfigValid(r.resource.Data[r.KubeConfigFileName]) {
|
||||
kubeconfig, err := kubeadm.CreateKubeconfig(
|
||||
r.KubeConfigFileName,
|
||||
|
||||
if (status.Checksum == checksum || len(r.resource.UID) > 0) && kubeadm.IsKubeconfigValid(r.resource.Data[r.KubeConfigFileName]) {
|
||||
return nil
|
||||
}
|
||||
kubeadm.CertificatePrivateKeyPair{
|
||||
Certificate: apiServerCertificatesSecret.Data[kubeadmconstants.CACertName],
|
||||
PrivateKey: apiServerCertificatesSecret.Data[kubeadmconstants.CAKeyName],
|
||||
},
|
||||
config,
|
||||
)
|
||||
if err != nil {
|
||||
logger.Error(err, "cannot create a valid kubeconfig")
|
||||
|
||||
kubeconfig, err := kubeadm.CreateKubeconfig(
|
||||
r.KubeConfigFileName,
|
||||
|
||||
kubeadm.CertificatePrivateKeyPair{
|
||||
Certificate: apiServerCertificatesSecret.Data[kubeadmconstants.CACertName],
|
||||
PrivateKey: apiServerCertificatesSecret.Data[kubeadmconstants.CAKeyName],
|
||||
},
|
||||
config,
|
||||
)
|
||||
if err != nil {
|
||||
logger.Error(err, "cannot create a valid kubeconfig")
|
||||
|
||||
return err
|
||||
}
|
||||
r.resource.Data = map[string][]byte{
|
||||
r.KubeConfigFileName: kubeconfig,
|
||||
return err
|
||||
}
|
||||
r.resource.Data = map[string][]byte{
|
||||
r.KubeConfigFileName: kubeconfig,
|
||||
}
|
||||
}
|
||||
|
||||
r.resource.SetLabels(utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName()))
|
||||
|
||||
@@ -4,5 +4,5 @@
|
||||
package upgrade
|
||||
|
||||
const (
|
||||
KubeadmVersion = "v1.26.1"
|
||||
KubeadmVersion = "v1.27.0"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user