mirror of
https://github.com/docker/compose.git
synced 2025-07-22 21:24:38 +02:00
Validate run restart option value. Default value is “none”, instead of “no”, this is more in line with compose values, and changes only the default so should not have too much impact on legacy usage.
This commit is contained in:
parent
a2c2d6aa5d
commit
f442eafe0b
@ -24,11 +24,6 @@ func ContainerToComposeProject(r containers.ContainerConfig) (types.Project, err
|
|||||||
return types.Project{}, err
|
return types.Project{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
composeRestartPolicyCondition := r.RestartPolicyCondition
|
|
||||||
if composeRestartPolicyCondition == "no" {
|
|
||||||
composeRestartPolicyCondition = "none"
|
|
||||||
}
|
|
||||||
|
|
||||||
project := types.Project{
|
project := types.Project{
|
||||||
Name: r.ID,
|
Name: r.ID,
|
||||||
Services: []types.ServiceConfig{
|
Services: []types.ServiceConfig{
|
||||||
@ -47,7 +42,7 @@ func ContainerToComposeProject(r containers.ContainerConfig) (types.Project, err
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
RestartPolicy: &types.RestartPolicy{
|
RestartPolicy: &types.RestartPolicy{
|
||||||
Condition: composeRestartPolicyCondition,
|
Condition: r.RestartPolicyCondition,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -51,7 +51,7 @@ func (suite *ContainerConvertTestSuite) TestConvertContainerEnvironment() {
|
|||||||
func (suite *ContainerConvertTestSuite) TestConvertRestartPolicy() {
|
func (suite *ContainerConvertTestSuite) TestConvertRestartPolicy() {
|
||||||
container := containers.ContainerConfig{
|
container := containers.ContainerConfig{
|
||||||
ID: "container1",
|
ID: "container1",
|
||||||
RestartPolicyCondition: "no",
|
RestartPolicyCondition: "none",
|
||||||
}
|
}
|
||||||
project, err := ContainerToComposeProject(container)
|
project, err := ContainerToComposeProject(container)
|
||||||
Expect(err).To(BeNil())
|
Expect(err).To(BeNil())
|
||||||
|
@ -71,15 +71,6 @@ func ToContainerGroup(aciContext store.AciContext, p types.Project) (containerin
|
|||||||
return containerinstance.ContainerGroup{}, err
|
return containerinstance.ContainerGroup{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var restartPolicyCondition containerinstance.ContainerGroupRestartPolicy
|
|
||||||
if len(p.Services) == 1 &&
|
|
||||||
p.Services[0].Deploy != nil &&
|
|
||||||
p.Services[0].Deploy.RestartPolicy != nil {
|
|
||||||
restartPolicyCondition = toAciRestartPolicy(p.Services[0].Deploy.RestartPolicy.Condition)
|
|
||||||
} else {
|
|
||||||
restartPolicyCondition = containerinstance.Always
|
|
||||||
}
|
|
||||||
|
|
||||||
var containers []containerinstance.Container
|
var containers []containerinstance.Container
|
||||||
groupDefinition := containerinstance.ContainerGroup{
|
groupDefinition := containerinstance.ContainerGroup{
|
||||||
Name: &containerGroupName,
|
Name: &containerGroupName,
|
||||||
@ -89,7 +80,7 @@ func ToContainerGroup(aciContext store.AciContext, p types.Project) (containerin
|
|||||||
Containers: &containers,
|
Containers: &containers,
|
||||||
Volumes: volumes,
|
Volumes: volumes,
|
||||||
ImageRegistryCredentials: ®istryCreds,
|
ImageRegistryCredentials: ®istryCreds,
|
||||||
RestartPolicy: restartPolicyCondition,
|
RestartPolicy: project.getRestartPolicy(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -135,32 +126,6 @@ func ToContainerGroup(aciContext store.AciContext, p types.Project) (containerin
|
|||||||
return groupDefinition, nil
|
return groupDefinition, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func toAciRestartPolicy(restartPolicy string) containerinstance.ContainerGroupRestartPolicy {
|
|
||||||
switch restartPolicy {
|
|
||||||
case containers.RestartPolicyNone:
|
|
||||||
return containerinstance.Never
|
|
||||||
case containers.RestartPolicyAny:
|
|
||||||
return containerinstance.Always
|
|
||||||
case containers.RestartPolicyOnFailure:
|
|
||||||
return containerinstance.OnFailure
|
|
||||||
default:
|
|
||||||
return containerinstance.Always
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func toContainerRestartPolicy(aciRestartPolicy containerinstance.ContainerGroupRestartPolicy) string {
|
|
||||||
switch aciRestartPolicy {
|
|
||||||
case containerinstance.Never:
|
|
||||||
return containers.RestartPolicyNone
|
|
||||||
case containerinstance.Always:
|
|
||||||
return containers.RestartPolicyAny
|
|
||||||
case containerinstance.OnFailure:
|
|
||||||
return containers.RestartPolicyOnFailure
|
|
||||||
default:
|
|
||||||
return containers.RestartPolicyAny
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getDNSSidecar(containers []containerinstance.Container) containerinstance.Container {
|
func getDNSSidecar(containers []containerinstance.Container) containerinstance.Container {
|
||||||
var commands []string
|
var commands []string
|
||||||
for _, container := range containers {
|
for _, container := range containers {
|
||||||
@ -251,6 +216,44 @@ func (p projectAciHelper) getAciFileVolumes() (map[string]bool, []containerinsta
|
|||||||
return azureFileVolumesMap, azureFileVolumesSlice, nil
|
return azureFileVolumesMap, azureFileVolumesSlice, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p projectAciHelper) getRestartPolicy() containerinstance.ContainerGroupRestartPolicy {
|
||||||
|
var restartPolicyCondition containerinstance.ContainerGroupRestartPolicy
|
||||||
|
if len(p.Services) == 1 &&
|
||||||
|
p.Services[0].Deploy != nil &&
|
||||||
|
p.Services[0].Deploy.RestartPolicy != nil {
|
||||||
|
restartPolicyCondition = toAciRestartPolicy(p.Services[0].Deploy.RestartPolicy.Condition)
|
||||||
|
} else {
|
||||||
|
restartPolicyCondition = containerinstance.Always
|
||||||
|
}
|
||||||
|
return restartPolicyCondition
|
||||||
|
}
|
||||||
|
|
||||||
|
func toAciRestartPolicy(restartPolicy string) containerinstance.ContainerGroupRestartPolicy {
|
||||||
|
switch restartPolicy {
|
||||||
|
case containers.RestartPolicyNone:
|
||||||
|
return containerinstance.Never
|
||||||
|
case containers.RestartPolicyAny:
|
||||||
|
return containerinstance.Always
|
||||||
|
case containers.RestartPolicyOnFailure:
|
||||||
|
return containerinstance.OnFailure
|
||||||
|
default:
|
||||||
|
return containerinstance.Always
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toContainerRestartPolicy(aciRestartPolicy containerinstance.ContainerGroupRestartPolicy) string {
|
||||||
|
switch aciRestartPolicy {
|
||||||
|
case containerinstance.Never:
|
||||||
|
return containers.RestartPolicyNone
|
||||||
|
case containerinstance.Always:
|
||||||
|
return containers.RestartPolicyAny
|
||||||
|
case containerinstance.OnFailure:
|
||||||
|
return containers.RestartPolicyOnFailure
|
||||||
|
default:
|
||||||
|
return containers.RestartPolicyAny
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type serviceConfigAciHelper types.ServiceConfig
|
type serviceConfigAciHelper types.ServiceConfig
|
||||||
|
|
||||||
func (s serviceConfigAciHelper) getAciFileVolumeMounts(volumesCache map[string]bool) ([]containerinstance.VolumeMount, error) {
|
func (s serviceConfigAciHelper) getAciFileVolumeMounts(volumesCache map[string]bool) ([]containerinstance.VolumeMount, error) {
|
||||||
|
@ -52,7 +52,7 @@ func Command() *cobra.Command {
|
|||||||
cmd.Flags().Float64Var(&opts.Cpus, "cpus", 1., "Number of CPUs")
|
cmd.Flags().Float64Var(&opts.Cpus, "cpus", 1., "Number of CPUs")
|
||||||
cmd.Flags().VarP(&opts.Memory, "memory", "m", "Memory limit")
|
cmd.Flags().VarP(&opts.Memory, "memory", "m", "Memory limit")
|
||||||
cmd.Flags().StringArrayVarP(&opts.Environment, "env", "e", []string{}, "Set environment variables")
|
cmd.Flags().StringArrayVarP(&opts.Environment, "env", "e", []string{}, "Set environment variables")
|
||||||
cmd.Flags().StringVarP(&opts.RestartPolicyCondition, "restart", "", "no", "Restart policy to apply when a container exits")
|
cmd.Flags().StringVarP(&opts.RestartPolicyCondition, "restart", "", containers.RestartPolicyNone, "Restart policy to apply when a container exits")
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
2
cli/cmd/run/testdata/run-help.golden
vendored
2
cli/cmd/run/testdata/run-help.golden
vendored
@ -11,5 +11,5 @@ Flags:
|
|||||||
-m, --memory bytes Memory limit
|
-m, --memory bytes Memory limit
|
||||||
--name string Assign a name to the container
|
--name string Assign a name to the container
|
||||||
-p, --publish stringArray Publish a container's port(s). [HOST_PORT:]CONTAINER_PORT
|
-p, --publish stringArray Publish a container's port(s). [HOST_PORT:]CONTAINER_PORT
|
||||||
--restart string Restart policy to apply when a container exits (default "no")
|
--restart string Restart policy to apply when a container exits (default "none")
|
||||||
-v, --volume stringArray Volume. Ex: user:key@my_share:/absolute/path/to/target
|
-v, --volume stringArray Volume. Ex: user:key@my_share:/absolute/path/to/target
|
||||||
|
2
cli/cmd/testdata/inspect-out-id.golden
vendored
2
cli/cmd/testdata/inspect-out-id.golden
vendored
@ -12,5 +12,5 @@
|
|||||||
"Labels": null,
|
"Labels": null,
|
||||||
"Ports": null,
|
"Ports": null,
|
||||||
"Platform": "Linux",
|
"Platform": "Linux",
|
||||||
"RestartPolicyCondition": ""
|
"RestartPolicyCondition": "none"
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,8 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/docker/api/utils"
|
||||||
|
|
||||||
"github.com/docker/docker/pkg/namesgenerator"
|
"github.com/docker/docker/pkg/namesgenerator"
|
||||||
"github.com/docker/go-connections/nat"
|
"github.com/docker/go-connections/nat"
|
||||||
|
|
||||||
@ -57,6 +59,11 @@ func (r *Opts) ToContainerConfig(image string) (containers.ContainerConfig, erro
|
|||||||
return containers.ContainerConfig{}, err
|
return containers.ContainerConfig{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
restartPolicy, err := toRestartPolicy(r.RestartPolicyCondition)
|
||||||
|
if err != nil {
|
||||||
|
return containers.ContainerConfig{}, err
|
||||||
|
}
|
||||||
|
|
||||||
return containers.ContainerConfig{
|
return containers.ContainerConfig{
|
||||||
ID: r.Name,
|
ID: r.Name,
|
||||||
Image: image,
|
Image: image,
|
||||||
@ -66,10 +73,21 @@ func (r *Opts) ToContainerConfig(image string) (containers.ContainerConfig, erro
|
|||||||
MemLimit: r.Memory,
|
MemLimit: r.Memory,
|
||||||
CPULimit: r.Cpus,
|
CPULimit: r.Cpus,
|
||||||
Environment: r.Environment,
|
Environment: r.Environment,
|
||||||
RestartPolicyCondition: r.RestartPolicyCondition,
|
RestartPolicyCondition: restartPolicy,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func toRestartPolicy(value string) (string, error) {
|
||||||
|
if value == "" {
|
||||||
|
return containers.RestartPolicyNone, nil
|
||||||
|
}
|
||||||
|
if utils.StringContains(containers.RestartPolicyList, value) {
|
||||||
|
return value, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", fmt.Errorf("invalid restart value, must be one of %s", strings.Join(containers.RestartPolicyList, ", "))
|
||||||
|
}
|
||||||
|
|
||||||
func (r *Opts) toPorts() ([]containers.Port, error) {
|
func (r *Opts) toPorts() ([]containers.Port, error) {
|
||||||
_, bindings, err := nat.ParsePortSpecs(r.Publish)
|
_, bindings, err := nat.ParsePortSpecs(r.Publish)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -173,3 +173,46 @@ func TestLabels(t *testing.T) {
|
|||||||
assert.DeepEqual(t, result, testCase.expected)
|
assert.DeepEqual(t, result, testCase.expected)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestValidateRestartPolicy(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
in string
|
||||||
|
expected string
|
||||||
|
expectedError error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
in: "none",
|
||||||
|
expected: "none",
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
in: "any",
|
||||||
|
expected: "any",
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
in: "on-failure",
|
||||||
|
expected: "on-failure",
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
in: "",
|
||||||
|
expected: "none",
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
in: "toto",
|
||||||
|
expected: "",
|
||||||
|
expectedError: errors.New("invalid restart value, must be one of none, any, on-failure"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
result, err := toRestartPolicy(testCase.in)
|
||||||
|
if testCase.expectedError == nil {
|
||||||
|
assert.NilError(t, err)
|
||||||
|
} else {
|
||||||
|
assert.Error(t, err, testCase.expectedError.Error())
|
||||||
|
}
|
||||||
|
assert.Equal(t, testCase.expected, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -23,6 +23,18 @@ import (
|
|||||||
"github.com/docker/api/formatter"
|
"github.com/docker/api/formatter"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// RestartPolicyAny Always restarts
|
||||||
|
RestartPolicyAny = "any"
|
||||||
|
// RestartPolicyNone Never restarts
|
||||||
|
RestartPolicyNone = "none"
|
||||||
|
// RestartPolicyOnFailure Restarts only on failure
|
||||||
|
RestartPolicyOnFailure = "on-failure"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RestartPolicyList all available restart policy values
|
||||||
|
var RestartPolicyList = []string{RestartPolicyNone, RestartPolicyAny, RestartPolicyOnFailure}
|
||||||
|
|
||||||
// Container represents a created container
|
// Container represents a created container
|
||||||
type Container struct {
|
type Container struct {
|
||||||
ID string
|
ID string
|
||||||
|
@ -1,11 +0,0 @@
|
|||||||
package containers
|
|
||||||
|
|
||||||
const (
|
|
||||||
// RestartPolicyAny Always restarts
|
|
||||||
RestartPolicyAny = "any"
|
|
||||||
// RestartPolicyNone Never restarts
|
|
||||||
// "no" is the value for docker run, "none" is the value in compose file (and default differ
|
|
||||||
RestartPolicyNone = "none"
|
|
||||||
// RestartPolicyOnFailure Restarts only on failure
|
|
||||||
RestartPolicyOnFailure = "on-failure"
|
|
||||||
)
|
|
@ -31,7 +31,7 @@ docker run [OPTIONS] _image_ [COMMAND] [ARG...]
|
|||||||
--cpus Number of CPUs to allocate, approximately
|
--cpus Number of CPUs to allocate, approximately
|
||||||
-p, --publish list Publish a container's port(s) to the host
|
-p, --publish list Publish a container's port(s) to the host
|
||||||
-P, --publish-all Publish all exposed ports to random ports
|
-P, --publish-all Publish all exposed ports to random ports
|
||||||
--restart string Restart policy to apply when a container exits (default "no")
|
--restart string Restart policy to apply when a container exits (default "none")
|
||||||
--entrypoint string Overwrite the default ENTRYPOINT of the image
|
--entrypoint string Overwrite the default ENTRYPOINT of the image
|
||||||
|
|
||||||
--mount mount Attach a filesystem mount to the container
|
--mount mount Attach a filesystem mount to the container
|
||||||
|
@ -57,9 +57,10 @@ type containerService struct{}
|
|||||||
|
|
||||||
func (cs *containerService) Inspect(ctx context.Context, id string) (containers.Container, error) {
|
func (cs *containerService) Inspect(ctx context.Context, id string) (containers.Container, error) {
|
||||||
return containers.Container{
|
return containers.Container{
|
||||||
ID: "id",
|
ID: "id",
|
||||||
Image: "nginx",
|
Image: "nginx",
|
||||||
Platform: "Linux",
|
Platform: "Linux",
|
||||||
|
RestartPolicyCondition: "none",
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,6 +20,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
flag "github.com/spf13/pflag"
|
flag "github.com/spf13/pflag"
|
||||||
|
|
||||||
|
"github.com/docker/api/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
var managementCommands = []string{
|
var managementCommands = []string{
|
||||||
@ -111,7 +113,7 @@ func getCommand(args []string, flags *flag.FlagSet) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for {
|
for {
|
||||||
if contains(managementCommands, command) {
|
if utils.StringContains(managementCommands, command) {
|
||||||
if sub := getSubCommand(command, strippedArgs[1:]); sub != "" {
|
if sub := getSubCommand(command, strippedArgs[1:]); sub != "" {
|
||||||
command += " " + sub
|
command += " " + sub
|
||||||
strippedArgs = strippedArgs[1:]
|
strippedArgs = strippedArgs[1:]
|
||||||
@ -128,11 +130,11 @@ func getCommand(args []string, flags *flag.FlagSet) string {
|
|||||||
func getScanCommand(args []string) string {
|
func getScanCommand(args []string) string {
|
||||||
command := args[0]
|
command := args[0]
|
||||||
|
|
||||||
if contains(args, "--auth") {
|
if utils.StringContains(args, "--auth") {
|
||||||
return command + " auth"
|
return command + " auth"
|
||||||
}
|
}
|
||||||
|
|
||||||
if contains(args, "--version") {
|
if utils.StringContains(args, "--version") {
|
||||||
return command + " version"
|
return command + " version"
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -145,7 +147,7 @@ func getSubCommand(command string, args []string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if val, ok := managementSubCommands[command]; ok {
|
if val, ok := managementSubCommands[command]; ok {
|
||||||
if contains(val, args[0]) {
|
if utils.StringContains(val, args[0]) {
|
||||||
return args[0]
|
return args[0]
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
@ -158,15 +160,6 @@ func getSubCommand(command string, args []string) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func contains(array []string, needle string) bool {
|
|
||||||
for _, val := range array {
|
|
||||||
if val == needle {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func stripFlags(args []string, flags *flag.FlagSet) []string {
|
func stripFlags(args []string, flags *flag.FlagSet) []string {
|
||||||
commands := []string{}
|
commands := []string{}
|
||||||
|
|
||||||
|
@ -25,6 +25,8 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/docker/api/utils"
|
||||||
|
|
||||||
"github.com/buger/goterm"
|
"github.com/buger/goterm"
|
||||||
"github.com/morikuni/aec"
|
"github.com/morikuni/aec"
|
||||||
)
|
)
|
||||||
@ -63,7 +65,7 @@ func (w *ttyWriter) Stop() {
|
|||||||
func (w *ttyWriter) Event(e Event) {
|
func (w *ttyWriter) Event(e Event) {
|
||||||
w.mtx.Lock()
|
w.mtx.Lock()
|
||||||
defer w.mtx.Unlock()
|
defer w.mtx.Unlock()
|
||||||
if !contains(w.eventIDs, e.ID) {
|
if !utils.StringContains(w.eventIDs, e.ID) {
|
||||||
w.eventIDs = append(w.eventIDs, e.ID)
|
w.eventIDs = append(w.eventIDs, e.ID)
|
||||||
}
|
}
|
||||||
if _, ok := w.events[e.ID]; ok {
|
if _, ok := w.events[e.ID]; ok {
|
||||||
@ -181,12 +183,3 @@ func numDone(events map[string]Event) int {
|
|||||||
func align(l, r string, w int) string {
|
func align(l, r string, w int) string {
|
||||||
return fmt.Sprintf("%-[2]*[1]s %[3]s", l, w-len(r)-1, r)
|
return fmt.Sprintf("%-[2]*[1]s %[3]s", l, w-len(r)-1, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
func contains(ar []string, needle string) bool {
|
|
||||||
for _, v := range ar {
|
|
||||||
if needle == v {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
3
tests/e2e/testdata/inspect-id.golden
vendored
3
tests/e2e/testdata/inspect-id.golden
vendored
@ -11,5 +11,6 @@
|
|||||||
"PidsLimit": 0,
|
"PidsLimit": 0,
|
||||||
"Labels": null,
|
"Labels": null,
|
||||||
"Ports": null,
|
"Ports": null,
|
||||||
"Platform": "Linux"
|
"Platform": "Linux",
|
||||||
|
"RestartPolicyCondition": "none"
|
||||||
}
|
}
|
||||||
|
11
utils/stringutils.go
Normal file
11
utils/stringutils.go
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
// StringContains check if an array contains a specific value
|
||||||
|
func StringContains(array []string, needle string) bool {
|
||||||
|
for _, val := range array {
|
||||||
|
if val == needle {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user