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
b2831a68
Commit
b2831a68
authored
Mar 04, 2019
by
Andrew Kim
Committed by
Andrew Sy Kim
May 08, 2019
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
move generic feature gate code from k8s.io/apiserver to k8s.io/component-base
parent
086a86b9
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
387 additions
and
34 deletions
+387
-34
.golint_failures
hack/.golint_failures
+1
-1
BUILD
staging/src/k8s.io/apiserver/pkg/util/feature/BUILD
+3
-23
feature_gate.go
...ing/src/k8s.io/apiserver/pkg/util/feature/feature_gate.go
+0
-0
BUILD
staging/src/k8s.io/component-base/featuregate/BUILD
+40
-0
feature_gate.go
...ing/src/k8s.io/component-base/featuregate/feature_gate.go
+333
-0
feature_gate_test.go
...rc/k8s.io/component-base/featuregate/feature_gate_test.go
+1
-1
BUILD
staging/src/k8s.io/component-base/featuregate/testing/BUILD
+4
-4
feature_gate.go
...k8s.io/component-base/featuregate/testing/feature_gate.go
+5
-5
No files found.
hack/.golint_failures
View file @
b2831a68
...
@@ -500,7 +500,6 @@ staging/src/k8s.io/apiserver/pkg/storage/storagebackend
...
@@ -500,7 +500,6 @@ staging/src/k8s.io/apiserver/pkg/storage/storagebackend
staging/src/k8s.io/apiserver/pkg/storage/testing
staging/src/k8s.io/apiserver/pkg/storage/testing
staging/src/k8s.io/apiserver/pkg/storage/tests
staging/src/k8s.io/apiserver/pkg/storage/tests
staging/src/k8s.io/apiserver/pkg/storage/value
staging/src/k8s.io/apiserver/pkg/storage/value
staging/src/k8s.io/apiserver/pkg/util/feature
staging/src/k8s.io/apiserver/pkg/util/proxy
staging/src/k8s.io/apiserver/pkg/util/proxy
staging/src/k8s.io/apiserver/pkg/util/webhook
staging/src/k8s.io/apiserver/pkg/util/webhook
staging/src/k8s.io/apiserver/pkg/util/wsstream
staging/src/k8s.io/apiserver/pkg/util/wsstream
...
@@ -574,6 +573,7 @@ staging/src/k8s.io/code-generator/cmd/go-to-protobuf/protobuf
...
@@ -574,6 +573,7 @@ staging/src/k8s.io/code-generator/cmd/go-to-protobuf/protobuf
staging/src/k8s.io/code-generator/cmd/lister-gen/generators
staging/src/k8s.io/code-generator/cmd/lister-gen/generators
staging/src/k8s.io/component-base/cli/flag
staging/src/k8s.io/component-base/cli/flag
staging/src/k8s.io/component-base/config/v1alpha1
staging/src/k8s.io/component-base/config/v1alpha1
staging/src/k8s.io/component-base/featuregate
staging/src/k8s.io/cri-api/pkg/apis/testing
staging/src/k8s.io/cri-api/pkg/apis/testing
staging/src/k8s.io/kube-aggregator/pkg/apis/apiregistration/v1
staging/src/k8s.io/kube-aggregator/pkg/apis/apiregistration/v1
staging/src/k8s.io/kube-aggregator/pkg/apis/apiregistration/v1beta1
staging/src/k8s.io/kube-aggregator/pkg/apis/apiregistration/v1beta1
...
...
staging/src/k8s.io/apiserver/pkg/util/feature/BUILD
View file @
b2831a68
package(default_visibility = ["//visibility:public"])
package(default_visibility = ["//visibility:public"])
load(
load("@io_bazel_rules_go//go:def.bzl", "go_library")
"@io_bazel_rules_go//go:def.bzl",
"go_library",
"go_test",
)
go_test(
name = "go_default_test",
srcs = ["feature_gate_test.go"],
embed = [":go_default_library"],
deps = [
"//vendor/github.com/spf13/pflag:go_default_library",
"//vendor/github.com/stretchr/testify/assert:go_default_library",
],
)
go_library(
go_library(
name = "go_default_library",
name = "go_default_library",
srcs = ["feature_gate.go"],
srcs = ["feature_gate.go"],
importmap = "k8s.io/kubernetes/vendor/k8s.io/apiserver/pkg/util/feature",
importmap = "k8s.io/kubernetes/vendor/k8s.io/apiserver/pkg/util/feature",
importpath = "k8s.io/apiserver/pkg/util/feature",
importpath = "k8s.io/apiserver/pkg/util/feature",
deps = [
deps = ["//staging/src/k8s.io/component-base/featuregate:go_default_library"],
"//vendor/github.com/spf13/pflag:go_default_library",
"//vendor/k8s.io/klog:go_default_library",
],
)
)
filegroup(
filegroup(
...
@@ -36,9 +19,6 @@ filegroup(
...
@@ -36,9 +19,6 @@ filegroup(
filegroup(
filegroup(
name = "all-srcs",
name = "all-srcs",
srcs = [
srcs = [":package-srcs"],
":package-srcs",
"//staging/src/k8s.io/apiserver/pkg/util/feature/testing:all-srcs",
],
tags = ["automanaged"],
tags = ["automanaged"],
)
)
staging/src/k8s.io/apiserver/pkg/util/feature/feature_gate.go
View file @
b2831a68
This diff is collapsed.
Click to expand it.
staging/src/k8s.io/component-base/featuregate/BUILD
0 → 100644
View file @
b2831a68
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "go_default_library",
srcs = ["feature_gate.go"],
importmap = "k8s.io/kubernetes/vendor/k8s.io/component-base/featuregate",
importpath = "k8s.io/component-base/featuregate",
visibility = ["//visibility:public"],
deps = [
"//vendor/github.com/spf13/pflag:go_default_library",
"//vendor/k8s.io/klog:go_default_library",
],
)
go_test(
name = "go_default_test",
srcs = ["feature_gate_test.go"],
embed = [":go_default_library"],
deps = [
"//vendor/github.com/spf13/pflag:go_default_library",
"//vendor/github.com/stretchr/testify/assert:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [
":package-srcs",
"//staging/src/k8s.io/component-base/featuregate/testing:all-srcs",
],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)
staging/src/k8s.io/component-base/featuregate/feature_gate.go
0 → 100644
View file @
b2831a68
/*
Copyright 2016 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
featuregate
import
(
"fmt"
"sort"
"strconv"
"strings"
"sync"
"sync/atomic"
"github.com/spf13/pflag"
"k8s.io/klog"
)
type
Feature
string
const
(
flagName
=
"feature-gates"
// allAlphaGate is a global toggle for alpha features. Per-feature key
// values override the default set by allAlphaGate. Examples:
// AllAlpha=false,NewFeature=true will result in newFeature=true
// AllAlpha=true,NewFeature=false will result in newFeature=false
allAlphaGate
Feature
=
"AllAlpha"
)
var
(
// The generic features.
defaultFeatures
=
map
[
Feature
]
FeatureSpec
{
allAlphaGate
:
{
Default
:
false
,
PreRelease
:
Alpha
},
}
// Special handling for a few gates.
specialFeatures
=
map
[
Feature
]
func
(
known
map
[
Feature
]
FeatureSpec
,
enabled
map
[
Feature
]
bool
,
val
bool
){
allAlphaGate
:
setUnsetAlphaGates
,
}
)
type
FeatureSpec
struct
{
// Default is the default enablement state for the feature
Default
bool
// LockToDefault indicates that the feature is locked to its default and cannot be changed
LockToDefault
bool
// PreRelease indicates the maturity level of the feature
PreRelease
prerelease
}
type
prerelease
string
const
(
// Values for PreRelease.
Alpha
=
prerelease
(
"ALPHA"
)
Beta
=
prerelease
(
"BETA"
)
GA
=
prerelease
(
""
)
// Deprecated
Deprecated
=
prerelease
(
"DEPRECATED"
)
)
// FeatureGate indicates whether a given feature is enabled or not
type
FeatureGate
interface
{
// Enabled returns true if the key is enabled.
Enabled
(
key
Feature
)
bool
// KnownFeatures returns a slice of strings describing the FeatureGate's known features.
KnownFeatures
()
[]
string
// DeepCopy returns a deep copy of the FeatureGate object, such that gates can be
// set on the copy without mutating the original. This is useful for validating
// config against potential feature gate changes before committing those changes.
DeepCopy
()
MutableFeatureGate
}
// MutableFeatureGate parses and stores flag gates for known features from
// a string like feature1=true,feature2=false,...
type
MutableFeatureGate
interface
{
FeatureGate
// AddFlag adds a flag for setting global feature gates to the specified FlagSet.
AddFlag
(
fs
*
pflag
.
FlagSet
)
// Set parses and stores flag gates for known features
// from a string like feature1=true,feature2=false,...
Set
(
value
string
)
error
// SetFromMap stores flag gates for known features from a map[string]bool or returns an error
SetFromMap
(
m
map
[
string
]
bool
)
error
// Add adds features to the featureGate.
Add
(
features
map
[
Feature
]
FeatureSpec
)
error
}
// featureGate implements FeatureGate as well as pflag.Value for flag parsing.
type
featureGate
struct
{
special
map
[
Feature
]
func
(
map
[
Feature
]
FeatureSpec
,
map
[
Feature
]
bool
,
bool
)
// lock guards writes to known, enabled, and reads/writes of closed
lock
sync
.
Mutex
// known holds a map[Feature]FeatureSpec
known
*
atomic
.
Value
// enabled holds a map[Feature]bool
enabled
*
atomic
.
Value
// closed is set to true when AddFlag is called, and prevents subsequent calls to Add
closed
bool
}
func
setUnsetAlphaGates
(
known
map
[
Feature
]
FeatureSpec
,
enabled
map
[
Feature
]
bool
,
val
bool
)
{
for
k
,
v
:=
range
known
{
if
v
.
PreRelease
==
Alpha
{
if
_
,
found
:=
enabled
[
k
];
!
found
{
enabled
[
k
]
=
val
}
}
}
}
// Set, String, and Type implement pflag.Value
var
_
pflag
.
Value
=
&
featureGate
{}
func
NewFeatureGate
()
*
featureGate
{
known
:=
map
[
Feature
]
FeatureSpec
{}
for
k
,
v
:=
range
defaultFeatures
{
known
[
k
]
=
v
}
knownValue
:=
&
atomic
.
Value
{}
knownValue
.
Store
(
known
)
enabled
:=
map
[
Feature
]
bool
{}
enabledValue
:=
&
atomic
.
Value
{}
enabledValue
.
Store
(
enabled
)
f
:=
&
featureGate
{
known
:
knownValue
,
special
:
specialFeatures
,
enabled
:
enabledValue
,
}
return
f
}
// Set parses a string of the form "key1=value1,key2=value2,..." into a
// map[string]bool of known keys or returns an error.
func
(
f
*
featureGate
)
Set
(
value
string
)
error
{
m
:=
make
(
map
[
string
]
bool
)
for
_
,
s
:=
range
strings
.
Split
(
value
,
","
)
{
if
len
(
s
)
==
0
{
continue
}
arr
:=
strings
.
SplitN
(
s
,
"="
,
2
)
k
:=
strings
.
TrimSpace
(
arr
[
0
])
if
len
(
arr
)
!=
2
{
return
fmt
.
Errorf
(
"missing bool value for %s"
,
k
)
}
v
:=
strings
.
TrimSpace
(
arr
[
1
])
boolValue
,
err
:=
strconv
.
ParseBool
(
v
)
if
err
!=
nil
{
return
fmt
.
Errorf
(
"invalid value of %s=%s, err: %v"
,
k
,
v
,
err
)
}
m
[
k
]
=
boolValue
}
return
f
.
SetFromMap
(
m
)
}
// SetFromMap stores flag gates for known features from a map[string]bool or returns an error
func
(
f
*
featureGate
)
SetFromMap
(
m
map
[
string
]
bool
)
error
{
f
.
lock
.
Lock
()
defer
f
.
lock
.
Unlock
()
// Copy existing state
known
:=
map
[
Feature
]
FeatureSpec
{}
for
k
,
v
:=
range
f
.
known
.
Load
()
.
(
map
[
Feature
]
FeatureSpec
)
{
known
[
k
]
=
v
}
enabled
:=
map
[
Feature
]
bool
{}
for
k
,
v
:=
range
f
.
enabled
.
Load
()
.
(
map
[
Feature
]
bool
)
{
enabled
[
k
]
=
v
}
for
k
,
v
:=
range
m
{
k
:=
Feature
(
k
)
featureSpec
,
ok
:=
known
[
k
]
if
!
ok
{
return
fmt
.
Errorf
(
"unrecognized feature gate: %s"
,
k
)
}
if
featureSpec
.
LockToDefault
&&
featureSpec
.
Default
!=
v
{
return
fmt
.
Errorf
(
"cannot set feature gate %v to %v, feature is locked to %v"
,
k
,
v
,
featureSpec
.
Default
)
}
enabled
[
k
]
=
v
// Handle "special" features like "all alpha gates"
if
fn
,
found
:=
f
.
special
[
k
];
found
{
fn
(
known
,
enabled
,
v
)
}
if
featureSpec
.
PreRelease
==
Deprecated
{
klog
.
Warningf
(
"Setting deprecated feature gate %s=%t. It will be removed in a future release."
,
k
,
v
)
}
else
if
featureSpec
.
PreRelease
==
GA
{
klog
.
Warningf
(
"Setting GA feature gate %s=%t. It will be removed in a future release."
,
k
,
v
)
}
}
// Persist changes
f
.
known
.
Store
(
known
)
f
.
enabled
.
Store
(
enabled
)
klog
.
V
(
1
)
.
Infof
(
"feature gates: %v"
,
f
.
enabled
)
return
nil
}
// String returns a string containing all enabled feature gates, formatted as "key1=value1,key2=value2,...".
func
(
f
*
featureGate
)
String
()
string
{
pairs
:=
[]
string
{}
for
k
,
v
:=
range
f
.
enabled
.
Load
()
.
(
map
[
Feature
]
bool
)
{
pairs
=
append
(
pairs
,
fmt
.
Sprintf
(
"%s=%t"
,
k
,
v
))
}
sort
.
Strings
(
pairs
)
return
strings
.
Join
(
pairs
,
","
)
}
func
(
f
*
featureGate
)
Type
()
string
{
return
"mapStringBool"
}
// Add adds features to the featureGate.
func
(
f
*
featureGate
)
Add
(
features
map
[
Feature
]
FeatureSpec
)
error
{
f
.
lock
.
Lock
()
defer
f
.
lock
.
Unlock
()
if
f
.
closed
{
return
fmt
.
Errorf
(
"cannot add a feature gate after adding it to the flag set"
)
}
// Copy existing state
known
:=
map
[
Feature
]
FeatureSpec
{}
for
k
,
v
:=
range
f
.
known
.
Load
()
.
(
map
[
Feature
]
FeatureSpec
)
{
known
[
k
]
=
v
}
for
name
,
spec
:=
range
features
{
if
existingSpec
,
found
:=
known
[
name
];
found
{
if
existingSpec
==
spec
{
continue
}
return
fmt
.
Errorf
(
"feature gate %q with different spec already exists: %v"
,
name
,
existingSpec
)
}
known
[
name
]
=
spec
}
// Persist updated state
f
.
known
.
Store
(
known
)
return
nil
}
// Enabled returns true if the key is enabled.
func
(
f
*
featureGate
)
Enabled
(
key
Feature
)
bool
{
if
v
,
ok
:=
f
.
enabled
.
Load
()
.
(
map
[
Feature
]
bool
)[
key
];
ok
{
return
v
}
return
f
.
known
.
Load
()
.
(
map
[
Feature
]
FeatureSpec
)[
key
]
.
Default
}
// AddFlag adds a flag for setting global feature gates to the specified FlagSet.
func
(
f
*
featureGate
)
AddFlag
(
fs
*
pflag
.
FlagSet
)
{
f
.
lock
.
Lock
()
// TODO(mtaufen): Shouldn't we just close it on the first Set/SetFromMap instead?
// Not all components expose a feature gates flag using this AddFlag method, and
// in the future, all components will completely stop exposing a feature gates flag,
// in favor of componentconfig.
f
.
closed
=
true
f
.
lock
.
Unlock
()
known
:=
f
.
KnownFeatures
()
fs
.
Var
(
f
,
flagName
,
""
+
"A set of key=value pairs that describe feature gates for alpha/experimental features. "
+
"Options are:
\n
"
+
strings
.
Join
(
known
,
"
\n
"
))
}
// KnownFeatures returns a slice of strings describing the FeatureGate's known features.
// Deprecated and GA features are hidden from the list.
func
(
f
*
featureGate
)
KnownFeatures
()
[]
string
{
var
known
[]
string
for
k
,
v
:=
range
f
.
known
.
Load
()
.
(
map
[
Feature
]
FeatureSpec
)
{
if
v
.
PreRelease
==
GA
||
v
.
PreRelease
==
Deprecated
{
continue
}
known
=
append
(
known
,
fmt
.
Sprintf
(
"%s=true|false (%s - default=%t)"
,
k
,
v
.
PreRelease
,
v
.
Default
))
}
sort
.
Strings
(
known
)
return
known
}
// DeepCopy returns a deep copy of the FeatureGate object, such that gates can be
// set on the copy without mutating the original. This is useful for validating
// config against potential feature gate changes before committing those changes.
func
(
f
*
featureGate
)
DeepCopy
()
MutableFeatureGate
{
// Copy existing state.
known
:=
map
[
Feature
]
FeatureSpec
{}
for
k
,
v
:=
range
f
.
known
.
Load
()
.
(
map
[
Feature
]
FeatureSpec
)
{
known
[
k
]
=
v
}
enabled
:=
map
[
Feature
]
bool
{}
for
k
,
v
:=
range
f
.
enabled
.
Load
()
.
(
map
[
Feature
]
bool
)
{
enabled
[
k
]
=
v
}
// Store copied state in new atomics.
knownValue
:=
&
atomic
.
Value
{}
knownValue
.
Store
(
known
)
enabledValue
:=
&
atomic
.
Value
{}
enabledValue
.
Store
(
enabled
)
// Construct a new featureGate around the copied state.
// Note that specialFeatures is treated as immutable by convention,
// and we maintain the value of f.closed across the copy.
return
&
featureGate
{
special
:
specialFeatures
,
known
:
knownValue
,
enabled
:
enabledValue
,
closed
:
f
.
closed
,
}
}
staging/src/k8s.io/
apiserver/pkg/util/featur
e/feature_gate_test.go
→
staging/src/k8s.io/
component-base/featuregat
e/feature_gate_test.go
View file @
b2831a68
...
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
...
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
limitations under the License.
*/
*/
package
feature
package
feature
gate
import
(
import
(
"fmt"
"fmt"
...
...
staging/src/k8s.io/
apiserver/pkg/util/featur
e/testing/BUILD
→
staging/src/k8s.io/
component-base/featuregat
e/testing/BUILD
View file @
b2831a68
...
@@ -2,11 +2,11 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library")
...
@@ -2,11 +2,11 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library")
go_library(
go_library(
name = "go_default_library",
name = "go_default_library",
srcs = ["feature_gate
_testing
.go"],
srcs = ["feature_gate.go"],
importmap = "k8s.io/kubernetes/vendor/k8s.io/
apiserver/pkg/util/featur
e/testing",
importmap = "k8s.io/kubernetes/vendor/k8s.io/
component-base/featuregat
e/testing",
importpath = "k8s.io/
apiserver/pkg/util/featur
e/testing",
importpath = "k8s.io/
component-base/featuregat
e/testing",
visibility = ["//visibility:public"],
visibility = ["//visibility:public"],
deps = ["//staging/src/k8s.io/
apiserver/pkg/util/featur
e:go_default_library"],
deps = ["//staging/src/k8s.io/
component-base/featuregat
e:go_default_library"],
)
)
filegroup(
filegroup(
...
...
staging/src/k8s.io/
apiserver/pkg/util/feature/testing/feature_gate_testing
.go
→
staging/src/k8s.io/
component-base/featuregate/testing/feature_gate
.go
View file @
b2831a68
...
@@ -20,7 +20,7 @@ import (
...
@@ -20,7 +20,7 @@ import (
"fmt"
"fmt"
"testing"
"testing"
"k8s.io/
apiserver/pkg/util/featur
e"
"k8s.io/
component-base/featuregat
e"
)
)
// SetFeatureGateDuringTest sets the specified gate to the specified value, and returns a function that restores the original value.
// SetFeatureGateDuringTest sets the specified gate to the specified value, and returns a function that restores the original value.
...
@@ -28,16 +28,16 @@ import (
...
@@ -28,16 +28,16 @@ import (
//
//
// Example use:
// Example use:
//
//
// defer
utilfeatur
etesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.<FeatureName>, true)()
// defer
featuregat
etesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.<FeatureName>, true)()
func
SetFeatureGateDuringTest
(
tb
testing
.
TB
,
gate
feature
.
FeatureGate
,
f
featur
e
.
Feature
,
value
bool
)
func
()
{
func
SetFeatureGateDuringTest
(
tb
testing
.
TB
,
gate
feature
gate
.
FeatureGate
,
f
featuregat
e
.
Feature
,
value
bool
)
func
()
{
originalValue
:=
gate
.
Enabled
(
f
)
originalValue
:=
gate
.
Enabled
(
f
)
if
err
:=
gate
.
(
feature
.
MutableFeatureGate
)
.
Set
(
fmt
.
Sprintf
(
"%s=%v"
,
f
,
value
));
err
!=
nil
{
if
err
:=
gate
.
(
feature
gate
.
MutableFeatureGate
)
.
Set
(
fmt
.
Sprintf
(
"%s=%v"
,
f
,
value
));
err
!=
nil
{
tb
.
Errorf
(
"error setting %s=%v: %v"
,
f
,
value
,
err
)
tb
.
Errorf
(
"error setting %s=%v: %v"
,
f
,
value
,
err
)
}
}
return
func
()
{
return
func
()
{
if
err
:=
gate
.
(
feature
.
MutableFeatureGate
)
.
Set
(
fmt
.
Sprintf
(
"%s=%v"
,
f
,
originalValue
));
err
!=
nil
{
if
err
:=
gate
.
(
feature
gate
.
MutableFeatureGate
)
.
Set
(
fmt
.
Sprintf
(
"%s=%v"
,
f
,
originalValue
));
err
!=
nil
{
tb
.
Errorf
(
"error restoring %s=%v: %v"
,
f
,
originalValue
,
err
)
tb
.
Errorf
(
"error restoring %s=%v: %v"
,
f
,
originalValue
,
err
)
}
}
}
}
...
...
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