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
d5d7d6d6
Commit
d5d7d6d6
authored
Dec 12, 2017
by
Michael Taufen
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Send an event just before the Kubelet restarts to use a new config
parent
b97e1760
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
107 additions
and
15 deletions
+107
-15
server.go
cmd/kubelet/app/server.go
+1
-1
BUILD
pkg/kubelet/kubeletconfig/BUILD
+3
-0
download.go
pkg/kubelet/kubeletconfig/checkpoint/download.go
+9
-0
download_test.go
pkg/kubelet/kubeletconfig/checkpoint/download_test.go
+15
-0
configsync.go
pkg/kubelet/kubeletconfig/configsync.go
+75
-11
controller.go
pkg/kubelet/kubeletconfig/controller.go
+3
-3
BUILD
test/e2e_node/BUILD
+1
-0
dynamic_kubelet_config_test.go
test/e2e_node/dynamic_kubelet_config_test.go
+0
-0
No files found.
cmd/kubelet/app/server.go
View file @
d5d7d6d6
...
@@ -401,7 +401,7 @@ func run(s *options.KubeletServer, kubeDeps *kubelet.Dependencies) (err error) {
...
@@ -401,7 +401,7 @@ func run(s *options.KubeletServer, kubeDeps *kubelet.Dependencies) (err error) {
// Alpha Dynamic Configuration Implementation;
// Alpha Dynamic Configuration Implementation;
// if the kubelet config controller is available, inject the latest to start the config and status sync loops
// if the kubelet config controller is available, inject the latest to start the config and status sync loops
if
utilfeature
.
DefaultFeatureGate
.
Enabled
(
features
.
DynamicKubeletConfig
)
&&
kubeDeps
.
KubeletConfigController
!=
nil
&&
!
standaloneMode
&&
!
s
.
RunOnce
{
if
utilfeature
.
DefaultFeatureGate
.
Enabled
(
features
.
DynamicKubeletConfig
)
&&
kubeDeps
.
KubeletConfigController
!=
nil
&&
!
standaloneMode
&&
!
s
.
RunOnce
{
kubeDeps
.
KubeletConfigController
.
StartSync
(
kubeDeps
.
KubeClient
,
string
(
nodeName
))
kubeDeps
.
KubeletConfigController
.
StartSync
(
kubeDeps
.
KubeClient
,
kubeDeps
.
EventClient
,
string
(
nodeName
))
}
}
if
kubeDeps
.
Auth
==
nil
{
if
kubeDeps
.
Auth
==
nil
{
...
...
pkg/kubelet/kubeletconfig/BUILD
View file @
d5d7d6d6
...
@@ -25,13 +25,16 @@ go_library(
...
@@ -25,13 +25,16 @@ go_library(
"//pkg/kubelet/kubeletconfig/util/log:go_default_library",
"//pkg/kubelet/kubeletconfig/util/log:go_default_library",
"//pkg/kubelet/kubeletconfig/util/panic:go_default_library",
"//pkg/kubelet/kubeletconfig/util/panic:go_default_library",
"//pkg/util/filesystem:go_default_library",
"//pkg/util/filesystem:go_default_library",
"//vendor/github.com/golang/glog:go_default_library",
"//vendor/k8s.io/api/core/v1:go_default_library",
"//vendor/k8s.io/api/core/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/fields:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/fields:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/types:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/wait:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/wait:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/watch:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/watch:go_default_library",
"//vendor/k8s.io/client-go/kubernetes:go_default_library",
"//vendor/k8s.io/client-go/kubernetes:go_default_library",
"//vendor/k8s.io/client-go/kubernetes/typed/core/v1:go_default_library",
"//vendor/k8s.io/client-go/tools/cache:go_default_library",
"//vendor/k8s.io/client-go/tools/cache:go_default_library",
],
],
)
)
...
...
pkg/kubelet/kubeletconfig/checkpoint/download.go
View file @
d5d7d6d6
...
@@ -34,6 +34,8 @@ import (
...
@@ -34,6 +34,8 @@ import (
type
RemoteConfigSource
interface
{
type
RemoteConfigSource
interface
{
// UID returns the UID of the remote config source object
// UID returns the UID of the remote config source object
UID
()
string
UID
()
string
// APIPath returns the API path to the remote resource, e.g. its SelfLink
APIPath
()
string
// Download downloads the remote config source object returns a Checkpoint backed by the object,
// Download downloads the remote config source object returns a Checkpoint backed by the object,
// or a sanitized failure reason and error if the download fails
// or a sanitized failure reason and error if the download fails
Download
(
client
clientset
.
Interface
)
(
Checkpoint
,
string
,
error
)
Download
(
client
clientset
.
Interface
)
(
Checkpoint
,
string
,
error
)
...
@@ -110,6 +112,13 @@ func (r *remoteConfigMap) UID() string {
...
@@ -110,6 +112,13 @@ func (r *remoteConfigMap) UID() string {
return
string
(
r
.
source
.
ConfigMapRef
.
UID
)
return
string
(
r
.
source
.
ConfigMapRef
.
UID
)
}
}
const
configMapAPIPathFmt
=
"/api/v1/namespaces/%s/configmaps/%s"
func
(
r
*
remoteConfigMap
)
APIPath
()
string
{
ref
:=
r
.
source
.
ConfigMapRef
return
fmt
.
Sprintf
(
configMapAPIPathFmt
,
ref
.
Namespace
,
ref
.
Name
)
}
func
(
r
*
remoteConfigMap
)
Download
(
client
clientset
.
Interface
)
(
Checkpoint
,
string
,
error
)
{
func
(
r
*
remoteConfigMap
)
Download
(
client
clientset
.
Interface
)
(
Checkpoint
,
string
,
error
)
{
var
reason
string
var
reason
string
uid
:=
string
(
r
.
source
.
ConfigMapRef
.
UID
)
uid
:=
string
(
r
.
source
.
ConfigMapRef
.
UID
)
...
...
pkg/kubelet/kubeletconfig/checkpoint/download_test.go
View file @
d5d7d6d6
...
@@ -17,6 +17,7 @@ limitations under the License.
...
@@ -17,6 +17,7 @@ limitations under the License.
package
checkpoint
package
checkpoint
import
(
import
(
"fmt"
"testing"
"testing"
"github.com/davecgh/go-spew/spew"
"github.com/davecgh/go-spew/spew"
...
@@ -92,6 +93,20 @@ func TestRemoteConfigMapUID(t *testing.T) {
...
@@ -92,6 +93,20 @@ func TestRemoteConfigMapUID(t *testing.T) {
}
}
}
}
func
TestRemoteConfigMapAPIPath
(
t
*
testing
.
T
)
{
name
:=
"name"
namespace
:=
"namespace"
cpt
:=
&
remoteConfigMap
{
&
apiv1
.
NodeConfigSource
{
ConfigMapRef
:
&
apiv1
.
ObjectReference
{
Name
:
name
,
Namespace
:
namespace
,
UID
:
""
}},
}
expect
:=
fmt
.
Sprintf
(
configMapAPIPathFmt
,
cpt
.
source
.
ConfigMapRef
.
Namespace
,
cpt
.
source
.
ConfigMapRef
.
Name
)
// APIPath() method should return the correct path to the referenced resource
path
:=
cpt
.
APIPath
()
if
expect
!=
path
{
t
.
Errorf
(
"expect APIPath() to return %q, but got %q"
,
expect
,
namespace
)
}
}
func
TestRemoteConfigMapDownload
(
t
*
testing
.
T
)
{
func
TestRemoteConfigMapDownload
(
t
*
testing
.
T
)
{
_
,
kubeletCodecs
,
err
:=
kubeletscheme
.
NewSchemeAndCodecs
()
_
,
kubeletCodecs
,
err
:=
kubeletscheme
.
NewSchemeAndCodecs
()
if
err
!=
nil
{
if
err
!=
nil
{
...
...
pkg/kubelet/kubeletconfig/configsync.go
View file @
d5d7d6d6
...
@@ -19,15 +19,30 @@ package kubeletconfig
...
@@ -19,15 +19,30 @@ package kubeletconfig
import
(
import
(
"fmt"
"fmt"
"os"
"os"
"time"
"github.com/golang/glog"
apiv1
"k8s.io/api/core/v1"
apiv1
"k8s.io/api/core/v1"
metav1
"k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
clientset
"k8s.io/client-go/kubernetes"
clientset
"k8s.io/client-go/kubernetes"
v1core
"k8s.io/client-go/kubernetes/typed/core/v1"
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/tools/cache"
"k8s.io/kubernetes/pkg/kubelet/kubeletconfig/checkpoint"
"k8s.io/kubernetes/pkg/kubelet/kubeletconfig/checkpoint"
"k8s.io/kubernetes/pkg/kubelet/kubeletconfig/status"
"k8s.io/kubernetes/pkg/kubelet/kubeletconfig/status"
utillog
"k8s.io/kubernetes/pkg/kubelet/kubeletconfig/util/log"
utillog
"k8s.io/kubernetes/pkg/kubelet/kubeletconfig/util/log"
)
)
const
(
// KubeletConfigChangedEventReason identifies an event as a change of Kubelet configuration
KubeletConfigChangedEventReason
=
"KubeletConfigChanged"
// EventMessageFmt is the message format for Kubelet config change events
EventMessageFmt
=
"Kubelet will restart to use: %s"
// LocalConfigMessage is the text to apply to EventMessageFmt when the Kubelet has been configured to use its local config (init or defaults)
LocalConfigMessage
=
"local config"
)
// pokeConfiSourceWorker tells the worker thread that syncs config sources that work needs to be done
// pokeConfiSourceWorker tells the worker thread that syncs config sources that work needs to be done
func
(
cc
*
Controller
)
pokeConfigSourceWorker
()
{
func
(
cc
*
Controller
)
pokeConfigSourceWorker
()
{
select
{
select
{
...
@@ -37,7 +52,7 @@ func (cc *Controller) pokeConfigSourceWorker() {
...
@@ -37,7 +52,7 @@ func (cc *Controller) pokeConfigSourceWorker() {
}
}
// syncConfigSource checks if work needs to be done to use a new configuration, and does that work if necessary
// syncConfigSource checks if work needs to be done to use a new configuration, and does that work if necessary
func
(
cc
*
Controller
)
syncConfigSource
(
client
clientset
.
Interface
,
nodeName
string
)
{
func
(
cc
*
Controller
)
syncConfigSource
(
client
clientset
.
Interface
,
eventClient
v1core
.
EventsGetter
,
nodeName
string
)
{
select
{
select
{
case
<-
cc
.
pendingConfigSource
:
case
<-
cc
.
pendingConfigSource
:
default
:
default
:
...
@@ -62,13 +77,22 @@ func (cc *Controller) syncConfigSource(client clientset.Interface, nodeName stri
...
@@ -62,13 +77,22 @@ func (cc *Controller) syncConfigSource(client clientset.Interface, nodeName stri
}
}
// check the Node and download any new config
// check the Node and download any new config
if
updated
,
reason
,
err
:=
cc
.
doSyncConfigSource
(
client
,
node
.
Spec
.
ConfigSource
);
err
!=
nil
{
if
updated
,
cur
,
reason
,
err
:=
cc
.
doSyncConfigSource
(
client
,
node
.
Spec
.
ConfigSource
);
err
!=
nil
{
cc
.
configOK
.
SetFailSyncCondition
(
reason
)
cc
.
configOK
.
SetFailSyncCondition
(
reason
)
syncerr
=
fmt
.
Errorf
(
"%s, error: %v"
,
reason
,
err
)
syncerr
=
fmt
.
Errorf
(
"%s, error: %v"
,
reason
,
err
)
return
return
}
else
if
updated
{
}
else
if
updated
{
// TODO(mtaufen): Consider adding a "currently restarting kubelet" ConfigOK message for this case
path
:=
LocalConfigMessage
utillog
.
Infof
(
"config updated, Kubelet will restart to begin using new config"
)
if
cur
!=
nil
{
path
=
cur
.
APIPath
()
}
// we directly log and send the event, instead of using the event recorder,
// because the event recorder won't flush its queue before we exit (we'd lose the event)
event
:=
eventf
(
nodeName
,
apiv1
.
EventTypeNormal
,
KubeletConfigChangedEventReason
,
EventMessageFmt
,
path
)
glog
.
V
(
3
)
.
Infof
(
"Event(%#v): type: '%v' reason: '%v' %v"
,
event
.
InvolvedObject
,
event
.
Type
,
event
.
Reason
,
event
.
Message
)
if
_
,
err
:=
eventClient
.
Events
(
apiv1
.
NamespaceDefault
)
.
Create
(
event
);
err
!=
nil
{
utillog
.
Errorf
(
"failed to send event, error: %v"
,
err
)
}
os
.
Exit
(
0
)
os
.
Exit
(
0
)
}
}
...
@@ -81,31 +105,31 @@ func (cc *Controller) syncConfigSource(client clientset.Interface, nodeName stri
...
@@ -81,31 +105,31 @@ func (cc *Controller) syncConfigSource(client clientset.Interface, nodeName stri
// doSyncConfigSource checkpoints and sets the store's current config to the new config or resets config,
// doSyncConfigSource checkpoints and sets the store's current config to the new config or resets config,
// depending on the `source`, and returns whether the current config in the checkpoint store was updated as a result
// depending on the `source`, and returns whether the current config in the checkpoint store was updated as a result
func
(
cc
*
Controller
)
doSyncConfigSource
(
client
clientset
.
Interface
,
source
*
apiv1
.
NodeConfigSource
)
(
bool
,
string
,
error
)
{
func
(
cc
*
Controller
)
doSyncConfigSource
(
client
clientset
.
Interface
,
source
*
apiv1
.
NodeConfigSource
)
(
bool
,
checkpoint
.
RemoteConfigSource
,
string
,
error
)
{
if
source
==
nil
{
if
source
==
nil
{
utillog
.
Infof
(
"Node.Spec.ConfigSource is empty, will reset current and last-known-good to defaults"
)
utillog
.
Infof
(
"Node.Spec.ConfigSource is empty, will reset current and last-known-good to defaults"
)
updated
,
reason
,
err
:=
cc
.
resetConfig
()
updated
,
reason
,
err
:=
cc
.
resetConfig
()
if
err
!=
nil
{
if
err
!=
nil
{
return
false
,
reason
,
err
return
false
,
nil
,
reason
,
err
}
}
return
updated
,
""
,
nil
return
updated
,
nil
,
""
,
nil
}
}
// if the NodeConfigSource is non-nil, download the config
// if the NodeConfigSource is non-nil, download the config
utillog
.
Infof
(
"Node.Spec.ConfigSource is non-empty, will checkpoint source and update config if necessary"
)
utillog
.
Infof
(
"Node.Spec.ConfigSource is non-empty, will checkpoint source and update config if necessary"
)
remote
,
reason
,
err
:=
checkpoint
.
NewRemoteConfigSource
(
source
)
remote
,
reason
,
err
:=
checkpoint
.
NewRemoteConfigSource
(
source
)
if
err
!=
nil
{
if
err
!=
nil
{
return
false
,
reason
,
err
return
false
,
nil
,
reason
,
err
}
}
reason
,
err
=
cc
.
checkpointConfigSource
(
client
,
remote
)
reason
,
err
=
cc
.
checkpointConfigSource
(
client
,
remote
)
if
err
!=
nil
{
if
err
!=
nil
{
return
false
,
reason
,
err
return
false
,
nil
,
reason
,
err
}
}
updated
,
reason
,
err
:=
cc
.
setCurrentConfig
(
remote
)
updated
,
reason
,
err
:=
cc
.
setCurrentConfig
(
remote
)
if
err
!=
nil
{
if
err
!=
nil
{
return
false
,
reason
,
err
return
false
,
nil
,
reason
,
err
}
}
return
updated
,
""
,
nil
return
updated
,
remote
,
""
,
nil
}
}
// checkpointConfigSource downloads and checkpoints the object referred to by `source` if the checkpoint does not already exist,
// checkpointConfigSource downloads and checkpoints the object referred to by `source` if the checkpoint does not already exist,
...
@@ -181,3 +205,43 @@ func latestNode(store cache.Store, nodeName string) (*apiv1.Node, error) {
...
@@ -181,3 +205,43 @@ func latestNode(store cache.Store, nodeName string) (*apiv1.Node, error) {
}
}
return
node
,
nil
return
node
,
nil
}
}
// eventf constructs and returns an event containing a formatted message
// similar to k8s.io/client-go/tools/record/event.go
func
eventf
(
nodeName
,
eventType
,
reason
,
messageFmt
string
,
args
...
interface
{})
*
apiv1
.
Event
{
return
makeEvent
(
nodeName
,
eventType
,
reason
,
fmt
.
Sprintf
(
messageFmt
,
args
...
))
}
// makeEvent constructs an event
// similar to makeEvent in k8s.io/client-go/tools/record/event.go
func
makeEvent
(
nodeName
,
eventtype
,
reason
,
message
string
)
*
apiv1
.
Event
{
const
componentKubelet
=
"kubelet"
// NOTE(mtaufen): This is consistent with pkg/kubelet/kubelet.go. Even though setting the node
// name as the UID looks strange, it appears to be conventional for events sent by the Kubelet.
ref
:=
apiv1
.
ObjectReference
{
Kind
:
"Node"
,
Name
:
nodeName
,
UID
:
types
.
UID
(
nodeName
),
Namespace
:
""
,
}
t
:=
metav1
.
Time
{
Time
:
time
.
Now
()}
namespace
:=
ref
.
Namespace
if
namespace
==
""
{
namespace
=
metav1
.
NamespaceDefault
}
return
&
apiv1
.
Event
{
ObjectMeta
:
metav1
.
ObjectMeta
{
Name
:
fmt
.
Sprintf
(
"%v.%x"
,
ref
.
Name
,
t
.
UnixNano
()),
Namespace
:
namespace
,
},
InvolvedObject
:
ref
,
Reason
:
reason
,
Message
:
message
,
FirstTimestamp
:
t
,
LastTimestamp
:
t
,
Count
:
1
,
Type
:
eventtype
,
Source
:
apiv1
.
EventSource
{
Component
:
componentKubelet
,
Host
:
string
(
nodeName
)},
}
}
pkg/kubelet/kubeletconfig/controller.go
View file @
d5d7d6d6
...
@@ -24,10 +24,10 @@ import (
...
@@ -24,10 +24,10 @@ import (
apiv1
"k8s.io/api/core/v1"
apiv1
"k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/apimachinery/pkg/util/wait"
clientset
"k8s.io/client-go/kubernetes"
clientset
"k8s.io/client-go/kubernetes"
v1core
"k8s.io/client-go/kubernetes/typed/core/v1"
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/tools/cache"
"k8s.io/kubernetes/pkg/kubelet/apis/kubeletconfig"
"k8s.io/kubernetes/pkg/kubelet/apis/kubeletconfig"
"k8s.io/kubernetes/pkg/kubelet/apis/kubeletconfig/validation"
"k8s.io/kubernetes/pkg/kubelet/apis/kubeletconfig/validation"
"k8s.io/kubernetes/pkg/kubelet/kubeletconfig/checkpoint/store"
"k8s.io/kubernetes/pkg/kubelet/kubeletconfig/checkpoint/store"
"k8s.io/kubernetes/pkg/kubelet/kubeletconfig/configfiles"
"k8s.io/kubernetes/pkg/kubelet/kubeletconfig/configfiles"
"k8s.io/kubernetes/pkg/kubelet/kubeletconfig/status"
"k8s.io/kubernetes/pkg/kubelet/kubeletconfig/status"
...
@@ -201,7 +201,7 @@ func (cc *Controller) Bootstrap() (*kubeletconfig.KubeletConfiguration, error) {
...
@@ -201,7 +201,7 @@ func (cc *Controller) Bootstrap() (*kubeletconfig.KubeletConfiguration, error) {
// StartSync launches the controller's sync loops if `client` is non-nil and `nodeName` is non-empty.
// StartSync launches the controller's sync loops if `client` is non-nil and `nodeName` is non-empty.
// It will always start the Node condition reporting loop, and will also start the dynamic conifg sync loops
// It will always start the Node condition reporting loop, and will also start the dynamic conifg sync loops
// if dynamic config is enabled on the controller. If `nodeName` is empty but `client` is non-nil, an error is logged.
// if dynamic config is enabled on the controller. If `nodeName` is empty but `client` is non-nil, an error is logged.
func
(
cc
*
Controller
)
StartSync
(
client
clientset
.
Interface
,
nodeName
string
)
{
func
(
cc
*
Controller
)
StartSync
(
client
clientset
.
Interface
,
eventClient
v1core
.
EventsGetter
,
nodeName
string
)
{
if
client
==
nil
{
if
client
==
nil
{
utillog
.
Infof
(
"nil client, will not start sync loops"
)
utillog
.
Infof
(
"nil client, will not start sync loops"
)
return
return
...
@@ -236,7 +236,7 @@ func (cc *Controller) StartSync(client clientset.Interface, nodeName string) {
...
@@ -236,7 +236,7 @@ func (cc *Controller) StartSync(client clientset.Interface, nodeName string) {
go
utilpanic
.
HandlePanic
(
func
()
{
go
utilpanic
.
HandlePanic
(
func
()
{
utillog
.
Infof
(
"starting config source sync loop"
)
utillog
.
Infof
(
"starting config source sync loop"
)
wait
.
JitterUntil
(
func
()
{
wait
.
JitterUntil
(
func
()
{
cc
.
syncConfigSource
(
client
,
nodeName
)
cc
.
syncConfigSource
(
client
,
eventClient
,
nodeName
)
},
10
*
time
.
Second
,
0.2
,
true
,
wait
.
NeverStop
)
},
10
*
time
.
Second
,
0.2
,
true
,
wait
.
NeverStop
)
})()
})()
}
else
{
}
else
{
...
...
test/e2e_node/BUILD
View file @
d5d7d6d6
...
@@ -128,6 +128,7 @@ go_test(
...
@@ -128,6 +128,7 @@ go_test(
"//pkg/kubelet/container:go_default_library",
"//pkg/kubelet/container:go_default_library",
"//pkg/kubelet/dockershim/libdocker:go_default_library",
"//pkg/kubelet/dockershim/libdocker:go_default_library",
"//pkg/kubelet/images:go_default_library",
"//pkg/kubelet/images:go_default_library",
"//pkg/kubelet/kubeletconfig:go_default_library",
"//pkg/kubelet/kubeletconfig/status:go_default_library",
"//pkg/kubelet/kubeletconfig/status:go_default_library",
"//pkg/kubelet/metrics:go_default_library",
"//pkg/kubelet/metrics:go_default_library",
"//pkg/kubelet/types:go_default_library",
"//pkg/kubelet/types:go_default_library",
...
...
test/e2e_node/dynamic_kubelet_config_test.go
View file @
d5d7d6d6
This diff is collapsed.
Click to expand it.
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