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
d3445d71
Commit
d3445d71
authored
Jul 26, 2018
by
Mikalai Radchuk
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Return an error if there is no resources matching
This makes `kubectl wait` print useful message when there is no resources matching a query. Also it will now exit with the exit status 1.
parent
259e0743
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
121 additions
and
58 deletions
+121
-58
wait.go
pkg/kubectl/cmd/wait/wait.go
+17
-4
wait_test.go
pkg/kubectl/cmd/wait/wait_test.go
+104
-54
No files found.
pkg/kubectl/cmd/wait/wait.go
View file @
d3445d71
...
@@ -17,13 +17,14 @@ limitations under the License.
...
@@ -17,13 +17,14 @@ limitations under the License.
package
wait
package
wait
import
(
import
(
"errors"
"fmt"
"fmt"
"strings"
"strings"
"time"
"time"
"github.com/spf13/cobra"
"github.com/spf13/cobra"
"k8s.io/apimachinery/pkg/api/errors"
apierrors
"k8s.io/apimachinery/pkg/api/errors"
metav1
"k8s.io/apimachinery/pkg/apis/meta/v1"
metav1
"k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime"
...
@@ -61,6 +62,9 @@ var (
...
@@ -61,6 +62,9 @@ var (
kubectl wait --for=delete pod/busybox1 --timeout=60s`
)
kubectl wait --for=delete pod/busybox1 --timeout=60s`
)
)
)
// errNoMatchingResources is returned when there is no resources matching a query.
var
errNoMatchingResources
=
errors
.
New
(
"no matching resources found"
)
// WaitFlags directly reflect the information that CLI is gathering via flags. They will be converted to Options, which
// WaitFlags directly reflect the information that CLI is gathering via flags. They will be converted to Options, which
// reflect the runtime requirements for the command. This structure reduces the transformation to wiring and makes
// reflect the runtime requirements for the command. This structure reduces the transformation to wiring and makes
// the logic itself easy to unit test
// the logic itself easy to unit test
...
@@ -206,11 +210,13 @@ type ConditionFunc func(info *resource.Info, o *WaitOptions) (finalObject runtim
...
@@ -206,11 +210,13 @@ type ConditionFunc func(info *resource.Info, o *WaitOptions) (finalObject runtim
// RunWait runs the waiting logic
// RunWait runs the waiting logic
func
(
o
*
WaitOptions
)
RunWait
()
error
{
func
(
o
*
WaitOptions
)
RunWait
()
error
{
return
o
.
ResourceFinder
.
Do
()
.
Visit
(
func
(
info
*
resource
.
Info
,
err
error
)
error
{
visitCount
:=
0
err
:=
o
.
ResourceFinder
.
Do
()
.
Visit
(
func
(
info
*
resource
.
Info
,
err
error
)
error
{
if
err
!=
nil
{
if
err
!=
nil
{
return
err
return
err
}
}
visitCount
++
finalObject
,
success
,
err
:=
o
.
ConditionFn
(
info
,
o
)
finalObject
,
success
,
err
:=
o
.
ConditionFn
(
info
,
o
)
if
success
{
if
success
{
o
.
Printer
.
PrintObj
(
finalObject
,
o
.
Out
)
o
.
Printer
.
PrintObj
(
finalObject
,
o
.
Out
)
...
@@ -221,6 +227,13 @@ func (o *WaitOptions) RunWait() error {
...
@@ -221,6 +227,13 @@ func (o *WaitOptions) RunWait() error {
}
}
return
err
return
err
})
})
if
err
!=
nil
{
return
err
}
if
visitCount
==
0
{
return
errNoMatchingResources
}
return
err
}
}
// IsDeleted is a condition func for waiting for something to be deleted
// IsDeleted is a condition func for waiting for something to be deleted
...
@@ -228,7 +241,7 @@ func IsDeleted(info *resource.Info, o *WaitOptions) (runtime.Object, bool, error
...
@@ -228,7 +241,7 @@ func IsDeleted(info *resource.Info, o *WaitOptions) (runtime.Object, bool, error
endTime
:=
time
.
Now
()
.
Add
(
o
.
Timeout
)
endTime
:=
time
.
Now
()
.
Add
(
o
.
Timeout
)
for
{
for
{
gottenObj
,
err
:=
o
.
DynamicClient
.
Resource
(
info
.
Mapping
.
Resource
)
.
Namespace
(
info
.
Namespace
)
.
Get
(
info
.
Name
,
metav1
.
GetOptions
{})
gottenObj
,
err
:=
o
.
DynamicClient
.
Resource
(
info
.
Mapping
.
Resource
)
.
Namespace
(
info
.
Namespace
)
.
Get
(
info
.
Name
,
metav1
.
GetOptions
{})
if
errors
.
IsNotFound
(
err
)
{
if
api
errors
.
IsNotFound
(
err
)
{
return
info
.
Object
,
true
,
nil
return
info
.
Object
,
true
,
nil
}
}
if
err
!=
nil
{
if
err
!=
nil
{
...
@@ -293,7 +306,7 @@ func (w ConditionalWait) IsConditionMet(info *resource.Info, o *WaitOptions) (ru
...
@@ -293,7 +306,7 @@ func (w ConditionalWait) IsConditionMet(info *resource.Info, o *WaitOptions) (ru
resourceVersion
:=
""
resourceVersion
:=
""
gottenObj
,
err
:=
o
.
DynamicClient
.
Resource
(
info
.
Mapping
.
Resource
)
.
Namespace
(
info
.
Namespace
)
.
Get
(
info
.
Name
,
metav1
.
GetOptions
{})
gottenObj
,
err
:=
o
.
DynamicClient
.
Resource
(
info
.
Mapping
.
Resource
)
.
Namespace
(
info
.
Namespace
)
.
Get
(
info
.
Name
,
metav1
.
GetOptions
{})
switch
{
switch
{
case
errors
.
IsNotFound
(
err
)
:
case
api
errors
.
IsNotFound
(
err
)
:
resourceVersion
=
"0"
resourceVersion
=
"0"
case
err
!=
nil
:
case
err
!=
nil
:
return
info
.
Object
,
false
,
err
return
info
.
Object
,
false
,
err
...
...
pkg/kubectl/cmd/wait/wait_test.go
View file @
d3445d71
...
@@ -68,7 +68,7 @@ func TestWaitForDeletion(t *testing.T) {
...
@@ -68,7 +68,7 @@ func TestWaitForDeletion(t *testing.T) {
tests
:=
[]
struct
{
tests
:=
[]
struct
{
name
string
name
string
info
*
resource
.
Info
info
s
[]
*
resource
.
Info
fakeClient
func
()
*
dynamicfakeclient
.
FakeDynamicClient
fakeClient
func
()
*
dynamicfakeclient
.
FakeDynamicClient
timeout
time
.
Duration
timeout
time
.
Duration
uidMap
UIDMap
uidMap
UIDMap
...
@@ -78,12 +78,14 @@ func TestWaitForDeletion(t *testing.T) {
...
@@ -78,12 +78,14 @@ func TestWaitForDeletion(t *testing.T) {
}{
}{
{
{
name
:
"missing on get"
,
name
:
"missing on get"
,
info
:
&
resource
.
Info
{
infos
:
[]
*
resource
.
Info
{
Mapping
:
&
meta
.
RESTMapping
{
{
Resource
:
schema
.
GroupVersionResource
{
Group
:
"group"
,
Version
:
"version"
,
Resource
:
"theresource"
},
Mapping
:
&
meta
.
RESTMapping
{
Resource
:
schema
.
GroupVersionResource
{
Group
:
"group"
,
Version
:
"version"
,
Resource
:
"theresource"
},
},
Name
:
"name-foo"
,
Namespace
:
"ns-foo"
,
},
},
Name
:
"name-foo"
,
Namespace
:
"ns-foo"
,
},
},
fakeClient
:
func
()
*
dynamicfakeclient
.
FakeDynamicClient
{
fakeClient
:
func
()
*
dynamicfakeclient
.
FakeDynamicClient
{
return
dynamicfakeclient
.
NewSimpleDynamicClient
(
scheme
)
return
dynamicfakeclient
.
NewSimpleDynamicClient
(
scheme
)
...
@@ -100,13 +102,30 @@ func TestWaitForDeletion(t *testing.T) {
...
@@ -100,13 +102,30 @@ func TestWaitForDeletion(t *testing.T) {
},
},
},
},
{
{
name
:
"handles no infos"
,
infos
:
[]
*
resource
.
Info
{},
fakeClient
:
func
()
*
dynamicfakeclient
.
FakeDynamicClient
{
return
dynamicfakeclient
.
NewSimpleDynamicClient
(
scheme
)
},
timeout
:
10
*
time
.
Second
,
expectedErr
:
errNoMatchingResources
.
Error
(),
validateActions
:
func
(
t
*
testing
.
T
,
actions
[]
clienttesting
.
Action
)
{
if
len
(
actions
)
!=
0
{
t
.
Fatal
(
spew
.
Sdump
(
actions
))
}
},
},
{
name
:
"uid conflict on get"
,
name
:
"uid conflict on get"
,
info
:
&
resource
.
Info
{
infos
:
[]
*
resource
.
Info
{
Mapping
:
&
meta
.
RESTMapping
{
{
Resource
:
schema
.
GroupVersionResource
{
Group
:
"group"
,
Version
:
"version"
,
Resource
:
"theresource"
},
Mapping
:
&
meta
.
RESTMapping
{
Resource
:
schema
.
GroupVersionResource
{
Group
:
"group"
,
Version
:
"version"
,
Resource
:
"theresource"
},
},
Name
:
"name-foo"
,
Namespace
:
"ns-foo"
,
},
},
Name
:
"name-foo"
,
Namespace
:
"ns-foo"
,
},
},
fakeClient
:
func
()
*
dynamicfakeclient
.
FakeDynamicClient
{
fakeClient
:
func
()
*
dynamicfakeclient
.
FakeDynamicClient
{
fakeClient
:=
dynamicfakeclient
.
NewSimpleDynamicClient
(
scheme
)
fakeClient
:=
dynamicfakeclient
.
NewSimpleDynamicClient
(
scheme
)
...
@@ -146,12 +165,14 @@ func TestWaitForDeletion(t *testing.T) {
...
@@ -146,12 +165,14 @@ func TestWaitForDeletion(t *testing.T) {
},
},
{
{
name
:
"times out"
,
name
:
"times out"
,
info
:
&
resource
.
Info
{
infos
:
[]
*
resource
.
Info
{
Mapping
:
&
meta
.
RESTMapping
{
{
Resource
:
schema
.
GroupVersionResource
{
Group
:
"group"
,
Version
:
"version"
,
Resource
:
"theresource"
},
Mapping
:
&
meta
.
RESTMapping
{
Resource
:
schema
.
GroupVersionResource
{
Group
:
"group"
,
Version
:
"version"
,
Resource
:
"theresource"
},
},
Name
:
"name-foo"
,
Namespace
:
"ns-foo"
,
},
},
Name
:
"name-foo"
,
Namespace
:
"ns-foo"
,
},
},
fakeClient
:
func
()
*
dynamicfakeclient
.
FakeDynamicClient
{
fakeClient
:
func
()
*
dynamicfakeclient
.
FakeDynamicClient
{
fakeClient
:=
dynamicfakeclient
.
NewSimpleDynamicClient
(
scheme
)
fakeClient
:=
dynamicfakeclient
.
NewSimpleDynamicClient
(
scheme
)
...
@@ -177,12 +198,14 @@ func TestWaitForDeletion(t *testing.T) {
...
@@ -177,12 +198,14 @@ func TestWaitForDeletion(t *testing.T) {
},
},
{
{
name
:
"handles watch close out"
,
name
:
"handles watch close out"
,
info
:
&
resource
.
Info
{
infos
:
[]
*
resource
.
Info
{
Mapping
:
&
meta
.
RESTMapping
{
{
Resource
:
schema
.
GroupVersionResource
{
Group
:
"group"
,
Version
:
"version"
,
Resource
:
"theresource"
},
Mapping
:
&
meta
.
RESTMapping
{
Resource
:
schema
.
GroupVersionResource
{
Group
:
"group"
,
Version
:
"version"
,
Resource
:
"theresource"
},
},
Name
:
"name-foo"
,
Namespace
:
"ns-foo"
,
},
},
Name
:
"name-foo"
,
Namespace
:
"ns-foo"
,
},
},
fakeClient
:
func
()
*
dynamicfakeclient
.
FakeDynamicClient
{
fakeClient
:
func
()
*
dynamicfakeclient
.
FakeDynamicClient
{
fakeClient
:=
dynamicfakeclient
.
NewSimpleDynamicClient
(
scheme
)
fakeClient
:=
dynamicfakeclient
.
NewSimpleDynamicClient
(
scheme
)
...
@@ -228,12 +251,14 @@ func TestWaitForDeletion(t *testing.T) {
...
@@ -228,12 +251,14 @@ func TestWaitForDeletion(t *testing.T) {
},
},
{
{
name
:
"handles watch delete"
,
name
:
"handles watch delete"
,
info
:
&
resource
.
Info
{
infos
:
[]
*
resource
.
Info
{
Mapping
:
&
meta
.
RESTMapping
{
{
Resource
:
schema
.
GroupVersionResource
{
Group
:
"group"
,
Version
:
"version"
,
Resource
:
"theresource"
},
Mapping
:
&
meta
.
RESTMapping
{
Resource
:
schema
.
GroupVersionResource
{
Group
:
"group"
,
Version
:
"version"
,
Resource
:
"theresource"
},
},
Name
:
"name-foo"
,
Namespace
:
"ns-foo"
,
},
},
Name
:
"name-foo"
,
Namespace
:
"ns-foo"
,
},
},
fakeClient
:
func
()
*
dynamicfakeclient
.
FakeDynamicClient
{
fakeClient
:
func
()
*
dynamicfakeclient
.
FakeDynamicClient
{
fakeClient
:=
dynamicfakeclient
.
NewSimpleDynamicClient
(
scheme
)
fakeClient
:=
dynamicfakeclient
.
NewSimpleDynamicClient
(
scheme
)
...
@@ -267,7 +292,7 @@ func TestWaitForDeletion(t *testing.T) {
...
@@ -267,7 +292,7 @@ func TestWaitForDeletion(t *testing.T) {
t
.
Run
(
test
.
name
,
func
(
t
*
testing
.
T
)
{
t
.
Run
(
test
.
name
,
func
(
t
*
testing
.
T
)
{
fakeClient
:=
test
.
fakeClient
()
fakeClient
:=
test
.
fakeClient
()
o
:=
&
WaitOptions
{
o
:=
&
WaitOptions
{
ResourceFinder
:
genericclioptions
.
NewSimpleFakeResourceFinder
(
test
.
info
),
ResourceFinder
:
genericclioptions
.
NewSimpleFakeResourceFinder
(
test
.
info
s
...
),
UIDMap
:
test
.
uidMap
,
UIDMap
:
test
.
uidMap
,
DynamicClient
:
fakeClient
,
DynamicClient
:
fakeClient
,
Timeout
:
test
.
timeout
,
Timeout
:
test
.
timeout
,
...
@@ -299,7 +324,7 @@ func TestWaitForCondition(t *testing.T) {
...
@@ -299,7 +324,7 @@ func TestWaitForCondition(t *testing.T) {
tests
:=
[]
struct
{
tests
:=
[]
struct
{
name
string
name
string
info
*
resource
.
Info
info
s
[]
*
resource
.
Info
fakeClient
func
()
*
dynamicfakeclient
.
FakeDynamicClient
fakeClient
func
()
*
dynamicfakeclient
.
FakeDynamicClient
timeout
time
.
Duration
timeout
time
.
Duration
...
@@ -308,12 +333,14 @@ func TestWaitForCondition(t *testing.T) {
...
@@ -308,12 +333,14 @@ func TestWaitForCondition(t *testing.T) {
}{
}{
{
{
name
:
"present on get"
,
name
:
"present on get"
,
info
:
&
resource
.
Info
{
infos
:
[]
*
resource
.
Info
{
Mapping
:
&
meta
.
RESTMapping
{
{
Resource
:
schema
.
GroupVersionResource
{
Group
:
"group"
,
Version
:
"version"
,
Resource
:
"theresource"
},
Mapping
:
&
meta
.
RESTMapping
{
Resource
:
schema
.
GroupVersionResource
{
Group
:
"group"
,
Version
:
"version"
,
Resource
:
"theresource"
},
},
Name
:
"name-foo"
,
Namespace
:
"ns-foo"
,
},
},
Name
:
"name-foo"
,
Namespace
:
"ns-foo"
,
},
},
fakeClient
:
func
()
*
dynamicfakeclient
.
FakeDynamicClient
{
fakeClient
:
func
()
*
dynamicfakeclient
.
FakeDynamicClient
{
fakeClient
:=
dynamicfakeclient
.
NewSimpleDynamicClient
(
scheme
)
fakeClient
:=
dynamicfakeclient
.
NewSimpleDynamicClient
(
scheme
)
...
@@ -337,13 +364,30 @@ func TestWaitForCondition(t *testing.T) {
...
@@ -337,13 +364,30 @@ func TestWaitForCondition(t *testing.T) {
},
},
},
},
{
{
name
:
"handles no infos"
,
infos
:
[]
*
resource
.
Info
{},
fakeClient
:
func
()
*
dynamicfakeclient
.
FakeDynamicClient
{
return
dynamicfakeclient
.
NewSimpleDynamicClient
(
scheme
)
},
timeout
:
10
*
time
.
Second
,
expectedErr
:
errNoMatchingResources
.
Error
(),
validateActions
:
func
(
t
*
testing
.
T
,
actions
[]
clienttesting
.
Action
)
{
if
len
(
actions
)
!=
0
{
t
.
Fatal
(
spew
.
Sdump
(
actions
))
}
},
},
{
name
:
"times out"
,
name
:
"times out"
,
info
:
&
resource
.
Info
{
infos
:
[]
*
resource
.
Info
{
Mapping
:
&
meta
.
RESTMapping
{
{
Resource
:
schema
.
GroupVersionResource
{
Group
:
"group"
,
Version
:
"version"
,
Resource
:
"theresource"
},
Mapping
:
&
meta
.
RESTMapping
{
Resource
:
schema
.
GroupVersionResource
{
Group
:
"group"
,
Version
:
"version"
,
Resource
:
"theresource"
},
},
Name
:
"name-foo"
,
Namespace
:
"ns-foo"
,
},
},
Name
:
"name-foo"
,
Namespace
:
"ns-foo"
,
},
},
fakeClient
:
func
()
*
dynamicfakeclient
.
FakeDynamicClient
{
fakeClient
:
func
()
*
dynamicfakeclient
.
FakeDynamicClient
{
fakeClient
:=
dynamicfakeclient
.
NewSimpleDynamicClient
(
scheme
)
fakeClient
:=
dynamicfakeclient
.
NewSimpleDynamicClient
(
scheme
)
...
@@ -372,12 +416,14 @@ func TestWaitForCondition(t *testing.T) {
...
@@ -372,12 +416,14 @@ func TestWaitForCondition(t *testing.T) {
},
},
{
{
name
:
"handles watch close out"
,
name
:
"handles watch close out"
,
info
:
&
resource
.
Info
{
infos
:
[]
*
resource
.
Info
{
Mapping
:
&
meta
.
RESTMapping
{
{
Resource
:
schema
.
GroupVersionResource
{
Group
:
"group"
,
Version
:
"version"
,
Resource
:
"theresource"
},
Mapping
:
&
meta
.
RESTMapping
{
Resource
:
schema
.
GroupVersionResource
{
Group
:
"group"
,
Version
:
"version"
,
Resource
:
"theresource"
},
},
Name
:
"name-foo"
,
Namespace
:
"ns-foo"
,
},
},
Name
:
"name-foo"
,
Namespace
:
"ns-foo"
,
},
},
fakeClient
:
func
()
*
dynamicfakeclient
.
FakeDynamicClient
{
fakeClient
:
func
()
*
dynamicfakeclient
.
FakeDynamicClient
{
fakeClient
:=
dynamicfakeclient
.
NewSimpleDynamicClient
(
scheme
)
fakeClient
:=
dynamicfakeclient
.
NewSimpleDynamicClient
(
scheme
)
...
@@ -423,12 +469,14 @@ func TestWaitForCondition(t *testing.T) {
...
@@ -423,12 +469,14 @@ func TestWaitForCondition(t *testing.T) {
},
},
{
{
name
:
"handles watch condition change"
,
name
:
"handles watch condition change"
,
info
:
&
resource
.
Info
{
infos
:
[]
*
resource
.
Info
{
Mapping
:
&
meta
.
RESTMapping
{
{
Resource
:
schema
.
GroupVersionResource
{
Group
:
"group"
,
Version
:
"version"
,
Resource
:
"theresource"
},
Mapping
:
&
meta
.
RESTMapping
{
Resource
:
schema
.
GroupVersionResource
{
Group
:
"group"
,
Version
:
"version"
,
Resource
:
"theresource"
},
},
Name
:
"name-foo"
,
Namespace
:
"ns-foo"
,
},
},
Name
:
"name-foo"
,
Namespace
:
"ns-foo"
,
},
},
fakeClient
:
func
()
*
dynamicfakeclient
.
FakeDynamicClient
{
fakeClient
:
func
()
*
dynamicfakeclient
.
FakeDynamicClient
{
fakeClient
:=
dynamicfakeclient
.
NewSimpleDynamicClient
(
scheme
)
fakeClient
:=
dynamicfakeclient
.
NewSimpleDynamicClient
(
scheme
)
...
@@ -461,12 +509,14 @@ func TestWaitForCondition(t *testing.T) {
...
@@ -461,12 +509,14 @@ func TestWaitForCondition(t *testing.T) {
},
},
{
{
name
:
"handles watch created"
,
name
:
"handles watch created"
,
info
:
&
resource
.
Info
{
infos
:
[]
*
resource
.
Info
{
Mapping
:
&
meta
.
RESTMapping
{
{
Resource
:
schema
.
GroupVersionResource
{
Group
:
"group"
,
Version
:
"version"
,
Resource
:
"theresource"
},
Mapping
:
&
meta
.
RESTMapping
{
Resource
:
schema
.
GroupVersionResource
{
Group
:
"group"
,
Version
:
"version"
,
Resource
:
"theresource"
},
},
Name
:
"name-foo"
,
Namespace
:
"ns-foo"
,
},
},
Name
:
"name-foo"
,
Namespace
:
"ns-foo"
,
},
},
fakeClient
:
func
()
*
dynamicfakeclient
.
FakeDynamicClient
{
fakeClient
:
func
()
*
dynamicfakeclient
.
FakeDynamicClient
{
fakeClient
:=
dynamicfakeclient
.
NewSimpleDynamicClient
(
scheme
)
fakeClient
:=
dynamicfakeclient
.
NewSimpleDynamicClient
(
scheme
)
...
@@ -500,7 +550,7 @@ func TestWaitForCondition(t *testing.T) {
...
@@ -500,7 +550,7 @@ func TestWaitForCondition(t *testing.T) {
t
.
Run
(
test
.
name
,
func
(
t
*
testing
.
T
)
{
t
.
Run
(
test
.
name
,
func
(
t
*
testing
.
T
)
{
fakeClient
:=
test
.
fakeClient
()
fakeClient
:=
test
.
fakeClient
()
o
:=
&
WaitOptions
{
o
:=
&
WaitOptions
{
ResourceFinder
:
genericclioptions
.
NewSimpleFakeResourceFinder
(
test
.
info
),
ResourceFinder
:
genericclioptions
.
NewSimpleFakeResourceFinder
(
test
.
info
s
...
),
DynamicClient
:
fakeClient
,
DynamicClient
:
fakeClient
,
Timeout
:
test
.
timeout
,
Timeout
:
test
.
timeout
,
...
...
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