Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
K
k3s
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Registry
Registry
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Jacklull
k3s
Commits
18cb56bf
Commit
18cb56bf
authored
May 05, 2017
by
Fabiano Franz
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
kubectl plugins have access config, global flags and environment
parent
da85262f
Hide whitespace changes
Inline
Side-by-side
Showing
11 changed files
with
427 additions
and
25 deletions
+427
-25
test-cmd-util.sh
hack/make-rules/test-cmd-util.sh
+9
-0
plugin.go
pkg/kubectl/cmd/plugin.go
+65
-12
plugin_test.go
pkg/kubectl/cmd/plugin_test.go
+3
-1
BUILD
pkg/kubectl/plugins/BUILD
+4
-0
env.go
pkg/kubectl/plugins/env.go
+147
-0
env_test.go
pkg/kubectl/plugins/env_test.go
+162
-0
runner.go
pkg/kubectl/plugins/runner.go
+11
-7
runner_test.go
pkg/kubectl/plugins/runner_test.go
+3
-2
env.sh
test/fixtures/pkg/kubectl/plugins/env/env.sh
+17
-0
plugin.yaml
test/fixtures/pkg/kubectl/plugins/env/plugin.yaml
+3
-0
plugin.yaml
test/fixtures/pkg/kubectl/plugins/tree/plugin.yaml
+3
-3
No files found.
hack/make-rules/test-cmd-util.sh
View file @
18cb56bf
...
...
@@ -3784,6 +3784,15 @@ __EOF__
kube::test::if_has_not_string
"
${
output_message
}
"
'child1'
kube::test::if_has_not_string
"
${
output_message
}
"
'The first child'
# plugin env
output_message
=
$(
KUBECTL_PLUGINS_PATH
=
test
/fixtures/pkg/kubectl/plugins kubectl plugin
env
2>&1
)
kube::test::if_has_string
"
${
output_message
}
"
'KUBECTL_PLUGINS_CURRENT_NAMESPACE'
kube::test::if_has_string
"
${
output_message
}
"
'KUBECTL_PLUGINS_CALLER'
kube::test::if_has_string
"
${
output_message
}
"
'KUBECTL_PLUGINS_DESCRIPTOR_COMMAND=./env.sh'
kube::test::if_has_string
"
${
output_message
}
"
'KUBECTL_PLUGINS_DESCRIPTOR_SHORT_DESC=The plugin envs plugin'
kube::test::if_has_string
"
${
output_message
}
"
'KUBECTL_PLUGINS_GLOBAL_FLAG_KUBECONFIG'
kube::test::if_has_string
"
${
output_message
}
"
'KUBECTL_PLUGINS_GLOBAL_FLAG_REQUEST_TIMEOUT=0'
#################
# Impersonation #
#################
...
...
pkg/kubectl/cmd/plugin.go
View file @
18cb56bf
...
...
@@ -19,10 +19,10 @@ package cmd
import
(
"fmt"
"io"
"os"
"github.com/golang/glog"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"k8s.io/kubernetes/pkg/kubectl/cmd/templates"
cmdutil
"k8s.io/kubernetes/pkg/kubectl/cmd/util"
"k8s.io/kubernetes/pkg/kubectl/plugins"
...
...
@@ -61,7 +61,7 @@ func NewCmdPlugin(f cmdutil.Factory, in io.Reader, out, err io.Writer) *cobra.Co
if
len
(
loadedPlugins
)
>
0
{
pluginRunner
:=
f
.
PluginRunner
()
for
_
,
p
:=
range
loadedPlugins
{
cmd
.
AddCommand
(
NewCmdForPlugin
(
p
,
pluginRunner
,
in
,
out
,
err
))
cmd
.
AddCommand
(
NewCmdForPlugin
(
f
,
p
,
pluginRunner
,
in
,
out
,
err
))
}
}
...
...
@@ -69,28 +69,81 @@ func NewCmdPlugin(f cmdutil.Factory, in io.Reader, out, err io.Writer) *cobra.Co
}
// NewCmdForPlugin creates a command capable of running the provided plugin.
func
NewCmdForPlugin
(
plugin
*
plugins
.
Plugin
,
runner
plugins
.
PluginRunner
,
in
io
.
Reader
,
out
,
errout
io
.
Writer
)
*
cobra
.
Command
{
func
NewCmdForPlugin
(
f
cmdutil
.
Factory
,
plugin
*
plugins
.
Plugin
,
runner
plugins
.
PluginRunner
,
in
io
.
Reader
,
out
,
errout
io
.
Writer
)
*
cobra
.
Command
{
if
!
plugin
.
IsValid
()
{
return
nil
}
return
&
cobra
.
Command
{
cmd
:=
&
cobra
.
Command
{
Use
:
plugin
.
Name
,
Short
:
plugin
.
ShortDesc
,
Long
:
templates
.
LongDesc
(
plugin
.
LongDesc
),
Example
:
templates
.
Examples
(
plugin
.
Example
),
Run
:
func
(
cmd
*
cobra
.
Command
,
args
[]
string
)
{
ctx
:=
plugins
.
RunningContext
{
In
:
in
,
Out
:
out
,
ErrOut
:
errout
,
Args
:
args
,
Env
:
os
.
Environ
(),
WorkingDir
:
plugin
.
Dir
,
if
len
(
plugin
.
Command
)
==
0
{
cmdutil
.
DefaultSubCommandRun
(
errout
)(
cmd
,
args
)
return
}
envProvider
:=
&
plugins
.
MultiEnvProvider
{
&
plugins
.
PluginCallerEnvProvider
{},
&
plugins
.
OSEnvProvider
{},
&
plugins
.
PluginDescriptorEnvProvider
{
Plugin
:
plugin
,
},
&
flagsPluginEnvProvider
{
cmd
:
cmd
,
},
&
factoryAttrsPluginEnvProvider
{
factory
:
f
,
},
}
runningContext
:=
plugins
.
RunningContext
{
In
:
in
,
Out
:
out
,
ErrOut
:
errout
,
Args
:
args
,
EnvProvider
:
envProvider
,
WorkingDir
:
plugin
.
Dir
,
}
if
err
:=
runner
.
Run
(
plugin
,
ctx
);
err
!=
nil
{
if
err
:=
runner
.
Run
(
plugin
,
runningContext
);
err
!=
nil
{
cmdutil
.
CheckErr
(
err
)
}
},
}
for
_
,
childPlugin
:=
range
plugin
.
Tree
{
cmd
.
AddCommand
(
NewCmdForPlugin
(
f
,
childPlugin
,
runner
,
in
,
out
,
errout
))
}
return
cmd
}
type
flagsPluginEnvProvider
struct
{
cmd
*
cobra
.
Command
}
func
(
p
*
flagsPluginEnvProvider
)
Env
()
(
plugins
.
EnvList
,
error
)
{
prefix
:=
"KUBECTL_PLUGINS_GLOBAL_FLAG_"
env
:=
plugins
.
EnvList
{}
p
.
cmd
.
Flags
()
.
VisitAll
(
func
(
flag
*
pflag
.
Flag
)
{
env
=
append
(
env
,
plugins
.
FlagToEnv
(
flag
,
prefix
))
})
return
env
,
nil
}
type
factoryAttrsPluginEnvProvider
struct
{
factory
cmdutil
.
Factory
}
func
(
p
*
factoryAttrsPluginEnvProvider
)
Env
()
(
plugins
.
EnvList
,
error
)
{
cmdNamespace
,
_
,
err
:=
p
.
factory
.
DefaultNamespace
()
if
err
!=
nil
{
return
plugins
.
EnvList
{},
err
}
return
plugins
.
EnvList
{
plugins
.
Env
{
N
:
"KUBECTL_PLUGINS_CURRENT_NAMESPACE"
,
V
:
cmdNamespace
},
},
nil
}
pkg/kubectl/cmd/plugin_test.go
View file @
18cb56bf
...
...
@@ -21,6 +21,7 @@ import (
"fmt"
"testing"
cmdtesting
"k8s.io/kubernetes/pkg/kubectl/cmd/testing"
cmdutil
"k8s.io/kubernetes/pkg/kubectl/cmd/util"
"k8s.io/kubernetes/pkg/kubectl/plugins"
)
...
...
@@ -91,7 +92,8 @@ func TestPluginCmd(t *testing.T) {
success
:
test
.
expectedSuccess
,
}
cmd
:=
NewCmdForPlugin
(
test
.
plugin
,
runner
,
inBuf
,
outBuf
,
errBuf
)
f
,
_
,
_
,
_
:=
cmdtesting
.
NewAPIFactory
()
cmd
:=
NewCmdForPlugin
(
f
,
test
.
plugin
,
runner
,
inBuf
,
outBuf
,
errBuf
)
if
cmd
==
nil
{
if
!
test
.
expectedNilCmd
{
t
.
Fatalf
(
"%s: command was unexpectedly not registered"
,
test
.
name
)
...
...
pkg/kubectl/plugins/BUILD
View file @
18cb56bf
...
...
@@ -11,6 +11,7 @@ load(
go_library(
name = "go_default_library",
srcs = [
"env.go",
"loader.go",
"plugins.go",
"runner.go",
...
...
@@ -19,6 +20,7 @@ go_library(
deps = [
"//vendor/github.com/ghodss/yaml:go_default_library",
"//vendor/github.com/golang/glog:go_default_library",
"//vendor/github.com/spf13/pflag:go_default_library",
"//vendor/k8s.io/client-go/tools/clientcmd:go_default_library",
],
)
...
...
@@ -39,10 +41,12 @@ filegroup(
go_test(
name = "go_default_test",
srcs = [
"env_test.go",
"loader_test.go",
"plugins_test.go",
"runner_test.go",
],
library = ":go_default_library",
tags = ["automanaged"],
deps = ["//vendor/github.com/spf13/pflag:go_default_library"],
)
pkg/kubectl/plugins/env.go
0 → 100644
View file @
18cb56bf
/*
Copyright 2017 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package
plugins
import
(
"fmt"
"os"
"strings"
"github.com/spf13/pflag"
)
// Env represents an environment variable with its name and value
type
Env
struct
{
N
string
V
string
}
func
(
e
Env
)
String
()
string
{
return
fmt
.
Sprintf
(
"%s=%s"
,
e
.
N
,
e
.
V
)
}
// EnvList is a list of Env
type
EnvList
[]
Env
func
(
e
EnvList
)
Slice
()
[]
string
{
envs
:=
[]
string
{}
for
_
,
env
:=
range
e
{
envs
=
append
(
envs
,
env
.
String
())
}
return
envs
}
func
(
e
EnvList
)
Merge
(
s
...
string
)
EnvList
{
newList
:=
e
newList
=
append
(
newList
,
fromSlice
(
s
)
...
)
return
newList
}
// EnvProvider provides the environment in which the plugin will run.
type
EnvProvider
interface
{
Env
()
(
EnvList
,
error
)
}
// MultiEnvProvider is an EnvProvider for multiple env providers, returns on first error.
type
MultiEnvProvider
[]
EnvProvider
func
(
p
MultiEnvProvider
)
Env
()
(
EnvList
,
error
)
{
env
:=
EnvList
{}
for
_
,
provider
:=
range
p
{
pEnv
,
err
:=
provider
.
Env
()
if
err
!=
nil
{
return
EnvList
{},
err
}
env
=
append
(
env
,
pEnv
...
)
}
return
env
,
nil
}
// PluginCallerEnvProvider provides env with the path to the caller binary (usually full path to 'kubectl').
type
PluginCallerEnvProvider
struct
{}
func
(
p
*
PluginCallerEnvProvider
)
Env
()
(
EnvList
,
error
)
{
caller
,
err
:=
os
.
Executable
()
if
err
!=
nil
{
return
EnvList
{},
err
}
return
EnvList
{
{
"KUBECTL_PLUGINS_CALLER"
,
caller
},
},
nil
}
// PluginDescriptorEnvProvider provides env vars with information about the running plugin.
type
PluginDescriptorEnvProvider
struct
{
Plugin
*
Plugin
}
func
(
p
*
PluginDescriptorEnvProvider
)
Env
()
(
EnvList
,
error
)
{
if
p
.
Plugin
==
nil
{
return
[]
Env
{},
fmt
.
Errorf
(
"plugin not present to extract env"
)
}
prefix
:=
"KUBECTL_PLUGINS_DESCRIPTOR_"
env
:=
EnvList
{
{
prefix
+
"NAME"
,
p
.
Plugin
.
Name
},
{
prefix
+
"SHORT_DESC"
,
p
.
Plugin
.
ShortDesc
},
{
prefix
+
"LONG_DESC"
,
p
.
Plugin
.
LongDesc
},
{
prefix
+
"EXAMPLE"
,
p
.
Plugin
.
Example
},
{
prefix
+
"COMMAND"
,
p
.
Plugin
.
Command
},
}
return
env
,
nil
}
// OSEnvProvider provides current environment from the operating system.
type
OSEnvProvider
struct
{}
func
(
p
*
OSEnvProvider
)
Env
()
(
EnvList
,
error
)
{
return
fromSlice
(
os
.
Environ
()),
nil
}
type
EmptyEnvProvider
struct
{}
func
(
p
*
EmptyEnvProvider
)
Env
()
(
EnvList
,
error
)
{
return
EnvList
{},
nil
}
func
FlagToEnvName
(
flagName
,
prefix
string
)
string
{
envName
:=
strings
.
TrimPrefix
(
flagName
,
"--"
)
envName
=
strings
.
ToUpper
(
envName
)
envName
=
strings
.
Replace
(
envName
,
"-"
,
"_"
,
-
1
)
envName
=
prefix
+
envName
return
envName
}
func
FlagToEnv
(
flag
*
pflag
.
Flag
,
prefix
string
)
Env
{
envName
:=
FlagToEnvName
(
flag
.
Name
,
prefix
)
return
Env
{
envName
,
flag
.
Value
.
String
()}
}
func
fromSlice
(
envs
[]
string
)
EnvList
{
list
:=
EnvList
{}
for
_
,
env
:=
range
envs
{
list
=
append
(
list
,
parseEnv
(
env
))
}
return
list
}
func
parseEnv
(
env
string
)
Env
{
if
!
strings
.
Contains
(
env
,
"="
)
{
env
=
env
+
"="
}
parsed
:=
strings
.
SplitN
(
env
,
"="
,
2
)
return
Env
{
parsed
[
0
],
parsed
[
1
]}
}
pkg/kubectl/plugins/env_test.go
0 → 100644
View file @
18cb56bf
/*
Copyright 2017 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package
plugins
import
(
"reflect"
"testing"
"github.com/spf13/pflag"
)
func
TestEnv
(
t
*
testing
.
T
)
{
tests
:=
[]
struct
{
env
Env
expected
string
}{
{
env
:
Env
{
"FOO"
,
"BAR"
},
expected
:
"FOO=BAR"
,
},
{
env
:
Env
{
"FOO"
,
"BAR="
},
expected
:
"FOO=BAR="
,
},
{
env
:
Env
{
"FOO"
,
""
},
expected
:
"FOO="
,
},
}
for
_
,
test
:=
range
tests
{
if
s
:=
test
.
env
.
String
();
s
!=
test
.
expected
{
t
.
Errorf
(
"%v: expected string %q, got %q"
,
test
.
env
,
test
.
expected
,
s
)
}
}
}
func
TestEnvListToSlice
(
t
*
testing
.
T
)
{
tests
:=
[]
struct
{
env
EnvList
expected
[]
string
}{
{
env
:
EnvList
{
{
"FOO"
,
"BAR"
},
{
"ZEE"
,
"YO"
},
{
"ONE"
,
"1"
},
{
"EQUALS"
,
"=="
},
{
"EMPTY"
,
""
},
},
expected
:
[]
string
{
"FOO=BAR"
,
"ZEE=YO"
,
"ONE=1"
,
"EQUALS==="
,
"EMPTY="
},
},
}
for
_
,
test
:=
range
tests
{
if
s
:=
test
.
env
.
Slice
();
!
reflect
.
DeepEqual
(
test
.
expected
,
s
)
{
t
.
Errorf
(
"%v: expected %v, got %v"
,
test
.
env
,
test
.
expected
,
s
)
}
}
}
func
TestAddToEnvList
(
t
*
testing
.
T
)
{
tests
:=
[]
struct
{
add
[]
string
expected
EnvList
}{
{
add
:
[]
string
{
"FOO=BAR"
,
"EMPTY="
,
"EQUALS==="
,
"JUSTNAME"
},
expected
:
EnvList
{
{
"FOO"
,
"BAR"
},
{
"EMPTY"
,
""
},
{
"EQUALS"
,
"=="
},
{
"JUSTNAME"
,
""
},
},
},
}
for
_
,
test
:=
range
tests
{
env
:=
EnvList
{}
.
Merge
(
test
.
add
...
)
if
!
reflect
.
DeepEqual
(
test
.
expected
,
env
)
{
t
.
Errorf
(
"%v: expected %v, got %v"
,
test
.
add
,
test
.
expected
,
env
)
}
}
}
func
TestFlagToEnv
(
t
*
testing
.
T
)
{
flags
:=
pflag
.
NewFlagSet
(
""
,
pflag
.
ContinueOnError
)
flags
.
String
(
"test"
,
"ok"
,
""
)
flags
.
String
(
"kube-master"
,
"http://something"
,
""
)
flags
.
String
(
"from-file"
,
"default"
,
""
)
flags
.
Parse
([]
string
{
"--from-file=nondefault"
})
tests
:=
[]
struct
{
flag
*
pflag
.
Flag
prefix
string
expected
Env
}{
{
flag
:
flags
.
Lookup
(
"test"
),
expected
:
Env
{
"TEST"
,
"ok"
},
},
{
flag
:
flags
.
Lookup
(
"kube-master"
),
expected
:
Env
{
"KUBE_MASTER"
,
"http://something"
},
},
{
prefix
:
"KUBECTL_"
,
flag
:
flags
.
Lookup
(
"from-file"
),
expected
:
Env
{
"KUBECTL_FROM_FILE"
,
"nondefault"
},
},
}
for
_
,
test
:=
range
tests
{
if
env
:=
FlagToEnv
(
test
.
flag
,
test
.
prefix
);
!
reflect
.
DeepEqual
(
test
.
expected
,
env
)
{
t
.
Errorf
(
"%v: expected %v, got %v"
,
test
.
flag
.
Name
,
test
.
expected
,
env
)
}
}
}
func
TestPluginDescriptorEnvProvider
(
t
*
testing
.
T
)
{
tests
:=
[]
struct
{
plugin
*
Plugin
expected
EnvList
}{
{
plugin
:
&
Plugin
{
Description
:
Description
{
Name
:
"test"
,
ShortDesc
:
"Short Description"
,
Command
:
"foo --bar"
,
},
},
expected
:
EnvList
{
{
"KUBECTL_PLUGINS_DESCRIPTOR_NAME"
,
"test"
},
{
"KUBECTL_PLUGINS_DESCRIPTOR_SHORT_DESC"
,
"Short Description"
},
{
"KUBECTL_PLUGINS_DESCRIPTOR_LONG_DESC"
,
""
},
{
"KUBECTL_PLUGINS_DESCRIPTOR_EXAMPLE"
,
""
},
{
"KUBECTL_PLUGINS_DESCRIPTOR_COMMAND"
,
"foo --bar"
},
},
},
}
for
_
,
test
:=
range
tests
{
provider
:=
&
PluginDescriptorEnvProvider
{
Plugin
:
test
.
plugin
,
}
env
,
_
:=
provider
.
Env
()
if
!
reflect
.
DeepEqual
(
test
.
expected
,
env
)
{
t
.
Errorf
(
"%v: expected %v, got %v"
,
test
.
plugin
.
Name
,
test
.
expected
,
env
)
}
}
}
pkg/kubectl/plugins/runner.go
View file @
18cb56bf
...
...
@@ -34,12 +34,12 @@ type PluginRunner interface {
// in, out, and err streams, arguments and environment passed to it, and the
// working directory.
type
RunningContext
struct
{
In
io
.
Reader
Out
io
.
Writer
ErrOut
io
.
Writer
Args
[]
string
Env
[]
string
WorkingDir
string
In
io
.
Reader
Out
io
.
Writer
ErrOut
io
.
Writer
Args
[]
string
Env
Provider
EnvProvider
WorkingDir
string
}
// ExecPluginRunner is a PluginRunner that uses Go's os/exec to run plugins.
...
...
@@ -62,7 +62,11 @@ func (r *ExecPluginRunner) Run(plugin *Plugin, ctx RunningContext) error {
cmd
.
Stdout
=
ctx
.
Out
cmd
.
Stderr
=
ctx
.
ErrOut
cmd
.
Env
=
ctx
.
Env
env
,
err
:=
ctx
.
EnvProvider
.
Env
()
if
err
!=
nil
{
return
err
}
cmd
.
Env
=
env
.
Slice
()
cmd
.
Dir
=
ctx
.
WorkingDir
glog
.
V
(
9
)
.
Infof
(
"Running plugin %q as base command %q with args %v"
,
plugin
.
Name
,
base
,
args
)
...
...
pkg/kubectl/plugins/runner_test.go
View file @
18cb56bf
...
...
@@ -61,8 +61,9 @@ func TestExecRunner(t *testing.T) {
}
ctx
:=
RunningContext
{
Out
:
outBuf
,
WorkingDir
:
"."
,
Out
:
outBuf
,
WorkingDir
:
"."
,
EnvProvider
:
&
EmptyEnvProvider
{},
}
runner
:=
&
ExecPluginRunner
{}
...
...
test/fixtures/pkg/kubectl/plugins/env/env.sh
0 → 100755
View file @
18cb56bf
#!/bin/bash
# Copyright 2017 The Kubernetes Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
env
|
grep
'KUBECTL_PLUGINS'
|
sort
test/fixtures/pkg/kubectl/plugins/env/plugin.yaml
0 → 100644
View file @
18cb56bf
name
:
env
shortDesc
:
"
The
plugin
envs
plugin"
command
:
"
./env.sh"
test/fixtures/pkg/kubectl/plugins/tree/plugin.yaml
View file @
18cb56bf
...
...
@@ -3,11 +3,11 @@ shortDesc: "Plugin with a tree of commands"
tree
:
-
name
:
"
child1"
shortDesc
:
"
The
first
child
of
a
tree"
command
:
echo child
1
command
:
echo child
one
-
name
:
"
child2"
shortDesc
:
"
The
second
child
of
a
tree"
command
:
echo child
2
command
:
echo child
two
-
name
:
"
child3"
shortDesc
:
"
The
third
child
of
a
tree"
command
:
echo child
3
command
:
echo child
three
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment