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
4ece39ac
Commit
4ece39ac
authored
Aug 07, 2015
by
Satnam Singh
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #9452 from yifan-gu/img_pulling
kubelet: Add 'image pulling' event. Refactor dockerManager.createPodInfraContainer()
parents
2986c2fd
053db8db
Hide whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
92 additions
and
53 deletions
+92
-53
fake.go
pkg/client/record/fake.go
+11
-3
runtime.go
pkg/kubelet/container/runtime.go
+4
-1
manager.go
pkg/kubelet/dockertools/manager.go
+23
-37
manager_test.go
pkg/kubelet/dockertools/manager_test.go
+44
-11
runtime_hooks.go
pkg/kubelet/runtime_hooks.go
+10
-1
No files found.
pkg/client/record/fake.go
View file @
4ece39ac
...
@@ -17,16 +17,24 @@ limitations under the License.
...
@@ -17,16 +17,24 @@ limitations under the License.
package
record
package
record
import
(
import
(
"fmt"
"k8s.io/kubernetes/pkg/runtime"
"k8s.io/kubernetes/pkg/runtime"
"k8s.io/kubernetes/pkg/util"
"k8s.io/kubernetes/pkg/util"
)
)
// FakeRecorder is used as a fake during tests.
// FakeRecorder is used as a fake during tests.
type
FakeRecorder
struct
{}
type
FakeRecorder
struct
{
Events
[]
string
}
func
(
f
*
FakeRecorder
)
Event
(
object
runtime
.
Object
,
reason
,
message
string
)
{}
func
(
f
*
FakeRecorder
)
Event
(
object
runtime
.
Object
,
reason
,
message
string
)
{
f
.
Events
=
append
(
f
.
Events
,
fmt
.
Sprintf
(
"%s %s"
,
reason
,
message
))
}
func
(
f
*
FakeRecorder
)
Eventf
(
object
runtime
.
Object
,
reason
,
messageFmt
string
,
args
...
interface
{})
{}
func
(
f
*
FakeRecorder
)
Eventf
(
object
runtime
.
Object
,
reason
,
messageFmt
string
,
args
...
interface
{})
{
f
.
Events
=
append
(
f
.
Events
,
fmt
.
Sprintf
(
reason
+
" "
+
messageFmt
,
args
...
))
}
func
(
f
*
FakeRecorder
)
PastEventf
(
object
runtime
.
Object
,
timestamp
util
.
Time
,
reason
,
messageFmt
string
,
args
...
interface
{})
{
func
(
f
*
FakeRecorder
)
PastEventf
(
object
runtime
.
Object
,
timestamp
util
.
Time
,
reason
,
messageFmt
string
,
args
...
interface
{})
{
}
}
pkg/kubelet/container/runtime.go
View file @
4ece39ac
...
@@ -105,9 +105,12 @@ type RuntimeHooks interface {
...
@@ -105,9 +105,12 @@ type RuntimeHooks interface {
// Determines whether the runtime should pull the specified container's image.
// Determines whether the runtime should pull the specified container's image.
ShouldPullImage
(
pod
*
api
.
Pod
,
container
*
api
.
Container
,
imagePresent
bool
)
bool
ShouldPullImage
(
pod
*
api
.
Pod
,
container
*
api
.
Container
,
imagePresent
bool
)
bool
// Runs when we start to pull an image.
ReportImagePulling
(
pod
*
api
.
Pod
,
container
*
api
.
Container
)
// Runs after an image is pulled reporting its status. Error may be nil
// Runs after an image is pulled reporting its status. Error may be nil
// for a successful pull.
// for a successful pull.
ReportImagePull
(
pod
*
api
.
Pod
,
container
*
api
.
Container
,
err
error
)
ReportImagePull
ed
(
pod
*
api
.
Pod
,
container
*
api
.
Container
,
err
error
)
}
}
// Pod is a group of containers, with the status of the pod.
// Pod is a group of containers, with the status of the pod.
...
...
pkg/kubelet/dockertools/manager.go
View file @
4ece39ac
...
@@ -67,6 +67,9 @@ const (
...
@@ -67,6 +67,9 @@ const (
// DockerManager implements the Runtime interface.
// DockerManager implements the Runtime interface.
var
_
kubecontainer
.
Runtime
=
&
DockerManager
{}
var
_
kubecontainer
.
Runtime
=
&
DockerManager
{}
// TODO: make this a TTL based pull (if image older than X policy, pull)
var
podInfraContainerImagePullPolicy
=
api
.
PullIfNotPresent
type
DockerManager
struct
{
type
DockerManager
struct
{
client
DockerInterface
client
DockerInterface
recorder
record
.
EventRecorder
recorder
record
.
EventRecorder
...
@@ -866,9 +869,10 @@ func (dm *DockerManager) podInfraContainerChanged(pod *api.Pod, podInfraContaine
...
@@ -866,9 +869,10 @@ func (dm *DockerManager) podInfraContainerChanged(pod *api.Pod, podInfraContaine
}
}
}
}
expectedPodInfraContainer
:=
&
api
.
Container
{
expectedPodInfraContainer
:=
&
api
.
Container
{
Name
:
PodInfraContainerName
,
Name
:
PodInfraContainerName
,
Image
:
dm
.
podInfraContainerImage
,
Image
:
dm
.
podInfraContainerImage
,
Ports
:
ports
,
Ports
:
ports
,
ImagePullPolicy
:
podInfraContainerImagePullPolicy
,
}
}
return
podInfraContainer
.
Hash
!=
kubecontainer
.
HashContainer
(
expectedPodInfraContainer
),
nil
return
podInfraContainer
.
Hash
!=
kubecontainer
.
HashContainer
(
expectedPodInfraContainer
),
nil
}
}
...
@@ -1357,37 +1361,16 @@ func (dm *DockerManager) createPodInfraContainer(pod *api.Pod) (kubeletTypes.Doc
...
@@ -1357,37 +1361,16 @@ func (dm *DockerManager) createPodInfraContainer(pod *api.Pod) (kubeletTypes.Doc
}
}
container
:=
&
api
.
Container
{
container
:=
&
api
.
Container
{
Name
:
PodInfraContainerName
,
Name
:
PodInfraContainerName
,
Image
:
dm
.
podInfraContainerImage
,
Image
:
dm
.
podInfraContainerImage
,
Ports
:
ports
,
Ports
:
ports
,
ImagePullPolicy
:
podInfraContainerImagePullPolicy
,
}
}
ref
,
err
:=
kubecontainer
.
GenerateContainerRef
(
pod
,
container
)
if
err
!=
nil
{
// No pod secrets for the infra container.
glog
.
Errorf
(
"Couldn't make a ref to pod %v, container %v: '%v'"
,
pod
.
Name
,
container
.
Name
,
err
)
if
err
:=
dm
.
pullImage
(
pod
,
container
,
nil
);
err
!=
nil
{
}
spec
:=
kubecontainer
.
ImageSpec
{
container
.
Image
}
// TODO: make this a TTL based pull (if image older than X policy, pull)
ok
,
err
:=
dm
.
IsImagePresent
(
spec
)
if
err
!=
nil
{
if
ref
!=
nil
{
dm
.
recorder
.
Eventf
(
ref
,
"failed"
,
"Failed to inspect image %q: %v"
,
container
.
Image
,
err
)
}
return
""
,
err
return
""
,
err
}
}
if
!
ok
{
if
err
:=
dm
.
PullImage
(
spec
,
nil
/* no pod secrets for the infra container */
);
err
!=
nil
{
if
ref
!=
nil
{
dm
.
recorder
.
Eventf
(
ref
,
"failed"
,
"Failed to pull image %q: %v"
,
container
.
Image
,
err
)
}
return
""
,
err
}
if
ref
!=
nil
{
dm
.
recorder
.
Eventf
(
ref
,
"pulled"
,
"Successfully pulled Pod container image %q"
,
container
.
Image
)
}
}
if
ok
&&
ref
!=
nil
{
dm
.
recorder
.
Eventf
(
ref
,
"pulled"
,
"Pod container image %q already present on machine"
,
container
.
Image
)
}
id
,
err
:=
dm
.
runContainerInPod
(
pod
,
container
,
netNamespace
,
""
)
id
,
err
:=
dm
.
runContainerInPod
(
pod
,
container
,
netNamespace
,
""
)
if
err
!=
nil
{
if
err
!=
nil
{
...
@@ -1544,25 +1527,28 @@ func (dm *DockerManager) clearReasonCache(pod *api.Pod, container *api.Container
...
@@ -1544,25 +1527,28 @@ func (dm *DockerManager) clearReasonCache(pod *api.Pod, container *api.Container
// Pull the image for the specified pod and container.
// Pull the image for the specified pod and container.
func
(
dm
*
DockerManager
)
pullImage
(
pod
*
api
.
Pod
,
container
*
api
.
Container
,
pullSecrets
[]
api
.
Secret
)
error
{
func
(
dm
*
DockerManager
)
pullImage
(
pod
*
api
.
Pod
,
container
*
api
.
Container
,
pullSecrets
[]
api
.
Secret
)
error
{
ref
,
err
:=
kubecontainer
.
GenerateContainerRef
(
pod
,
container
)
if
err
!=
nil
{
glog
.
Errorf
(
"Couldn't make a ref to pod %v, container %v: '%v'"
,
pod
.
Name
,
container
.
Name
,
err
)
}
spec
:=
kubecontainer
.
ImageSpec
{
container
.
Image
}
spec
:=
kubecontainer
.
ImageSpec
{
container
.
Image
}
present
,
err
:=
dm
.
IsImagePresent
(
spec
)
present
,
err
:=
dm
.
IsImagePresent
(
spec
)
if
err
!=
nil
{
if
err
!=
nil
{
ref
,
err
:=
kubecontainer
.
GenerateContainerRef
(
pod
,
container
)
if
err
!=
nil
{
glog
.
Errorf
(
"Couldn't make a ref to pod %v, container %v: '%v'"
,
pod
.
Name
,
container
.
Name
,
err
)
}
if
ref
!=
nil
{
if
ref
!=
nil
{
dm
.
recorder
.
Eventf
(
ref
,
"failed"
,
"Failed to inspect image %q: %v"
,
container
.
Image
,
err
)
dm
.
recorder
.
Eventf
(
ref
,
"failed"
,
"Failed to inspect image %q: %v"
,
container
.
Image
,
err
)
}
}
return
fmt
.
Errorf
(
"failed to inspect image %q: %v"
,
container
.
Image
,
err
)
return
fmt
.
Errorf
(
"failed to inspect image %q: %v"
,
container
.
Image
,
err
)
}
}
if
!
dm
.
runtimeHooks
.
ShouldPullImage
(
pod
,
container
,
present
)
{
if
!
dm
.
runtimeHooks
.
ShouldPullImage
(
pod
,
container
,
present
)
{
if
present
&&
ref
!=
nil
{
dm
.
recorder
.
Eventf
(
ref
,
"pulled"
,
"Container image %q already present on machine"
,
container
.
Image
)
}
return
nil
return
nil
}
}
dm
.
runtimeHooks
.
ReportImagePulling
(
pod
,
container
)
err
=
dm
.
PullImage
(
spec
,
pullSecrets
)
err
=
dm
.
PullImage
(
spec
,
pullSecrets
)
dm
.
runtimeHooks
.
ReportImagePull
(
pod
,
container
,
err
)
dm
.
runtimeHooks
.
ReportImagePull
ed
(
pod
,
container
,
err
)
return
err
return
err
}
}
...
...
pkg/kubelet/dockertools/manager_test.go
View file @
4ece39ac
...
@@ -80,7 +80,12 @@ func (fr *fakeRuntimeHooks) ShouldPullImage(pod *api.Pod, container *api.Contain
...
@@ -80,7 +80,12 @@ func (fr *fakeRuntimeHooks) ShouldPullImage(pod *api.Pod, container *api.Contain
return
false
return
false
}
}
func
(
fr
*
fakeRuntimeHooks
)
ReportImagePull
(
pod
*
api
.
Pod
,
container
*
api
.
Container
,
pullError
error
)
{
func
(
fr
*
fakeRuntimeHooks
)
ReportImagePulling
(
pod
*
api
.
Pod
,
container
*
api
.
Container
)
{
fr
.
recorder
.
Eventf
(
nil
,
"pulling"
,
fmt
.
Sprintf
(
"%s:%s:%s"
,
pod
.
Name
,
container
.
Name
,
container
.
Image
))
}
func
(
fr
*
fakeRuntimeHooks
)
ReportImagePulled
(
pod
*
api
.
Pod
,
container
*
api
.
Container
,
pullError
error
)
{
fr
.
recorder
.
Eventf
(
nil
,
"pulled"
,
fmt
.
Sprintf
(
"%s:%s:%s"
,
pod
.
Name
,
container
.
Name
,
container
.
Image
))
}
}
type
fakeOptionGenerator
struct
{}
type
fakeOptionGenerator
struct
{}
...
@@ -865,9 +870,10 @@ func generatePodInfraContainerHash(pod *api.Pod) uint64 {
...
@@ -865,9 +870,10 @@ func generatePodInfraContainerHash(pod *api.Pod) uint64 {
}
}
container
:=
&
api
.
Container
{
container
:=
&
api
.
Container
{
Name
:
PodInfraContainerName
,
Name
:
PodInfraContainerName
,
Image
:
PodInfraContainerImage
,
Image
:
PodInfraContainerImage
,
Ports
:
ports
,
Ports
:
ports
,
ImagePullPolicy
:
podInfraContainerImagePullPolicy
,
}
}
return
kubecontainer
.
HashContainer
(
container
)
return
kubecontainer
.
HashContainer
(
container
)
}
}
...
@@ -893,7 +899,7 @@ func runSyncPod(t *testing.T, dm *DockerManager, fakeDocker *FakeDockerClient, p
...
@@ -893,7 +899,7 @@ func runSyncPod(t *testing.T, dm *DockerManager, fakeDocker *FakeDockerClient, p
func
TestSyncPodCreateNetAndContainer
(
t
*
testing
.
T
)
{
func
TestSyncPodCreateNetAndContainer
(
t
*
testing
.
T
)
{
dm
,
fakeDocker
:=
newTestDockerManager
()
dm
,
fakeDocker
:=
newTestDockerManager
()
dm
.
podInfraContainerImage
=
"
custom_image_nam
e"
dm
.
podInfraContainerImage
=
"
pod_infra_imag
e"
fakeDocker
.
ContainerList
=
[]
docker
.
APIContainers
{}
fakeDocker
.
ContainerList
=
[]
docker
.
APIContainers
{}
pod
:=
&
api
.
Pod
{
pod
:=
&
api
.
Pod
{
ObjectMeta
:
api
.
ObjectMeta
{
ObjectMeta
:
api
.
ObjectMeta
{
...
@@ -920,7 +926,7 @@ func TestSyncPodCreateNetAndContainer(t *testing.T) {
...
@@ -920,7 +926,7 @@ func TestSyncPodCreateNetAndContainer(t *testing.T) {
found
:=
false
found
:=
false
for
_
,
c
:=
range
fakeDocker
.
ContainerList
{
for
_
,
c
:=
range
fakeDocker
.
ContainerList
{
if
c
.
Image
==
"
custom_image_nam
e"
&&
strings
.
HasPrefix
(
c
.
Names
[
0
],
"/k8s_POD"
)
{
if
c
.
Image
==
"
pod_infra_imag
e"
&&
strings
.
HasPrefix
(
c
.
Names
[
0
],
"/k8s_POD"
)
{
found
=
true
found
=
true
}
}
}
}
...
@@ -938,10 +944,10 @@ func TestSyncPodCreateNetAndContainer(t *testing.T) {
...
@@ -938,10 +944,10 @@ func TestSyncPodCreateNetAndContainer(t *testing.T) {
func
TestSyncPodCreatesNetAndContainerPullsImage
(
t
*
testing
.
T
)
{
func
TestSyncPodCreatesNetAndContainerPullsImage
(
t
*
testing
.
T
)
{
dm
,
fakeDocker
:=
newTestDockerManager
()
dm
,
fakeDocker
:=
newTestDockerManager
()
dm
.
podInfraContainerImage
=
"
custom_image_nam
e"
dm
.
podInfraContainerImage
=
"
pod_infra_imag
e"
puller
:=
dm
.
puller
.
(
*
FakeDockerPuller
)
puller
:=
dm
.
puller
.
(
*
FakeDockerPuller
)
puller
.
HasImages
=
[]
string
{}
puller
.
HasImages
=
[]
string
{}
dm
.
podInfraContainerImage
=
"
custom_image_nam
e"
dm
.
podInfraContainerImage
=
"
pod_infra_imag
e"
fakeDocker
.
ContainerList
=
[]
docker
.
APIContainers
{}
fakeDocker
.
ContainerList
=
[]
docker
.
APIContainers
{}
pod
:=
&
api
.
Pod
{
pod
:=
&
api
.
Pod
{
ObjectMeta
:
api
.
ObjectMeta
{
ObjectMeta
:
api
.
ObjectMeta
{
...
@@ -967,7 +973,7 @@ func TestSyncPodCreatesNetAndContainerPullsImage(t *testing.T) {
...
@@ -967,7 +973,7 @@ func TestSyncPodCreatesNetAndContainerPullsImage(t *testing.T) {
fakeDocker
.
Lock
()
fakeDocker
.
Lock
()
if
!
reflect
.
DeepEqual
(
puller
.
ImagesPulled
,
[]
string
{
"
custom_image_nam
e"
,
"something"
})
{
if
!
reflect
.
DeepEqual
(
puller
.
ImagesPulled
,
[]
string
{
"
pod_infra_imag
e"
,
"something"
})
{
t
.
Errorf
(
"Unexpected pulled containers: %v"
,
puller
.
ImagesPulled
)
t
.
Errorf
(
"Unexpected pulled containers: %v"
,
puller
.
ImagesPulled
)
}
}
...
@@ -1298,10 +1304,11 @@ func TestSyncPodsDoesNothing(t *testing.T) {
...
@@ -1298,10 +1304,11 @@ func TestSyncPodsDoesNothing(t *testing.T) {
}
}
func
TestSyncPodWithPullPolicy
(
t
*
testing
.
T
)
{
func
TestSyncPodWithPullPolicy
(
t
*
testing
.
T
)
{
api
.
ForTesting_ReferencesAllowBlankSelfLinks
=
true
dm
,
fakeDocker
:=
newTestDockerManager
()
dm
,
fakeDocker
:=
newTestDockerManager
()
puller
:=
dm
.
puller
.
(
*
FakeDockerPuller
)
puller
:=
dm
.
puller
.
(
*
FakeDockerPuller
)
puller
.
HasImages
=
[]
string
{
"existing_one"
,
"want:latest"
}
puller
.
HasImages
=
[]
string
{
"existing_one"
,
"want:latest"
}
dm
.
podInfraContainerImage
=
"
custom_image_nam
e"
dm
.
podInfraContainerImage
=
"
pod_infra_imag
e"
fakeDocker
.
ContainerList
=
[]
docker
.
APIContainers
{}
fakeDocker
.
ContainerList
=
[]
docker
.
APIContainers
{}
pod
:=
&
api
.
Pod
{
pod
:=
&
api
.
Pod
{
...
@@ -1325,13 +1332,39 @@ func TestSyncPodWithPullPolicy(t *testing.T) {
...
@@ -1325,13 +1332,39 @@ func TestSyncPodWithPullPolicy(t *testing.T) {
fakeDocker
.
Lock
()
fakeDocker
.
Lock
()
eventSet
:=
[]
string
{
"pulling foo:POD:pod_infra_image"
,
"pulled foo:POD:pod_infra_image"
,
"pulling foo:bar:pull_always_image"
,
"pulled foo:bar:pull_always_image"
,
"pulling foo:bar2:pull_if_not_present_image"
,
"pulled foo:bar2:pull_if_not_present_image"
,
`pulled Container image "existing_one" already present on machine`
,
`pulled Container image "want:latest" already present on machine`
,
}
runtimeHooks
:=
dm
.
runtimeHooks
.
(
*
fakeRuntimeHooks
)
recorder
:=
runtimeHooks
.
recorder
.
(
*
record
.
FakeRecorder
)
var
actualEvents
[]
string
for
_
,
ev
:=
range
recorder
.
Events
{
if
strings
.
HasPrefix
(
ev
,
"pull"
)
{
actualEvents
=
append
(
actualEvents
,
ev
)
}
}
sort
.
StringSlice
(
actualEvents
)
.
Sort
()
sort
.
StringSlice
(
eventSet
)
.
Sort
()
if
!
reflect
.
DeepEqual
(
actualEvents
,
eventSet
)
{
t
.
Errorf
(
"Expected: %#v, Actual: %#v"
,
eventSet
,
actualEvents
)
}
pulledImageSet
:=
make
(
map
[
string
]
empty
)
pulledImageSet
:=
make
(
map
[
string
]
empty
)
for
v
:=
range
puller
.
ImagesPulled
{
for
v
:=
range
puller
.
ImagesPulled
{
pulledImageSet
[
puller
.
ImagesPulled
[
v
]]
=
empty
{}
pulledImageSet
[
puller
.
ImagesPulled
[
v
]]
=
empty
{}
}
}
if
!
reflect
.
DeepEqual
(
pulledImageSet
,
map
[
string
]
empty
{
if
!
reflect
.
DeepEqual
(
pulledImageSet
,
map
[
string
]
empty
{
"
custom_image_name"
:
{},
"
pod_infra_image"
:
{},
"pull_always_image"
:
{},
"pull_always_image"
:
{},
"pull_if_not_present_image"
:
{},
"pull_if_not_present_image"
:
{},
})
{
})
{
...
...
pkg/kubelet/runtime_hooks.go
View file @
4ece39ac
...
@@ -49,7 +49,7 @@ func (kr *kubeletRuntimeHooks) ShouldPullImage(pod *api.Pod, container *api.Cont
...
@@ -49,7 +49,7 @@ func (kr *kubeletRuntimeHooks) ShouldPullImage(pod *api.Pod, container *api.Cont
return
false
return
false
}
}
func
(
kr
*
kubeletRuntimeHooks
)
ReportImagePull
(
pod
*
api
.
Pod
,
container
*
api
.
Container
,
pullError
error
)
{
func
(
kr
*
kubeletRuntimeHooks
)
ReportImagePull
ed
(
pod
*
api
.
Pod
,
container
*
api
.
Container
,
pullError
error
)
{
ref
,
err
:=
kubecontainer
.
GenerateContainerRef
(
pod
,
container
)
ref
,
err
:=
kubecontainer
.
GenerateContainerRef
(
pod
,
container
)
if
err
!=
nil
{
if
err
!=
nil
{
glog
.
Errorf
(
"Couldn't make a ref to pod %q, container %q: '%v'"
,
pod
.
Name
,
container
.
Name
,
err
)
glog
.
Errorf
(
"Couldn't make a ref to pod %q, container %q: '%v'"
,
pod
.
Name
,
container
.
Name
,
err
)
...
@@ -62,3 +62,12 @@ func (kr *kubeletRuntimeHooks) ReportImagePull(pod *api.Pod, container *api.Cont
...
@@ -62,3 +62,12 @@ func (kr *kubeletRuntimeHooks) ReportImagePull(pod *api.Pod, container *api.Cont
kr
.
recorder
.
Eventf
(
ref
,
"pulled"
,
"Successfully pulled image %q"
,
container
.
Image
)
kr
.
recorder
.
Eventf
(
ref
,
"pulled"
,
"Successfully pulled image %q"
,
container
.
Image
)
}
}
}
}
func
(
kr
*
kubeletRuntimeHooks
)
ReportImagePulling
(
pod
*
api
.
Pod
,
container
*
api
.
Container
)
{
ref
,
err
:=
kubecontainer
.
GenerateContainerRef
(
pod
,
container
)
if
err
!=
nil
{
glog
.
Errorf
(
"Couldn't make a ref to pod %q, container %q: '%v'"
,
pod
.
Name
,
container
.
Name
,
err
)
return
}
kr
.
recorder
.
Eventf
(
ref
,
"pulling"
,
"Pulling image %q"
,
container
.
Image
)
}
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