Commit 5af81b09 authored by Kubernetes Submit Queue's avatar Kubernetes Submit Queue Committed by GitHub

Merge pull request #42173 from enisoc/controller-ref-ds

Automatic merge from submit-queue (batch tested with PRs 42692, 42169, 42173) DaemonSet: Respect ControllerRef **What this PR does / why we need it**: This is part of the completion of the [ControllerRef](https://github.com/kubernetes/community/blob/master/contributors/design-proposals/controller-ref.md) proposal. It brings DaemonSet into full compliance with ControllerRef. See the individual commit messages for details. **Which issue this PR fixes**: This ensures that DaemonSet does not fight with other controllers over control of Pods. **Special notes for your reviewer**: **Release note**: ```release-note DaemonSet now respects ControllerRef to avoid fighting over Pods. ``` cc @erictune @kubernetes/sig-apps-pr-reviews
parents 5bc7387b fac372d0
...@@ -36,7 +36,6 @@ func startDaemonSetController(ctx ControllerContext) (bool, error) { ...@@ -36,7 +36,6 @@ func startDaemonSetController(ctx ControllerContext) (bool, error) {
ctx.InformerFactory.Core().V1().Pods(), ctx.InformerFactory.Core().V1().Pods(),
ctx.InformerFactory.Core().V1().Nodes(), ctx.InformerFactory.Core().V1().Nodes(),
ctx.ClientBuilder.ClientOrDie("daemon-set-controller"), ctx.ClientBuilder.ClientOrDie("daemon-set-controller"),
int(ctx.Options.LookupCacheSizeForDaemonSet),
).Run(int(ctx.Options.ConcurrentDaemonSetSyncs), ctx.Stop) ).Run(int(ctx.Options.ConcurrentDaemonSetSyncs), ctx.Stop)
return true, nil return true, nil
} }
......
...@@ -134,7 +134,9 @@ func (s *CMServer) AddFlags(fs *pflag.FlagSet, allControllers []string, disabled ...@@ -134,7 +134,9 @@ func (s *CMServer) AddFlags(fs *pflag.FlagSet, allControllers []string, disabled
fs.Int32Var(&s.ConcurrentSATokenSyncs, "concurrent-serviceaccount-token-syncs", s.ConcurrentSATokenSyncs, "The number of service account token objects that are allowed to sync concurrently. Larger number = more responsive token generation, but more CPU (and network) load") fs.Int32Var(&s.ConcurrentSATokenSyncs, "concurrent-serviceaccount-token-syncs", s.ConcurrentSATokenSyncs, "The number of service account token objects that are allowed to sync concurrently. Larger number = more responsive token generation, but more CPU (and network) load")
fs.Int32Var(&s.LookupCacheSizeForRC, "replication-controller-lookup-cache-size", s.LookupCacheSizeForRC, "This flag is deprecated and will be removed in future releases. ReplicationController no longer requires a lookup cache.") fs.Int32Var(&s.LookupCacheSizeForRC, "replication-controller-lookup-cache-size", s.LookupCacheSizeForRC, "This flag is deprecated and will be removed in future releases. ReplicationController no longer requires a lookup cache.")
fs.Int32Var(&s.LookupCacheSizeForRS, "replicaset-lookup-cache-size", s.LookupCacheSizeForRS, "This flag is deprecated and will be removed in future releases. ReplicaSet no longer requires a lookup cache.") fs.Int32Var(&s.LookupCacheSizeForRS, "replicaset-lookup-cache-size", s.LookupCacheSizeForRS, "This flag is deprecated and will be removed in future releases. ReplicaSet no longer requires a lookup cache.")
fs.Int32Var(&s.LookupCacheSizeForDaemonSet, "daemonset-lookup-cache-size", s.LookupCacheSizeForDaemonSet, "The the size of lookup cache for daemonsets. Larger number = more responsive daemonsets, but more MEM load.") // TODO: Remove the following flag 6 months after v1.6.0 is released.
fs.Int32Var(&s.LookupCacheSizeForDaemonSet, "daemonset-lookup-cache-size", s.LookupCacheSizeForDaemonSet, "This flag is deprecated and will be removed in future releases. DaemonSet no longer requires a lookup cache.")
fs.MarkDeprecated("daemonset-lookup-cache-size", "This flag is deprecated and will be removed in future releases. DaemonSet no longer requires a lookup cache.")
fs.DurationVar(&s.ServiceSyncPeriod.Duration, "service-sync-period", s.ServiceSyncPeriod.Duration, "The period for syncing services with their external load balancers") fs.DurationVar(&s.ServiceSyncPeriod.Duration, "service-sync-period", s.ServiceSyncPeriod.Duration, "The period for syncing services with their external load balancers")
fs.DurationVar(&s.NodeSyncPeriod.Duration, "node-sync-period", 0, ""+ fs.DurationVar(&s.NodeSyncPeriod.Duration, "node-sync-period", 0, ""+
"This flag is deprecated and will be removed in future releases. See node-monitor-period for Node health checking or "+ "This flag is deprecated and will be removed in future releases. See node-monitor-period for Node health checking or "+
......
...@@ -690,6 +690,7 @@ type KubeControllerManagerConfiguration struct { ...@@ -690,6 +690,7 @@ type KubeControllerManagerConfiguration struct {
LookupCacheSizeForRS int32 LookupCacheSizeForRS int32
// lookupCacheSizeForDaemonSet is the size of lookup cache for daemonsets. // lookupCacheSizeForDaemonSet is the size of lookup cache for daemonsets.
// Larger number = more responsive daemonset, but more MEM load. // Larger number = more responsive daemonset, but more MEM load.
// DEPRECATED: This is no longer used.
LookupCacheSizeForDaemonSet int32 LookupCacheSizeForDaemonSet int32
// serviceSyncPeriod is the period for syncing services with their external // serviceSyncPeriod is the period for syncing services with their external
// load balancers. // load balancers.
......
...@@ -35,8 +35,9 @@ type DaemonSetListerExpansion interface { ...@@ -35,8 +35,9 @@ type DaemonSetListerExpansion interface {
// DaemonSetNamespaeLister. // DaemonSetNamespaeLister.
type DaemonSetNamespaceListerExpansion interface{} type DaemonSetNamespaceListerExpansion interface{}
// GetPodDaemonSets returns a list of daemon sets managing a pod. // GetPodDaemonSets returns a list of DaemonSets that potentially match a pod.
// Returns an error if and only if no matching daemon sets are found. // Only the one specified in the Pod's ControllerRef will actually manage it.
// Returns an error only if no matching DaemonSets are found.
func (s *daemonSetLister) GetPodDaemonSets(pod *api.Pod) ([]*extensions.DaemonSet, error) { func (s *daemonSetLister) GetPodDaemonSets(pod *api.Pod) ([]*extensions.DaemonSet, error) {
var selector labels.Selector var selector labels.Selector
var daemonSet *extensions.DaemonSet var daemonSet *extensions.DaemonSet
......
...@@ -35,8 +35,9 @@ type DaemonSetListerExpansion interface { ...@@ -35,8 +35,9 @@ type DaemonSetListerExpansion interface {
// DaemonSetNamespaeLister. // DaemonSetNamespaeLister.
type DaemonSetNamespaceListerExpansion interface{} type DaemonSetNamespaceListerExpansion interface{}
// GetPodDaemonSets returns a list of daemon sets managing a pod. // GetPodDaemonSets returns a list of DaemonSets that potentially match a pod.
// Returns an error if and only if no matching daemon sets are found. // Only the one specified in the Pod's ControllerRef will actually manage it.
// Returns an error only if no matching DaemonSets are found.
func (s *daemonSetLister) GetPodDaemonSets(pod *v1.Pod) ([]*v1beta1.DaemonSet, error) { func (s *daemonSetLister) GetPodDaemonSets(pod *v1.Pod) ([]*v1beta1.DaemonSet, error) {
var selector labels.Selector var selector labels.Selector
var daemonSet *v1beta1.DaemonSet var daemonSet *v1beta1.DaemonSet
......
...@@ -400,8 +400,9 @@ func (r RealRSControl) PatchReplicaSet(namespace, name string, data []byte) erro ...@@ -400,8 +400,9 @@ func (r RealRSControl) PatchReplicaSet(namespace, name string, data []byte) erro
type PodControlInterface interface { type PodControlInterface interface {
// CreatePods creates new pods according to the spec. // CreatePods creates new pods according to the spec.
CreatePods(namespace string, template *v1.PodTemplateSpec, object runtime.Object) error CreatePods(namespace string, template *v1.PodTemplateSpec, object runtime.Object) error
// CreatePodsOnNode creates a new pod according to the spec on the specified node. // CreatePodsOnNode creates a new pod according to the spec on the specified node,
CreatePodsOnNode(nodeName, namespace string, template *v1.PodTemplateSpec, object runtime.Object) error // and sets the ControllerRef.
CreatePodsOnNode(nodeName, namespace string, template *v1.PodTemplateSpec, object runtime.Object, controllerRef *metav1.OwnerReference) error
// CreatePodsWithControllerRef creates new pods according to the spec, and sets object as the pod's controller. // CreatePodsWithControllerRef creates new pods according to the spec, and sets object as the pod's controller.
CreatePodsWithControllerRef(namespace string, template *v1.PodTemplateSpec, object runtime.Object, controllerRef *metav1.OwnerReference) error CreatePodsWithControllerRef(namespace string, template *v1.PodTemplateSpec, object runtime.Object, controllerRef *metav1.OwnerReference) error
// DeletePod deletes the pod identified by podID. // DeletePod deletes the pod identified by podID.
...@@ -466,11 +467,7 @@ func getPodsPrefix(controllerName string) string { ...@@ -466,11 +467,7 @@ func getPodsPrefix(controllerName string) string {
return prefix return prefix
} }
func (r RealPodControl) CreatePods(namespace string, template *v1.PodTemplateSpec, object runtime.Object) error { func validateControllerRef(controllerRef *metav1.OwnerReference) error {
return r.createPods("", namespace, template, object, nil)
}
func (r RealPodControl) CreatePodsWithControllerRef(namespace string, template *v1.PodTemplateSpec, controllerObject runtime.Object, controllerRef *metav1.OwnerReference) error {
if controllerRef == nil { if controllerRef == nil {
return fmt.Errorf("controllerRef is nil") return fmt.Errorf("controllerRef is nil")
} }
...@@ -481,16 +478,30 @@ func (r RealPodControl) CreatePodsWithControllerRef(namespace string, template * ...@@ -481,16 +478,30 @@ func (r RealPodControl) CreatePodsWithControllerRef(namespace string, template *
return fmt.Errorf("controllerRef has empty Kind") return fmt.Errorf("controllerRef has empty Kind")
} }
if controllerRef.Controller == nil || *controllerRef.Controller != true { if controllerRef.Controller == nil || *controllerRef.Controller != true {
return fmt.Errorf("controllerRef.Controller is not set") return fmt.Errorf("controllerRef.Controller is not set to true")
} }
if controllerRef.BlockOwnerDeletion == nil || *controllerRef.BlockOwnerDeletion != true { if controllerRef.BlockOwnerDeletion == nil || *controllerRef.BlockOwnerDeletion != true {
return fmt.Errorf("controllerRef.BlockOwnerDeletion is not set") return fmt.Errorf("controllerRef.BlockOwnerDeletion is not set")
} }
return nil
}
func (r RealPodControl) CreatePods(namespace string, template *v1.PodTemplateSpec, object runtime.Object) error {
return r.createPods("", namespace, template, object, nil)
}
func (r RealPodControl) CreatePodsWithControllerRef(namespace string, template *v1.PodTemplateSpec, controllerObject runtime.Object, controllerRef *metav1.OwnerReference) error {
if err := validateControllerRef(controllerRef); err != nil {
return err
}
return r.createPods("", namespace, template, controllerObject, controllerRef) return r.createPods("", namespace, template, controllerObject, controllerRef)
} }
func (r RealPodControl) CreatePodsOnNode(nodeName, namespace string, template *v1.PodTemplateSpec, object runtime.Object) error { func (r RealPodControl) CreatePodsOnNode(nodeName, namespace string, template *v1.PodTemplateSpec, object runtime.Object, controllerRef *metav1.OwnerReference) error {
return r.createPods(nodeName, namespace, template, object, nil) if err := validateControllerRef(controllerRef); err != nil {
return err
}
return r.createPods(nodeName, namespace, template, object, controllerRef)
} }
func (r RealPodControl) PatchPod(namespace, name string, data []byte) error { func (r RealPodControl) PatchPod(namespace, name string, data []byte) error {
...@@ -613,10 +624,11 @@ func (f *FakePodControl) CreatePodsWithControllerRef(namespace string, spec *v1. ...@@ -613,10 +624,11 @@ func (f *FakePodControl) CreatePodsWithControllerRef(namespace string, spec *v1.
return nil return nil
} }
func (f *FakePodControl) CreatePodsOnNode(nodeName, namespace string, template *v1.PodTemplateSpec, object runtime.Object) error { func (f *FakePodControl) CreatePodsOnNode(nodeName, namespace string, template *v1.PodTemplateSpec, object runtime.Object, controllerRef *metav1.OwnerReference) error {
f.Lock() f.Lock()
defer f.Unlock() defer f.Unlock()
f.Templates = append(f.Templates, *template) f.Templates = append(f.Templates, *template)
f.ControllerRefs = append(f.ControllerRefs, *controllerRef)
if f.Err != nil { if f.Err != nil {
return f.Err return f.Err
} }
......
...@@ -73,11 +73,13 @@ go_test( ...@@ -73,11 +73,13 @@ go_test(
"//vendor:k8s.io/apimachinery/pkg/apis/meta/v1", "//vendor:k8s.io/apimachinery/pkg/apis/meta/v1",
"//vendor:k8s.io/apimachinery/pkg/runtime", "//vendor:k8s.io/apimachinery/pkg/runtime",
"//vendor:k8s.io/apimachinery/pkg/util/intstr", "//vendor:k8s.io/apimachinery/pkg/util/intstr",
"//vendor:k8s.io/apimachinery/pkg/util/uuid",
"//vendor:k8s.io/apiserver/pkg/storage/names", "//vendor:k8s.io/apiserver/pkg/storage/names",
"//vendor:k8s.io/apiserver/pkg/util/feature", "//vendor:k8s.io/apiserver/pkg/util/feature",
"//vendor:k8s.io/client-go/testing", "//vendor:k8s.io/client-go/testing",
"//vendor:k8s.io/client-go/tools/cache", "//vendor:k8s.io/client-go/tools/cache",
"//vendor:k8s.io/client-go/tools/record", "//vendor:k8s.io/client-go/tools/record",
"//vendor:k8s.io/client-go/util/workqueue",
], ],
) )
......
...@@ -32,10 +32,13 @@ import ( ...@@ -32,10 +32,13 @@ import (
// rollingUpdate deletes old daemon set pods making sure that no more than // rollingUpdate deletes old daemon set pods making sure that no more than
// ds.Spec.UpdateStrategy.RollingUpdate.MaxUnavailable pods are unavailable // ds.Spec.UpdateStrategy.RollingUpdate.MaxUnavailable pods are unavailable
func (dsc *DaemonSetsController) rollingUpdate(ds *extensions.DaemonSet) error { func (dsc *DaemonSetsController) rollingUpdate(ds *extensions.DaemonSet) error {
newPods, oldPods, err := dsc.getAllDaemonSetPods(ds) nodeToDaemonPods, err := dsc.getNodesToDaemonPods(ds)
allPods := append(oldPods, newPods...) if err != nil {
return fmt.Errorf("couldn't get node to daemon pod mapping for daemon set %q: %v", ds.Name, err)
}
maxUnavailable, numUnavailable, err := dsc.getUnavailableNumbers(ds, allPods) _, oldPods, err := dsc.getAllDaemonSetPods(ds, nodeToDaemonPods)
maxUnavailable, numUnavailable, err := dsc.getUnavailableNumbers(ds, nodeToDaemonPods)
if err != nil { if err != nil {
return fmt.Errorf("Couldn't get unavailable numbers: %v", err) return fmt.Errorf("Couldn't get unavailable numbers: %v", err)
} }
...@@ -67,29 +70,23 @@ func (dsc *DaemonSetsController) rollingUpdate(ds *extensions.DaemonSet) error { ...@@ -67,29 +70,23 @@ func (dsc *DaemonSetsController) rollingUpdate(ds *extensions.DaemonSet) error {
return utilerrors.NewAggregate(errors) return utilerrors.NewAggregate(errors)
} }
func (dsc *DaemonSetsController) getAllDaemonSetPods(ds *extensions.DaemonSet) ([]*v1.Pod, []*v1.Pod, error) { func (dsc *DaemonSetsController) getAllDaemonSetPods(ds *extensions.DaemonSet, nodeToDaemonPods map[string][]*v1.Pod) ([]*v1.Pod, []*v1.Pod, error) {
var newPods []*v1.Pod var newPods []*v1.Pod
var oldPods []*v1.Pod var oldPods []*v1.Pod
selector, err := metav1.LabelSelectorAsSelector(ds.Spec.Selector) for _, pods := range nodeToDaemonPods {
if err != nil { for _, pod := range pods {
return newPods, oldPods, err if util.IsPodUpdated(ds.Spec.TemplateGeneration, pod) {
} newPods = append(newPods, pod)
daemonPods, err := dsc.podLister.Pods(ds.Namespace).List(selector) } else {
if err != nil { oldPods = append(oldPods, pod)
return newPods, oldPods, fmt.Errorf("Couldn't get list of pods for daemon set %#v: %v", ds, err) }
}
for _, pod := range daemonPods {
if util.IsPodUpdated(ds.Spec.TemplateGeneration, pod) {
newPods = append(newPods, pod)
} else {
oldPods = append(oldPods, pod)
} }
} }
return newPods, oldPods, nil return newPods, oldPods, nil
} }
func (dsc *DaemonSetsController) getUnavailableNumbers(ds *extensions.DaemonSet, allPods []*v1.Pod) (int, int, error) { func (dsc *DaemonSetsController) getUnavailableNumbers(ds *extensions.DaemonSet, nodeToDaemonPods map[string][]*v1.Pod) (int, int, error) {
glog.V(4).Infof("Getting unavailable numbers") glog.V(4).Infof("Getting unavailable numbers")
// TODO: get nodeList once in syncDaemonSet and pass it to other functions // TODO: get nodeList once in syncDaemonSet and pass it to other functions
nodeList, err := dsc.nodeLister.List(labels.Everything()) nodeList, err := dsc.nodeLister.List(labels.Everything())
...@@ -97,11 +94,6 @@ func (dsc *DaemonSetsController) getUnavailableNumbers(ds *extensions.DaemonSet, ...@@ -97,11 +94,6 @@ func (dsc *DaemonSetsController) getUnavailableNumbers(ds *extensions.DaemonSet,
return -1, -1, fmt.Errorf("couldn't get list of nodes during rolling update of daemon set %#v: %v", ds, err) return -1, -1, fmt.Errorf("couldn't get list of nodes during rolling update of daemon set %#v: %v", ds, err)
} }
nodeToDaemonPods, err := dsc.getNodesToDaemonPods(ds)
if err != nil {
return -1, -1, fmt.Errorf("couldn't get node to daemon pods mapping for daemon set %#v: %v", ds, err)
}
var numUnavailable, desiredNumberScheduled int var numUnavailable, desiredNumberScheduled int
for i := range nodeList { for i := range nodeList {
node := nodeList[i] node := nodeList[i]
......
...@@ -25,6 +25,7 @@ go_library( ...@@ -25,6 +25,7 @@ go_library(
"//vendor:k8s.io/apimachinery/pkg/util/validation/field", "//vendor:k8s.io/apimachinery/pkg/util/validation/field",
"//vendor:k8s.io/apiserver/pkg/endpoints/request", "//vendor:k8s.io/apiserver/pkg/endpoints/request",
"//vendor:k8s.io/apiserver/pkg/registry/generic", "//vendor:k8s.io/apiserver/pkg/registry/generic",
"//vendor:k8s.io/apiserver/pkg/registry/rest",
"//vendor:k8s.io/apiserver/pkg/storage", "//vendor:k8s.io/apiserver/pkg/storage",
"//vendor:k8s.io/apiserver/pkg/storage/names", "//vendor:k8s.io/apiserver/pkg/storage/names",
], ],
...@@ -40,6 +41,7 @@ go_test( ...@@ -40,6 +41,7 @@ go_test(
"//pkg/api/testapi:go_default_library", "//pkg/api/testapi:go_default_library",
"//pkg/api/testing:go_default_library", "//pkg/api/testing:go_default_library",
"//pkg/apis/extensions:go_default_library", "//pkg/apis/extensions:go_default_library",
"//vendor:k8s.io/apiserver/pkg/registry/rest",
], ],
) )
......
...@@ -26,6 +26,7 @@ import ( ...@@ -26,6 +26,7 @@ import (
"k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/apimachinery/pkg/util/validation/field"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request" genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/registry/generic" "k8s.io/apiserver/pkg/registry/generic"
"k8s.io/apiserver/pkg/registry/rest"
"k8s.io/apiserver/pkg/storage" "k8s.io/apiserver/pkg/storage"
"k8s.io/apiserver/pkg/storage/names" "k8s.io/apiserver/pkg/storage/names"
"k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api"
...@@ -42,6 +43,12 @@ type daemonSetStrategy struct { ...@@ -42,6 +43,12 @@ type daemonSetStrategy struct {
// Strategy is the default logic that applies when creating and updating DaemonSet objects. // Strategy is the default logic that applies when creating and updating DaemonSet objects.
var Strategy = daemonSetStrategy{api.Scheme, names.SimpleNameGenerator} var Strategy = daemonSetStrategy{api.Scheme, names.SimpleNameGenerator}
// DefaultGarbageCollectionPolicy returns Orphan because that was the default
// behavior before the server-side garbage collection was implemented.
func (daemonSetStrategy) DefaultGarbageCollectionPolicy() rest.GarbageCollectionPolicy {
return rest.OrphanDependents
}
// NamespaceScoped returns true because all DaemonSets need to be within a namespace. // NamespaceScoped returns true because all DaemonSets need to be within a namespace.
func (daemonSetStrategy) NamespaceScoped() bool { func (daemonSetStrategy) NamespaceScoped() bool {
return true return true
......
...@@ -19,6 +19,7 @@ package daemonset ...@@ -19,6 +19,7 @@ package daemonset
import ( import (
"testing" "testing"
"k8s.io/apiserver/pkg/registry/rest"
_ "k8s.io/kubernetes/pkg/api" _ "k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/testapi" "k8s.io/kubernetes/pkg/api/testapi"
apitesting "k8s.io/kubernetes/pkg/api/testing" apitesting "k8s.io/kubernetes/pkg/api/testing"
...@@ -33,3 +34,12 @@ func TestSelectableFieldLabelConversions(t *testing.T) { ...@@ -33,3 +34,12 @@ func TestSelectableFieldLabelConversions(t *testing.T) {
nil, nil,
) )
} }
func TestDefaultGarbageCollectionPolicy(t *testing.T) {
// Make sure we correctly implement the interface.
// Otherwise a typo could silently change the default.
var gcds rest.GarbageCollectionDeleteStrategy = Strategy
if got, want := gcds.DefaultGarbageCollectionPolicy(), rest.OrphanDependents; got != want {
t.Errorf("DefaultGarbageCollectionPolicy() = %#v, want %#v", got, want)
}
}
...@@ -35,8 +35,9 @@ type DaemonSetListerExpansion interface { ...@@ -35,8 +35,9 @@ type DaemonSetListerExpansion interface {
// DaemonSetNamespaeLister. // DaemonSetNamespaeLister.
type DaemonSetNamespaceListerExpansion interface{} type DaemonSetNamespaceListerExpansion interface{}
// GetPodDaemonSets returns a list of daemon sets managing a pod. // GetPodDaemonSets returns a list of DaemonSets that potentially match a pod.
// Returns an error if and only if no matching daemon sets are found. // Only the one specified in the Pod's ControllerRef will actually manage it.
// Returns an error only if no matching DaemonSets are found.
func (s *daemonSetLister) GetPodDaemonSets(pod *v1.Pod) ([]*v1beta1.DaemonSet, error) { func (s *daemonSetLister) GetPodDaemonSets(pod *v1.Pod) ([]*v1beta1.DaemonSet, error) {
var selector labels.Selector var selector labels.Selector
var daemonSet *v1beta1.DaemonSet var daemonSet *v1beta1.DaemonSet
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment