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
7f46d420
Commit
7f46d420
authored
Dec 23, 2014
by
Daniel Smith
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
add existence func and unit test
parent
16c624b2
Hide whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
406 additions
and
6 deletions
+406
-6
resource_printer.go
pkg/kubecfg/resource_printer.go
+94
-2
resource_printer_test.go
pkg/kubecfg/resource_printer_test.go
+110
-1
resource_printer.go
pkg/kubectl/resource_printer.go
+94
-2
resource_printer_test.go
pkg/kubectl/resource_printer_test.go
+108
-1
No files found.
pkg/kubecfg/resource_printer.go
View file @
7f46d420
...
@@ -332,7 +332,9 @@ type TemplatePrinter struct {
...
@@ -332,7 +332,9 @@ type TemplatePrinter struct {
}
}
func
NewTemplatePrinter
(
tmpl
[]
byte
)
(
*
TemplatePrinter
,
error
)
{
func
NewTemplatePrinter
(
tmpl
[]
byte
)
(
*
TemplatePrinter
,
error
)
{
t
,
err
:=
template
.
New
(
"output"
)
.
Parse
(
string
(
tmpl
))
t
,
err
:=
template
.
New
(
"output"
)
.
Funcs
(
template
.
FuncMap
{
"exists"
:
exists
})
.
Parse
(
string
(
tmpl
))
if
err
!=
nil
{
if
err
!=
nil
{
return
nil
,
err
return
nil
,
err
}
}
...
@@ -346,7 +348,7 @@ func (t *TemplatePrinter) Print(data []byte, w io.Writer) error {
...
@@ -346,7 +348,7 @@ func (t *TemplatePrinter) Print(data []byte, w io.Writer) error {
if
err
!=
nil
{
if
err
!=
nil
{
return
err
return
err
}
}
if
err
:=
t
.
template
.
Execute
(
w
,
out
);
err
!=
nil
{
if
err
:=
t
.
safe
Execute
(
w
,
out
);
err
!=
nil
{
// It is way easier to debug this stuff when it shows up in
// It is way easier to debug this stuff when it shows up in
// stdout instead of just stdin. So in addition to returning
// stdout instead of just stdin. So in addition to returning
// a nice error, also print useful stuff with the writer.
// a nice error, also print useful stuff with the writer.
...
@@ -367,3 +369,93 @@ func (t *TemplatePrinter) PrintObj(obj runtime.Object, w io.Writer) error {
...
@@ -367,3 +369,93 @@ func (t *TemplatePrinter) PrintObj(obj runtime.Object, w io.Writer) error {
}
}
return
t
.
Print
(
data
,
w
)
return
t
.
Print
(
data
,
w
)
}
}
// safeExecute tries to execute the template, but catches panics and returns an error
// should the template engine panic.
func
(
p
*
TemplatePrinter
)
safeExecute
(
w
io
.
Writer
,
obj
interface
{})
error
{
var
panicErr
error
// Sorry for the double anonymous function. There's probably a clever way
// to do this that has the defer'd func setting the value to be returned, but
// that would be even less obvious.
retErr
:=
func
()
error
{
defer
func
()
{
if
x
:=
recover
();
x
!=
nil
{
panicErr
=
fmt
.
Errorf
(
"caught panic: %+v"
,
x
)
}
}()
return
p
.
template
.
Execute
(
w
,
obj
)
}()
if
panicErr
!=
nil
{
return
panicErr
}
return
retErr
}
// exists returns true if it would be possible to call the index function
// with these arguments.
//
// TODO: how to document this for users?
//
// index returns the result of indexing its first argument by the following
// arguments. Thus "index x 1 2 3" is, in Go syntax, x[1][2][3]. Each
// indexed item must be a map, slice, or array.
func
exists
(
item
interface
{},
indices
...
interface
{})
bool
{
v
:=
reflect
.
ValueOf
(
item
)
for
_
,
i
:=
range
indices
{
index
:=
reflect
.
ValueOf
(
i
)
var
isNil
bool
if
v
,
isNil
=
indirect
(
v
);
isNil
{
return
false
}
switch
v
.
Kind
()
{
case
reflect
.
Array
,
reflect
.
Slice
,
reflect
.
String
:
var
x
int64
switch
index
.
Kind
()
{
case
reflect
.
Int
,
reflect
.
Int8
,
reflect
.
Int16
,
reflect
.
Int32
,
reflect
.
Int64
:
x
=
index
.
Int
()
case
reflect
.
Uint
,
reflect
.
Uint8
,
reflect
.
Uint16
,
reflect
.
Uint32
,
reflect
.
Uint64
,
reflect
.
Uintptr
:
x
=
int64
(
index
.
Uint
())
default
:
return
false
}
if
x
<
0
||
x
>=
int64
(
v
.
Len
())
{
return
false
}
v
=
v
.
Index
(
int
(
x
))
case
reflect
.
Map
:
if
!
index
.
IsValid
()
{
index
=
reflect
.
Zero
(
v
.
Type
()
.
Key
())
}
if
!
index
.
Type
()
.
AssignableTo
(
v
.
Type
()
.
Key
())
{
return
false
}
if
x
:=
v
.
MapIndex
(
index
);
x
.
IsValid
()
{
v
=
x
}
else
{
v
=
reflect
.
Zero
(
v
.
Type
()
.
Elem
())
}
default
:
return
false
}
}
if
_
,
isNil
:=
indirect
(
v
);
isNil
{
return
false
}
return
true
}
// stolen from text/template
// indirect returns the item at the end of indirection, and a bool to indicate if it's nil.
// We indirect through pointers and empty interfaces (only) because
// non-empty interfaces have methods we might need.
func
indirect
(
v
reflect
.
Value
)
(
rv
reflect
.
Value
,
isNil
bool
)
{
for
;
v
.
Kind
()
==
reflect
.
Ptr
||
v
.
Kind
()
==
reflect
.
Interface
;
v
=
v
.
Elem
()
{
if
v
.
IsNil
()
{
return
v
,
true
}
if
v
.
Kind
()
==
reflect
.
Interface
&&
v
.
NumMethod
()
>
0
{
break
}
}
return
v
,
false
}
pkg/kubecfg/resource_printer_test.go
View file @
7f46d420
...
@@ -26,6 +26,8 @@ import (
...
@@ -26,6 +26,8 @@ import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
"github.com/ghodss/yaml"
"github.com/ghodss/yaml"
)
)
...
@@ -180,7 +182,6 @@ func TestTemplateEmitsVersionedObjects(t *testing.T) {
...
@@ -180,7 +182,6 @@ func TestTemplateEmitsVersionedObjects(t *testing.T) {
func
TestTemplatePanic
(
t
*
testing
.
T
)
{
func
TestTemplatePanic
(
t
*
testing
.
T
)
{
tmpl
:=
`{{and ((index .currentState.info "update-demo").state.running.startedAt) .currentState.info.net.state.running.startedAt}}`
tmpl
:=
`{{and ((index .currentState.info "update-demo").state.running.startedAt) .currentState.info.net.state.running.startedAt}}`
// kind is always blank in memory and set on the wire
printer
,
err
:=
NewTemplatePrinter
([]
byte
(
tmpl
))
printer
,
err
:=
NewTemplatePrinter
([]
byte
(
tmpl
))
if
err
!=
nil
{
if
err
!=
nil
{
t
.
Fatalf
(
"tmpl fail: %v"
,
err
)
t
.
Fatalf
(
"tmpl fail: %v"
,
err
)
...
@@ -194,3 +195,111 @@ func TestTemplatePanic(t *testing.T) {
...
@@ -194,3 +195,111 @@ func TestTemplatePanic(t *testing.T) {
t
.
Errorf
(
"no debugging info was printed"
)
t
.
Errorf
(
"no debugging info was printed"
)
}
}
}
}
func
TestTemplateStrings
(
t
*
testing
.
T
)
{
// This unit tests the "exists" function as well as the template from update.sh
table
:=
map
[
string
]
struct
{
pod
api
.
Pod
expect
string
}{
"nilInfo"
:
{
api
.
Pod
{},
"false"
},
"emptyInfo"
:
{
api
.
Pod
{
Status
:
api
.
PodStatus
{
Info
:
api
.
PodInfo
{}}},
"false"
},
"containerExists"
:
{
api
.
Pod
{
Status
:
api
.
PodStatus
{
Info
:
api
.
PodInfo
{
"update-demo"
:
api
.
ContainerStatus
{}},
},
},
"false"
,
},
"netExists"
:
{
api
.
Pod
{
Status
:
api
.
PodStatus
{
Info
:
api
.
PodInfo
{
"net"
:
api
.
ContainerStatus
{}},
},
},
"false"
,
},
"bothExist"
:
{
api
.
Pod
{
Status
:
api
.
PodStatus
{
Info
:
api
.
PodInfo
{
"update-demo"
:
api
.
ContainerStatus
{},
"net"
:
api
.
ContainerStatus
{},
},
},
},
"false"
,
},
"oneValid"
:
{
api
.
Pod
{
Status
:
api
.
PodStatus
{
Info
:
api
.
PodInfo
{
"update-demo"
:
api
.
ContainerStatus
{},
"net"
:
api
.
ContainerStatus
{
State
:
api
.
ContainerState
{
Running
:
&
api
.
ContainerStateRunning
{
StartedAt
:
util
.
Time
{},
},
},
},
},
},
},
"false"
,
},
"bothValid"
:
{
api
.
Pod
{
Status
:
api
.
PodStatus
{
Info
:
api
.
PodInfo
{
"update-demo"
:
api
.
ContainerStatus
{
State
:
api
.
ContainerState
{
Running
:
&
api
.
ContainerStateRunning
{
StartedAt
:
util
.
Time
{},
},
},
},
"net"
:
api
.
ContainerStatus
{
State
:
api
.
ContainerState
{
Running
:
&
api
.
ContainerStateRunning
{
StartedAt
:
util
.
Time
{},
},
},
},
},
},
},
"true"
,
},
}
// The point of this test is to verify that the below template works. If you change this
// template, you need to update hack/e2e-suite/update.sh.
tmpl
:=
`{{and (exists . "currentState" "info" "update-demo" "state" "running") (exists . "currentState" "info" "net" "state" "running")}}`
useThisToDebug
:=
`
a: {{exists . "currentState"}}
b: {{exists . "currentState" "info"}}
c: {{exists . "currentState" "info" "update-demo"}}
d: {{exists . "currentState" "info" "update-demo" "state"}}
e: {{exists . "currentState" "info" "update-demo" "state" "running"}}
f: {{exists . "currentState" "info" "update-demo" "state" "running" "startedAt"}}`
_
=
useThisToDebug
// don't complain about unused var
printer
,
err
:=
NewTemplatePrinter
([]
byte
(
tmpl
))
if
err
!=
nil
{
t
.
Fatalf
(
"tmpl fail: %v"
,
err
)
}
for
name
,
item
:=
range
table
{
buffer
:=
&
bytes
.
Buffer
{}
err
=
printer
.
PrintObj
(
&
item
.
pod
,
buffer
)
if
err
!=
nil
{
t
.
Errorf
(
"%v: unexpected err: %v"
,
name
,
err
)
continue
}
if
e
,
a
:=
item
.
expect
,
buffer
.
String
();
e
!=
a
{
t
.
Errorf
(
"%v: expected %v, got %v"
,
name
,
e
,
a
)
}
}
}
pkg/kubectl/resource_printer.go
View file @
7f46d420
...
@@ -390,7 +390,9 @@ type TemplatePrinter struct {
...
@@ -390,7 +390,9 @@ type TemplatePrinter struct {
}
}
func
NewTemplatePrinter
(
tmpl
[]
byte
,
asVersion
string
,
convertor
runtime
.
ObjectConvertor
)
(
*
TemplatePrinter
,
error
)
{
func
NewTemplatePrinter
(
tmpl
[]
byte
,
asVersion
string
,
convertor
runtime
.
ObjectConvertor
)
(
*
TemplatePrinter
,
error
)
{
t
,
err
:=
template
.
New
(
"output"
)
.
Parse
(
string
(
tmpl
))
t
,
err
:=
template
.
New
(
"output"
)
.
Funcs
(
template
.
FuncMap
{
"exists"
:
exists
})
.
Parse
(
string
(
tmpl
))
if
err
!=
nil
{
if
err
!=
nil
{
return
nil
,
err
return
nil
,
err
}
}
...
@@ -416,7 +418,7 @@ func (p *TemplatePrinter) PrintObj(obj runtime.Object, w io.Writer) error {
...
@@ -416,7 +418,7 @@ func (p *TemplatePrinter) PrintObj(obj runtime.Object, w io.Writer) error {
if
err
:=
json
.
Unmarshal
(
data
,
&
out
);
err
!=
nil
{
if
err
:=
json
.
Unmarshal
(
data
,
&
out
);
err
!=
nil
{
return
err
return
err
}
}
if
err
=
p
.
template
.
Execute
(
w
,
out
);
err
!=
nil
{
if
err
=
p
.
safe
Execute
(
w
,
out
);
err
!=
nil
{
// It is way easier to debug this stuff when it shows up in
// It is way easier to debug this stuff when it shows up in
// stdout instead of just stdin. So in addition to returning
// stdout instead of just stdin. So in addition to returning
// a nice error, also print useful stuff with the writer.
// a nice error, also print useful stuff with the writer.
...
@@ -429,6 +431,27 @@ func (p *TemplatePrinter) PrintObj(obj runtime.Object, w io.Writer) error {
...
@@ -429,6 +431,27 @@ func (p *TemplatePrinter) PrintObj(obj runtime.Object, w io.Writer) error {
return
nil
return
nil
}
}
// safeExecute tries to execute the template, but catches panics and returns an error
// should the template engine panic.
func
(
p
*
TemplatePrinter
)
safeExecute
(
w
io
.
Writer
,
obj
interface
{})
error
{
var
panicErr
error
// Sorry for the double anonymous function. There's probably a clever way
// to do this that has the defer'd func setting the value to be returned, but
// that would be even less obvious.
retErr
:=
func
()
error
{
defer
func
()
{
if
x
:=
recover
();
x
!=
nil
{
panicErr
=
fmt
.
Errorf
(
"caught panic: %+v"
,
x
)
}
}()
return
p
.
template
.
Execute
(
w
,
obj
)
}()
if
panicErr
!=
nil
{
return
panicErr
}
return
retErr
}
func
tabbedString
(
f
func
(
io
.
Writer
)
error
)
(
string
,
error
)
{
func
tabbedString
(
f
func
(
io
.
Writer
)
error
)
(
string
,
error
)
{
out
:=
new
(
tabwriter
.
Writer
)
out
:=
new
(
tabwriter
.
Writer
)
buf
:=
&
bytes
.
Buffer
{}
buf
:=
&
bytes
.
Buffer
{}
...
@@ -443,3 +466,72 @@ func tabbedString(f func(io.Writer) error) (string, error) {
...
@@ -443,3 +466,72 @@ func tabbedString(f func(io.Writer) error) (string, error) {
str
:=
string
(
buf
.
String
())
str
:=
string
(
buf
.
String
())
return
str
,
nil
return
str
,
nil
}
}
// exists returns true if it would be possible to call the index function
// with these arguments.
//
// TODO: how to document this for users?
//
// index returns the result of indexing its first argument by the following
// arguments. Thus "index x 1 2 3" is, in Go syntax, x[1][2][3]. Each
// indexed item must be a map, slice, or array.
func
exists
(
item
interface
{},
indices
...
interface
{})
bool
{
v
:=
reflect
.
ValueOf
(
item
)
for
_
,
i
:=
range
indices
{
index
:=
reflect
.
ValueOf
(
i
)
var
isNil
bool
if
v
,
isNil
=
indirect
(
v
);
isNil
{
return
false
}
switch
v
.
Kind
()
{
case
reflect
.
Array
,
reflect
.
Slice
,
reflect
.
String
:
var
x
int64
switch
index
.
Kind
()
{
case
reflect
.
Int
,
reflect
.
Int8
,
reflect
.
Int16
,
reflect
.
Int32
,
reflect
.
Int64
:
x
=
index
.
Int
()
case
reflect
.
Uint
,
reflect
.
Uint8
,
reflect
.
Uint16
,
reflect
.
Uint32
,
reflect
.
Uint64
,
reflect
.
Uintptr
:
x
=
int64
(
index
.
Uint
())
default
:
return
false
}
if
x
<
0
||
x
>=
int64
(
v
.
Len
())
{
return
false
}
v
=
v
.
Index
(
int
(
x
))
case
reflect
.
Map
:
if
!
index
.
IsValid
()
{
index
=
reflect
.
Zero
(
v
.
Type
()
.
Key
())
}
if
!
index
.
Type
()
.
AssignableTo
(
v
.
Type
()
.
Key
())
{
return
false
}
if
x
:=
v
.
MapIndex
(
index
);
x
.
IsValid
()
{
v
=
x
}
else
{
v
=
reflect
.
Zero
(
v
.
Type
()
.
Elem
())
}
default
:
return
false
}
}
if
_
,
isNil
:=
indirect
(
v
);
isNil
{
return
false
}
return
true
}
// stolen from text/template
// indirect returns the item at the end of indirection, and a bool to indicate if it's nil.
// We indirect through pointers and empty interfaces (only) because
// non-empty interfaces have methods we might need.
func
indirect
(
v
reflect
.
Value
)
(
rv
reflect
.
Value
,
isNil
bool
)
{
for
;
v
.
Kind
()
==
reflect
.
Ptr
||
v
.
Kind
()
==
reflect
.
Interface
;
v
=
v
.
Elem
()
{
if
v
.
IsNil
()
{
return
v
,
true
}
if
v
.
Kind
()
==
reflect
.
Interface
&&
v
.
NumMethod
()
>
0
{
break
}
}
return
v
,
false
}
pkg/kubectl/resource_printer_test.go
View file @
7f46d420
...
@@ -292,7 +292,6 @@ func TestTemplateEmitsVersionedObjects(t *testing.T) {
...
@@ -292,7 +292,6 @@ func TestTemplateEmitsVersionedObjects(t *testing.T) {
func
TestTemplatePanic
(
t
*
testing
.
T
)
{
func
TestTemplatePanic
(
t
*
testing
.
T
)
{
tmpl
:=
`{{and ((index .currentState.info "update-demo").state.running.startedAt) .currentState.info.net.state.running.startedAt}}`
tmpl
:=
`{{and ((index .currentState.info "update-demo").state.running.startedAt) .currentState.info.net.state.running.startedAt}}`
// kind is always blank in memory and set on the wire
printer
,
err
:=
NewTemplatePrinter
([]
byte
(
tmpl
),
testapi
.
Version
(),
api
.
Scheme
)
printer
,
err
:=
NewTemplatePrinter
([]
byte
(
tmpl
),
testapi
.
Version
(),
api
.
Scheme
)
if
err
!=
nil
{
if
err
!=
nil
{
t
.
Fatalf
(
"tmpl fail: %v"
,
err
)
t
.
Fatalf
(
"tmpl fail: %v"
,
err
)
...
@@ -307,6 +306,114 @@ func TestTemplatePanic(t *testing.T) {
...
@@ -307,6 +306,114 @@ func TestTemplatePanic(t *testing.T) {
}
}
}
}
func
TestTemplateStrings
(
t
*
testing
.
T
)
{
// This unit tests the "exists" function as well as the template from update.sh
table
:=
map
[
string
]
struct
{
pod
api
.
Pod
expect
string
}{
"nilInfo"
:
{
api
.
Pod
{},
"false"
},
"emptyInfo"
:
{
api
.
Pod
{
Status
:
api
.
PodStatus
{
Info
:
api
.
PodInfo
{}}},
"false"
},
"containerExists"
:
{
api
.
Pod
{
Status
:
api
.
PodStatus
{
Info
:
api
.
PodInfo
{
"update-demo"
:
api
.
ContainerStatus
{}},
},
},
"false"
,
},
"netExists"
:
{
api
.
Pod
{
Status
:
api
.
PodStatus
{
Info
:
api
.
PodInfo
{
"net"
:
api
.
ContainerStatus
{}},
},
},
"false"
,
},
"bothExist"
:
{
api
.
Pod
{
Status
:
api
.
PodStatus
{
Info
:
api
.
PodInfo
{
"update-demo"
:
api
.
ContainerStatus
{},
"net"
:
api
.
ContainerStatus
{},
},
},
},
"false"
,
},
"oneValid"
:
{
api
.
Pod
{
Status
:
api
.
PodStatus
{
Info
:
api
.
PodInfo
{
"update-demo"
:
api
.
ContainerStatus
{},
"net"
:
api
.
ContainerStatus
{
State
:
api
.
ContainerState
{
Running
:
&
api
.
ContainerStateRunning
{
StartedAt
:
util
.
Time
{},
},
},
},
},
},
},
"false"
,
},
"bothValid"
:
{
api
.
Pod
{
Status
:
api
.
PodStatus
{
Info
:
api
.
PodInfo
{
"update-demo"
:
api
.
ContainerStatus
{
State
:
api
.
ContainerState
{
Running
:
&
api
.
ContainerStateRunning
{
StartedAt
:
util
.
Time
{},
},
},
},
"net"
:
api
.
ContainerStatus
{
State
:
api
.
ContainerState
{
Running
:
&
api
.
ContainerStateRunning
{
StartedAt
:
util
.
Time
{},
},
},
},
},
},
},
"true"
,
},
}
// The point of this test is to verify that the below template works. If you change this
// template, you need to update hack/e2e-suite/update.sh.
tmpl
:=
`{{and (exists . "currentState" "info" "update-demo" "state" "running") (exists . "currentState" "info" "net" "state" "running")}}`
useThisToDebug
:=
`
a: {{exists . "currentState"}}
b: {{exists . "currentState" "info"}}
c: {{exists . "currentState" "info" "update-demo"}}
d: {{exists . "currentState" "info" "update-demo" "state"}}
e: {{exists . "currentState" "info" "update-demo" "state" "running"}}
f: {{exists . "currentState" "info" "update-demo" "state" "running" "startedAt"}}`
_
=
useThisToDebug
// don't complain about unused var
printer
,
err
:=
NewTemplatePrinter
([]
byte
(
tmpl
),
"v1beta1"
,
api
.
Scheme
)
if
err
!=
nil
{
t
.
Fatalf
(
"tmpl fail: %v"
,
err
)
}
for
name
,
item
:=
range
table
{
buffer
:=
&
bytes
.
Buffer
{}
err
=
printer
.
PrintObj
(
&
item
.
pod
,
buffer
)
if
err
!=
nil
{
t
.
Errorf
(
"%v: unexpected err: %v"
,
name
,
err
)
continue
}
if
e
,
a
:=
item
.
expect
,
buffer
.
String
();
e
!=
a
{
t
.
Errorf
(
"%v: expected %v, got %v"
,
name
,
e
,
a
)
}
}
}
func
TestPrinters
(
t
*
testing
.
T
)
{
func
TestPrinters
(
t
*
testing
.
T
)
{
om
:=
func
(
name
string
)
api
.
ObjectMeta
{
return
api
.
ObjectMeta
{
Name
:
name
}
}
om
:=
func
(
name
string
)
api
.
ObjectMeta
{
return
api
.
ObjectMeta
{
Name
:
name
}
}
templatePrinter
,
err
:=
NewTemplatePrinter
([]
byte
(
"{{.name}}"
),
testapi
.
Version
(),
api
.
Scheme
)
templatePrinter
,
err
:=
NewTemplatePrinter
([]
byte
(
"{{.name}}"
),
testapi
.
Version
(),
api
.
Scheme
)
...
...
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