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
f775b24b
Commit
f775b24b
authored
Sep 11, 2015
by
k8s-merge-robot
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #13462 from ashcrow/master-tests
Auto commit by PR queue bot
parents
3758a01c
66dff577
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
402 additions
and
87 deletions
+402
-87
master.go
pkg/master/master.go
+1
-0
master_test.go
pkg/master/master_test.go
+401
-87
No files found.
pkg/master/master.go
View file @
f775b24b
...
@@ -1024,6 +1024,7 @@ func (m *Master) setupSecureProxy(user, privateKeyfile, publicKeyfile string) {
...
@@ -1024,6 +1024,7 @@ func (m *Master) setupSecureProxy(user, privateKeyfile, publicKeyfile string) {
}
}
func
(
m
*
Master
)
generateSSHKey
(
user
,
privateKeyfile
,
publicKeyfile
string
)
error
{
func
(
m
*
Master
)
generateSSHKey
(
user
,
privateKeyfile
,
publicKeyfile
string
)
error
{
// TODO: user is not used. Consider removing it as an input to the function.
private
,
public
,
err
:=
util
.
GenerateKey
(
2048
)
private
,
public
,
err
:=
util
.
GenerateKey
(
2048
)
if
err
!=
nil
{
if
err
!=
nil
{
return
err
return
err
...
...
pkg/master/master_test.go
View file @
f775b24b
...
@@ -19,25 +19,41 @@ package master
...
@@ -19,25 +19,41 @@ package master
import
(
import
(
"bytes"
"bytes"
"encoding/json"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"io/ioutil"
"net"
"net/http"
"net/http"
"net/http/httptest"
"net/http/httptest"
"os"
"path/filepath"
"reflect"
"reflect"
"testing"
"testing"
"time"
"github.com/emicklei/go-restful"
"github.com/stretchr/testify/assert"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/latest"
"k8s.io/kubernetes/pkg/api/latest"
"k8s.io/kubernetes/pkg/api/meta"
"k8s.io/kubernetes/pkg/api/v1"
"k8s.io/kubernetes/pkg/apiserver"
client
"k8s.io/kubernetes/pkg/client/unversioned"
"k8s.io/kubernetes/pkg/expapi"
"k8s.io/kubernetes/pkg/expapi"
explatest
"k8s.io/kubernetes/pkg/expapi/latest"
explatest
"k8s.io/kubernetes/pkg/expapi/latest"
"k8s.io/kubernetes/pkg/fields"
"k8s.io/kubernetes/pkg/labels"
"k8s.io/kubernetes/pkg/registry/endpoint"
"k8s.io/kubernetes/pkg/registry/namespace"
"k8s.io/kubernetes/pkg/registry/registrytest"
"k8s.io/kubernetes/pkg/registry/registrytest"
etcdstorage
"k8s.io/kubernetes/pkg/storage/etcd"
etcdstorage
"k8s.io/kubernetes/pkg/storage/etcd"
"k8s.io/kubernetes/pkg/tools"
"k8s.io/kubernetes/pkg/tools"
"k8s.io/kubernetes/pkg/tools/etcdtest"
"k8s.io/kubernetes/pkg/tools/etcdtest"
"k8s.io/kubernetes/pkg/util"
"github.com/emicklei/go-restful"
)
)
func
TestGetServersToValidate
(
t
*
testing
.
T
)
{
// setUp is a convience function for setting up for (most) tests.
func
setUp
(
t
*
testing
.
T
)
(
Master
,
Config
,
*
assert
.
Assertions
)
{
master
:=
Master
{}
master
:=
Master
{}
config
:=
Config
{}
config
:=
Config
{}
fakeClient
:=
tools
.
NewFakeEtcdClient
(
t
)
fakeClient
:=
tools
.
NewFakeEtcdClient
(
t
)
...
@@ -47,11 +63,64 @@ func TestGetServersToValidate(t *testing.T) {
...
@@ -47,11 +63,64 @@ func TestGetServersToValidate(t *testing.T) {
master
.
nodeRegistry
=
registrytest
.
NewNodeRegistry
([]
string
{
"node1"
,
"node2"
},
api
.
NodeResources
{})
master
.
nodeRegistry
=
registrytest
.
NewNodeRegistry
([]
string
{
"node1"
,
"node2"
},
api
.
NodeResources
{})
return
master
,
config
,
assert
.
New
(
t
)
}
// TestNew verifies that the New function returns a Master
// using the configuration properly.
func
TestNew
(
t
*
testing
.
T
)
{
_
,
config
,
assert
:=
setUp
(
t
)
config
.
KubeletClient
=
client
.
FakeKubeletClient
{}
master
:=
New
(
&
config
)
// Verify many of the variables match their config counterparts
assert
.
Equal
(
master
.
enableCoreControllers
,
config
.
EnableCoreControllers
)
assert
.
Equal
(
master
.
enableLogsSupport
,
config
.
EnableLogsSupport
)
assert
.
Equal
(
master
.
enableUISupport
,
config
.
EnableUISupport
)
assert
.
Equal
(
master
.
enableSwaggerSupport
,
config
.
EnableSwaggerSupport
)
assert
.
Equal
(
master
.
enableSwaggerSupport
,
config
.
EnableSwaggerSupport
)
assert
.
Equal
(
master
.
enableProfiling
,
config
.
EnableProfiling
)
assert
.
Equal
(
master
.
apiPrefix
,
config
.
APIPrefix
)
assert
.
Equal
(
master
.
expAPIPrefix
,
config
.
ExpAPIPrefix
)
assert
.
Equal
(
master
.
corsAllowedOriginList
,
config
.
CorsAllowedOriginList
)
assert
.
Equal
(
master
.
authenticator
,
config
.
Authenticator
)
assert
.
Equal
(
master
.
authorizer
,
config
.
Authorizer
)
assert
.
Equal
(
master
.
admissionControl
,
config
.
AdmissionControl
)
assert
.
Equal
(
master
.
v1
,
!
config
.
DisableV1
)
assert
.
Equal
(
master
.
exp
,
config
.
EnableExp
)
assert
.
Equal
(
master
.
requestContextMapper
,
config
.
RequestContextMapper
)
assert
.
Equal
(
master
.
cacheTimeout
,
config
.
CacheTimeout
)
assert
.
Equal
(
master
.
masterCount
,
config
.
MasterCount
)
assert
.
Equal
(
master
.
externalHost
,
config
.
ExternalHost
)
assert
.
Equal
(
master
.
clusterIP
,
config
.
PublicAddress
)
assert
.
Equal
(
master
.
publicReadWritePort
,
config
.
ReadWritePort
)
assert
.
Equal
(
master
.
serviceReadWriteIP
,
config
.
ServiceReadWriteIP
)
assert
.
Equal
(
master
.
installSSHKey
,
config
.
InstallSSHKey
)
}
// TestNewEtcdStorage verifies that the usage of NewEtcdStorage reacts properly when
// the correct data is input
func
TestNewEtcdStorage
(
t
*
testing
.
T
)
{
assert
:=
assert
.
New
(
t
)
fakeClient
:=
tools
.
NewFakeEtcdClient
(
t
)
// Pass case
_
,
err
:=
NewEtcdStorage
(
fakeClient
,
latest
.
InterfacesFor
,
latest
.
Version
,
etcdtest
.
PathPrefix
())
assert
.
NoError
(
err
,
"Unable to create etcdstorage: %s"
,
err
)
// Fail case
errorFunc
:=
func
(
apiVersion
string
)
(
*
meta
.
VersionInterfaces
,
error
)
{
return
nil
,
errors
.
New
(
"ERROR"
)
}
_
,
err
=
NewEtcdStorage
(
fakeClient
,
errorFunc
,
latest
.
Version
,
etcdtest
.
PathPrefix
())
assert
.
Error
(
err
,
"NewEtcdStorage should have failed"
)
}
// TestGetServersToValidate verifies the unexported getServersToValidate function
func
TestGetServersToValidate
(
t
*
testing
.
T
)
{
master
,
config
,
assert
:=
setUp
(
t
)
servers
:=
master
.
getServersToValidate
(
&
config
)
servers
:=
master
.
getServersToValidate
(
&
config
)
if
len
(
servers
)
!=
5
{
assert
.
Equal
(
5
,
len
(
servers
),
"unexpected server list: %#v"
,
servers
)
t
.
Errorf
(
"unexpected server list: %#v"
,
servers
)
}
for
_
,
server
:=
range
[]
string
{
"scheduler"
,
"controller-manager"
,
"etcd-0"
,
"etcd-1"
,
"etcd-2"
}
{
for
_
,
server
:=
range
[]
string
{
"scheduler"
,
"controller-manager"
,
"etcd-0"
,
"etcd-1"
,
"etcd-2"
}
{
if
_
,
ok
:=
servers
[
server
];
!
ok
{
if
_
,
ok
:=
servers
[
server
];
!
ok
{
t
.
Errorf
(
"server list missing: %s"
,
server
)
t
.
Errorf
(
"server list missing: %s"
,
server
)
...
@@ -59,7 +128,10 @@ func TestGetServersToValidate(t *testing.T) {
...
@@ -59,7 +128,10 @@ func TestGetServersToValidate(t *testing.T) {
}
}
}
}
// TestFindExternalAddress verifies both pass and fail cases for the unexported
// findExternalAddress function
func
TestFindExternalAddress
(
t
*
testing
.
T
)
{
func
TestFindExternalAddress
(
t
*
testing
.
T
)
{
assert
:=
assert
.
New
(
t
)
expectedIP
:=
"172.0.0.1"
expectedIP
:=
"172.0.0.1"
nodes
:=
[]
*
api
.
Node
{
new
(
api
.
Node
),
new
(
api
.
Node
),
new
(
api
.
Node
)}
nodes
:=
[]
*
api
.
Node
{
new
(
api
.
Node
),
new
(
api
.
Node
),
new
(
api
.
Node
)}
...
@@ -67,22 +139,289 @@ func TestFindExternalAddress(t *testing.T) {
...
@@ -67,22 +139,289 @@ func TestFindExternalAddress(t *testing.T) {
nodes
[
1
]
.
Status
.
Addresses
=
[]
api
.
NodeAddress
{{
"LegacyHostIP"
,
expectedIP
}}
nodes
[
1
]
.
Status
.
Addresses
=
[]
api
.
NodeAddress
{{
"LegacyHostIP"
,
expectedIP
}}
nodes
[
2
]
.
Status
.
Addresses
=
[]
api
.
NodeAddress
{{
"ExternalIP"
,
expectedIP
},
{
"LegacyHostIP"
,
"172.0.0.2"
}}
nodes
[
2
]
.
Status
.
Addresses
=
[]
api
.
NodeAddress
{{
"ExternalIP"
,
expectedIP
},
{
"LegacyHostIP"
,
"172.0.0.2"
}}
// Pass Case
for
_
,
node
:=
range
nodes
{
for
_
,
node
:=
range
nodes
{
ip
,
err
:=
findExternalAddress
(
node
)
ip
,
err
:=
findExternalAddress
(
node
)
if
err
!=
nil
{
assert
.
NoError
(
err
,
"error getting node external address"
)
t
.
Errorf
(
"error getting node external address: %s"
,
err
)
assert
.
Equal
(
expectedIP
,
ip
,
"expected ip to be %s, but was %s"
,
expectedIP
,
ip
)
}
if
ip
!=
expectedIP
{
t
.
Errorf
(
"expected ip to be %s, but was %s"
,
expectedIP
,
ip
)
}
}
}
// Fail case
_
,
err
:=
findExternalAddress
(
new
(
api
.
Node
))
_
,
err
:=
findExternalAddress
(
new
(
api
.
Node
))
if
err
==
nil
{
assert
.
Error
(
err
,
"expected findExternalAddress to fail on a node with missing ip information"
)
t
.
Errorf
(
"expected findExternalAddress to fail on a node with missing ip information"
)
}
// TestApi_v1 verifies that the unexported api_v1 function does indeed
// utilize the correct Version and Codec.
func
TestApi_v1
(
t
*
testing
.
T
)
{
master
,
_
,
assert
:=
setUp
(
t
)
version
:=
master
.
api_v1
()
assert
.
Equal
(
"v1"
,
version
.
Version
,
"Version was not v1: %s"
,
version
.
Version
)
assert
.
Equal
(
v1
.
Codec
,
version
.
Codec
,
"version.Codec was not for v1: %s"
,
version
.
Codec
)
for
k
,
v
:=
range
master
.
storage
{
assert
.
Contains
(
version
.
Storage
,
v
,
"Value %s not found (key: %s)"
,
k
,
v
)
}
}
}
}
// TestNewBootstrapController verifies master fields are properly copied into controller
func
TestNewBootstrapController
(
t
*
testing
.
T
)
{
// Tests a subset of inputs to ensure they are set properly in the controller
master
,
_
,
assert
:=
setUp
(
t
)
portRange
:=
util
.
PortRange
{
Base
:
10
,
Size
:
10
}
master
.
namespaceRegistry
=
namespace
.
NewRegistry
(
nil
)
master
.
serviceRegistry
=
registrytest
.
NewServiceRegistry
()
master
.
endpointRegistry
=
endpoint
.
NewRegistry
(
nil
)
master
.
serviceNodePortRange
=
portRange
master
.
masterCount
=
1
master
.
serviceReadWritePort
=
1000
master
.
publicReadWritePort
=
1010
controller
:=
master
.
NewBootstrapController
()
assert
.
Equal
(
controller
.
NamespaceRegistry
,
master
.
namespaceRegistry
)
assert
.
Equal
(
controller
.
EndpointRegistry
,
master
.
endpointRegistry
)
assert
.
Equal
(
controller
.
ServiceRegistry
,
master
.
serviceRegistry
)
assert
.
Equal
(
controller
.
ServiceNodePortRange
,
portRange
)
assert
.
Equal
(
controller
.
MasterCount
,
master
.
masterCount
)
assert
.
Equal
(
controller
.
ServicePort
,
master
.
serviceReadWritePort
)
assert
.
Equal
(
controller
.
PublicServicePort
,
master
.
publicReadWritePort
)
}
// TestNewHandlerContainer verifies that NewHandlerContainer uses the
// mux provided
func
TestNewHandlerContainer
(
t
*
testing
.
T
)
{
assert
:=
assert
.
New
(
t
)
mux
:=
http
.
NewServeMux
()
container
:=
NewHandlerContainer
(
mux
)
assert
.
Equal
(
mux
,
container
.
ServeMux
,
"ServerMux's do not match"
)
}
// TestHandleWithAuth verifies HandleWithAuth adds the path
// to the muxHelper.RegisteredPaths.
func
TestHandleWithAuth
(
t
*
testing
.
T
)
{
master
,
_
,
assert
:=
setUp
(
t
)
mh
:=
apiserver
.
MuxHelper
{
Mux
:
http
.
NewServeMux
()}
master
.
muxHelper
=
&
mh
handler
:=
func
(
r
http
.
ResponseWriter
,
w
*
http
.
Request
)
{
w
.
Write
(
nil
)
}
master
.
HandleWithAuth
(
"/test"
,
http
.
HandlerFunc
(
handler
))
assert
.
Contains
(
master
.
muxHelper
.
RegisteredPaths
,
"/test"
,
"Path not found in muxHelper"
)
}
// TestHandleFuncWithAuth verifies HandleFuncWithAuth adds the path
// to the muxHelper.RegisteredPaths.
func
TestHandleFuncWithAuth
(
t
*
testing
.
T
)
{
master
,
_
,
assert
:=
setUp
(
t
)
mh
:=
apiserver
.
MuxHelper
{
Mux
:
http
.
NewServeMux
()}
master
.
muxHelper
=
&
mh
handler
:=
func
(
r
http
.
ResponseWriter
,
w
*
http
.
Request
)
{
w
.
Write
(
nil
)
}
master
.
HandleFuncWithAuth
(
"/test"
,
handler
)
assert
.
Contains
(
master
.
muxHelper
.
RegisteredPaths
,
"/test"
,
"Path not found in muxHelper"
)
}
// TestInstallSwaggerAPI verifies that the swagger api is added
// at the proper endpoint.
func
TestInstallSwaggerAPI
(
t
*
testing
.
T
)
{
master
,
_
,
assert
:=
setUp
(
t
)
mux
:=
http
.
NewServeMux
()
master
.
handlerContainer
=
NewHandlerContainer
(
mux
)
// Ensure swagger isn't installed without the call
ws
:=
master
.
handlerContainer
.
RegisteredWebServices
()
if
!
assert
.
Equal
(
len
(
ws
),
0
)
{
for
x
:=
range
ws
{
assert
.
NotEqual
(
"/swaggerapi"
,
ws
[
x
]
.
RootPath
(),
"SwaggerAPI was installed without a call to InstallSwaggerAPI()"
)
}
}
// Install swagger and test
master
.
InstallSwaggerAPI
()
ws
=
master
.
handlerContainer
.
RegisteredWebServices
()
if
assert
.
NotEqual
(
0
,
len
(
ws
),
"SwaggerAPI not installed."
)
{
assert
.
Equal
(
"/swaggerapi/"
,
ws
[
0
]
.
RootPath
(),
"SwaggerAPI did not install to the proper path. %s != /swaggerapi"
,
ws
[
0
]
.
RootPath
())
}
// Empty externalHost verification
mux
=
http
.
NewServeMux
()
master
.
handlerContainer
=
NewHandlerContainer
(
mux
)
master
.
externalHost
=
""
master
.
clusterIP
=
net
.
IPv4
(
10
,
10
,
10
,
10
)
master
.
publicReadWritePort
=
1010
master
.
InstallSwaggerAPI
()
if
assert
.
NotEqual
(
0
,
len
(
ws
),
"SwaggerAPI not installed."
)
{
assert
.
Equal
(
"/swaggerapi/"
,
ws
[
0
]
.
RootPath
(),
"SwaggerAPI did not install to the proper path. %s != /swaggerapi"
,
ws
[
0
]
.
RootPath
())
}
}
// TestDefaultAPIGroupVersion verifies that the unexported defaultAPIGroupVersion
// creates the expected APIGroupVersion based off of master.
func
TestDefaultAPIGroupVersion
(
t
*
testing
.
T
)
{
master
,
_
,
assert
:=
setUp
(
t
)
master
.
dialer
=
func
(
network
,
addr
string
)
(
net
.
Conn
,
error
)
{
return
nil
,
nil
}
apiGroup
:=
master
.
defaultAPIGroupVersion
()
assert
.
Equal
(
apiGroup
.
Root
,
master
.
apiPrefix
)
assert
.
Equal
(
apiGroup
.
Admit
,
master
.
admissionControl
)
assert
.
Equal
(
apiGroup
.
Context
,
master
.
requestContextMapper
)
assert
.
Equal
(
apiGroup
.
MinRequestTimeout
,
master
.
minRequestTimeout
)
// These functions should be different instances of the same function
groupDialerFunc
:=
fmt
.
Sprintf
(
"%+v"
,
apiGroup
.
ProxyDialerFn
)
masterDialerFunc
:=
fmt
.
Sprintf
(
"%+v"
,
master
.
dialer
)
assert
.
Equal
(
groupDialerFunc
,
masterDialerFunc
)
}
// TestExpapi verifies that the unexported exapi creates
// the an experimental api APIGroupVersion.
func
TestExpapi
(
t
*
testing
.
T
)
{
master
,
config
,
assert
:=
setUp
(
t
)
expAPIGroup
:=
master
.
expapi
(
&
config
)
assert
.
Equal
(
expAPIGroup
.
Root
,
master
.
expAPIPrefix
)
assert
.
Equal
(
expAPIGroup
.
Mapper
,
explatest
.
RESTMapper
)
assert
.
Equal
(
expAPIGroup
.
Codec
,
explatest
.
Codec
)
assert
.
Equal
(
expAPIGroup
.
Linker
,
explatest
.
SelfLinker
)
assert
.
Equal
(
expAPIGroup
.
Version
,
explatest
.
Version
)
}
// TestSecondsSinceSync verifies that proper results are returned
// when checking the time between syncs
func
TestSecondsSinceSync
(
t
*
testing
.
T
)
{
master
,
_
,
assert
:=
setUp
(
t
)
master
.
lastSync
=
time
.
Date
(
2015
,
time
.
January
,
1
,
1
,
1
,
1
,
1
,
time
.
UTC
)
.
Unix
()
// Nano Second. No difference.
master
.
clock
=
&
util
.
FakeClock
{
Time
:
time
.
Date
(
2015
,
time
.
January
,
1
,
1
,
1
,
1
,
2
,
time
.
UTC
)}
assert
.
Equal
(
int64
(
0
),
master
.
secondsSinceSync
())
// Second
master
.
clock
=
&
util
.
FakeClock
{
Time
:
time
.
Date
(
2015
,
time
.
January
,
1
,
1
,
1
,
2
,
1
,
time
.
UTC
)}
assert
.
Equal
(
int64
(
1
),
master
.
secondsSinceSync
())
// Minute
master
.
clock
=
&
util
.
FakeClock
{
Time
:
time
.
Date
(
2015
,
time
.
January
,
1
,
1
,
2
,
1
,
1
,
time
.
UTC
)}
assert
.
Equal
(
int64
(
60
),
master
.
secondsSinceSync
())
// Hour
master
.
clock
=
&
util
.
FakeClock
{
Time
:
time
.
Date
(
2015
,
time
.
January
,
1
,
2
,
1
,
1
,
1
,
time
.
UTC
)}
assert
.
Equal
(
int64
(
3600
),
master
.
secondsSinceSync
())
// Day
master
.
clock
=
&
util
.
FakeClock
{
Time
:
time
.
Date
(
2015
,
time
.
January
,
2
,
1
,
1
,
1
,
1
,
time
.
UTC
)}
assert
.
Equal
(
int64
(
86400
),
master
.
secondsSinceSync
())
// Month
master
.
clock
=
&
util
.
FakeClock
{
Time
:
time
.
Date
(
2015
,
time
.
February
,
1
,
1
,
1
,
1
,
1
,
time
.
UTC
)}
assert
.
Equal
(
int64
(
2678400
),
master
.
secondsSinceSync
())
// Future Month. Should be -Month.
master
.
lastSync
=
time
.
Date
(
2015
,
time
.
February
,
1
,
1
,
1
,
1
,
1
,
time
.
UTC
)
.
Unix
()
master
.
clock
=
&
util
.
FakeClock
{
Time
:
time
.
Date
(
2015
,
time
.
January
,
1
,
1
,
1
,
1
,
1
,
time
.
UTC
)}
assert
.
Equal
(
int64
(
-
2678400
),
master
.
secondsSinceSync
())
}
// TestGetNodeAddresses verifies that proper results are returned
// when requesting node addresses.
func
TestGetNodeAddresses
(
t
*
testing
.
T
)
{
master
,
_
,
assert
:=
setUp
(
t
)
// Fail case (no addresses associated with nodes)
nodes
,
_
:=
master
.
nodeRegistry
.
ListNodes
(
api
.
NewDefaultContext
(),
labels
.
Everything
(),
fields
.
Everything
())
addrs
,
err
:=
master
.
getNodeAddresses
()
assert
.
Error
(
err
,
"getNodeAddresses should have caused an error as there are no addresses."
)
assert
.
Equal
([]
string
(
nil
),
addrs
)
// Pass case with External type IP
nodes
,
_
=
master
.
nodeRegistry
.
ListNodes
(
api
.
NewDefaultContext
(),
labels
.
Everything
(),
fields
.
Everything
())
for
index
:=
range
nodes
.
Items
{
nodes
.
Items
[
index
]
.
Status
.
Addresses
=
[]
api
.
NodeAddress
{{
Type
:
api
.
NodeExternalIP
,
Address
:
"127.0.0.1"
}}
}
addrs
,
err
=
master
.
getNodeAddresses
()
assert
.
NoError
(
err
,
"getNodeAddresses should not have returned an error."
)
assert
.
Equal
([]
string
{
"127.0.0.1"
,
"127.0.0.1"
},
addrs
)
// Pass case with LegacyHost type IP
nodes
,
_
=
master
.
nodeRegistry
.
ListNodes
(
api
.
NewDefaultContext
(),
labels
.
Everything
(),
fields
.
Everything
())
for
index
:=
range
nodes
.
Items
{
nodes
.
Items
[
index
]
.
Status
.
Addresses
=
[]
api
.
NodeAddress
{{
Type
:
api
.
NodeLegacyHostIP
,
Address
:
"127.0.0.2"
}}
}
addrs
,
err
=
master
.
getNodeAddresses
()
assert
.
NoError
(
err
,
"getNodeAddresses failback should not have returned an error."
)
assert
.
Equal
([]
string
{
"127.0.0.2"
,
"127.0.0.2"
},
addrs
)
}
// TestRefreshTunnels verifies that the function errors when no addresses
// are associated with nodes
func
TestRefreshTunnels
(
t
*
testing
.
T
)
{
master
,
_
,
assert
:=
setUp
(
t
)
// Fail case (no addresses associated with nodes)
assert
.
Error
(
master
.
refreshTunnels
(
"test"
,
"/tmp/undefined"
))
// TODO: pass case without needing actual connections?
}
// TestIsTunnelSyncHealthy verifies that the 600 second lag test
// is honored.
func
TestIsTunnelSyncHealthy
(
t
*
testing
.
T
)
{
master
,
_
,
assert
:=
setUp
(
t
)
// Pass case: 540 second lag
master
.
lastSync
=
time
.
Date
(
2015
,
time
.
January
,
1
,
1
,
1
,
1
,
1
,
time
.
UTC
)
.
Unix
()
master
.
clock
=
&
util
.
FakeClock
{
Time
:
time
.
Date
(
2015
,
time
.
January
,
1
,
1
,
9
,
1
,
1
,
time
.
UTC
)}
err
:=
master
.
IsTunnelSyncHealthy
(
nil
)
assert
.
NoError
(
err
,
"IsTunnelSyncHealthy() should not have returned an error."
)
// Fail case: 720 second lag
master
.
clock
=
&
util
.
FakeClock
{
Time
:
time
.
Date
(
2015
,
time
.
January
,
1
,
1
,
12
,
1
,
1
,
time
.
UTC
)}
err
=
master
.
IsTunnelSyncHealthy
(
nil
)
assert
.
Error
(
err
,
"IsTunnelSyncHealthy() should have returned an error."
)
}
// generateTempFile creates a temporary file path
func
generateTempFilePath
(
prefix
string
)
string
{
tmpPath
,
_
:=
filepath
.
Abs
(
fmt
.
Sprintf
(
"%s/%s-%d"
,
os
.
TempDir
(),
prefix
,
time
.
Now
()
.
Unix
()))
return
tmpPath
}
// TestGenerateSSHKey verifies that SSH key generation does indeed
// generate keys even with keys already exist.
func
TestGenerateSSHKey
(
t
*
testing
.
T
)
{
master
,
_
,
assert
:=
setUp
(
t
)
privateKey
:=
generateTempFilePath
(
"private"
)
publicKey
:=
generateTempFilePath
(
"public"
)
// Make sure we have no test keys laying around
os
.
Remove
(
privateKey
)
os
.
Remove
(
publicKey
)
// Pass case: Sunny day case
err
:=
master
.
generateSSHKey
(
"unused"
,
privateKey
,
publicKey
)
assert
.
NoError
(
err
,
"generateSSHKey should not have retuend an error: %s"
,
err
)
// Pass case: PrivateKey exists test case
os
.
Remove
(
publicKey
)
err
=
master
.
generateSSHKey
(
"unused"
,
privateKey
,
publicKey
)
assert
.
NoError
(
err
,
"generateSSHKey should not have retuend an error: %s"
,
err
)
// Pass case: PublicKey exists test case
os
.
Remove
(
privateKey
)
err
=
master
.
generateSSHKey
(
"unused"
,
privateKey
,
publicKey
)
assert
.
NoError
(
err
,
"generateSSHKey should not have retuend an error: %s"
,
err
)
// Make sure we have no test keys laying around
os
.
Remove
(
privateKey
)
os
.
Remove
(
publicKey
)
// TODO: testing error cases where the file can not be removed?
}
var
versionsToTest
=
[]
string
{
"v1"
,
"v3"
}
var
versionsToTest
=
[]
string
{
"v1"
,
"v3"
}
type
Foo
struct
{
type
Foo
struct
{
...
@@ -100,8 +439,9 @@ type FooList struct {
...
@@ -100,8 +439,9 @@ type FooList struct {
items
[]
Foo
`json:"items"`
items
[]
Foo
`json:"items"`
}
}
func
initThirdParty
(
t
*
testing
.
T
,
version
string
)
(
*
tools
.
FakeEtcdClient
,
*
httptest
.
Server
)
{
func
initThirdParty
(
t
*
testing
.
T
,
version
string
)
(
*
tools
.
FakeEtcdClient
,
*
httptest
.
Server
,
*
assert
.
Assertions
)
{
master
:=
&
Master
{}
master
,
_
,
assert
:=
setUp
(
t
)
api
:=
&
expapi
.
ThirdPartyResource
{
api
:=
&
expapi
.
ThirdPartyResource
{
ObjectMeta
:
api
.
ObjectMeta
{
ObjectMeta
:
api
.
ObjectMeta
{
Name
:
"foo.company.com"
,
Name
:
"foo.company.com"
,
...
@@ -119,13 +459,12 @@ func initThirdParty(t *testing.T, version string) (*tools.FakeEtcdClient, *httpt
...
@@ -119,13 +459,12 @@ func initThirdParty(t *testing.T, version string) (*tools.FakeEtcdClient, *httpt
fakeClient
.
Machines
=
[]
string
{
"http://machine1:4001"
,
"http://machine2"
,
"http://machine3:4003"
}
fakeClient
.
Machines
=
[]
string
{
"http://machine1:4001"
,
"http://machine2"
,
"http://machine3:4003"
}
master
.
thirdPartyStorage
=
etcdstorage
.
NewEtcdStorage
(
fakeClient
,
explatest
.
Codec
,
etcdtest
.
PathPrefix
())
master
.
thirdPartyStorage
=
etcdstorage
.
NewEtcdStorage
(
fakeClient
,
explatest
.
Codec
,
etcdtest
.
PathPrefix
())
if
err
:=
master
.
InstallThirdPartyAPI
(
api
);
err
!=
nil
{
if
!
assert
.
NoError
(
master
.
InstallThirdPartyAPI
(
api
))
{
t
.
Errorf
(
"unexpected error: %v"
,
err
)
t
.
FailNow
()
t
.
FailNow
()
}
}
server
:=
httptest
.
NewServer
(
master
.
handlerContainer
.
ServeMux
)
server
:=
httptest
.
NewServer
(
master
.
handlerContainer
.
ServeMux
)
return
fakeClient
,
server
return
fakeClient
,
server
,
assert
}
}
func
TestInstallThirdPartyAPIList
(
t
*
testing
.
T
)
{
func
TestInstallThirdPartyAPIList
(
t
*
testing
.
T
)
{
...
@@ -135,31 +474,27 @@ func TestInstallThirdPartyAPIList(t *testing.T) {
...
@@ -135,31 +474,27 @@ func TestInstallThirdPartyAPIList(t *testing.T) {
}
}
func
testInstallThirdPartyAPIListVersion
(
t
*
testing
.
T
,
version
string
)
{
func
testInstallThirdPartyAPIListVersion
(
t
*
testing
.
T
,
version
string
)
{
fakeClient
,
server
:=
initThirdParty
(
t
,
version
)
fakeClient
,
server
,
assert
:=
initThirdParty
(
t
,
version
)
defer
server
.
Close
()
defer
server
.
Close
()
fakeClient
.
ExpectNotFoundGet
(
etcdtest
.
PathPrefix
()
+
"/ThirdPartyResourceData/company.com/foos/default"
)
fakeClient
.
ExpectNotFoundGet
(
etcdtest
.
PathPrefix
()
+
"/ThirdPartyResourceData/company.com/foos/default"
)
resp
,
err
:=
http
.
Get
(
server
.
URL
+
"/thirdparty/company.com/"
+
version
+
"/namespaces/default/foos"
)
resp
,
err
:=
http
.
Get
(
server
.
URL
+
"/thirdparty/company.com/"
+
version
+
"/namespaces/default/foos"
)
if
err
!=
nil
{
if
!
assert
.
NoError
(
err
)
{
t
.
Errorf
(
"unexpected error: %v"
,
err
)
return
return
}
}
defer
resp
.
Body
.
Close
()
defer
resp
.
Body
.
Close
()
if
resp
.
StatusCode
!=
http
.
StatusOK
{
assert
.
Equal
(
http
.
StatusOK
,
resp
.
StatusCode
)
t
.
Errorf
(
"unexpected status: %v"
,
resp
)
}
data
,
err
:=
ioutil
.
ReadAll
(
resp
.
Body
)
data
,
err
:=
ioutil
.
ReadAll
(
resp
.
Body
)
if
err
!=
nil
{
assert
.
NoError
(
err
)
t
.
Errorf
(
"unexpected error: %v"
,
err
)
}
list
:=
FooList
{}
list
:=
FooList
{}
if
err
:=
json
.
Unmarshal
(
data
,
&
list
);
err
!=
nil
{
err
=
json
.
Unmarshal
(
data
,
&
list
)
t
.
Errorf
(
"unexpected error: %v"
,
err
)
assert
.
NoError
(
err
)
}
}
}
func
encodeToThirdParty
(
name
string
,
obj
interface
{})
([]
byte
,
error
)
{
func
encodeToThirdParty
(
name
string
,
obj
interface
{})
([]
byte
,
error
)
{
...
@@ -204,7 +539,7 @@ func TestInstallThirdPartyAPIGet(t *testing.T) {
...
@@ -204,7 +539,7 @@ func TestInstallThirdPartyAPIGet(t *testing.T) {
}
}
func
testInstallThirdPartyAPIGetVersion
(
t
*
testing
.
T
,
version
string
)
{
func
testInstallThirdPartyAPIGetVersion
(
t
*
testing
.
T
,
version
string
)
{
fakeClient
,
server
:=
initThirdParty
(
t
,
version
)
fakeClient
,
server
,
assert
:=
initThirdParty
(
t
,
version
)
defer
server
.
Close
()
defer
server
.
Close
()
expectedObj
:=
Foo
{
expectedObj
:=
Foo
{
...
@@ -218,28 +553,26 @@ func testInstallThirdPartyAPIGetVersion(t *testing.T, version string) {
...
@@ -218,28 +553,26 @@ func testInstallThirdPartyAPIGetVersion(t *testing.T, version string) {
SomeField
:
"test field"
,
SomeField
:
"test field"
,
OtherField
:
10
,
OtherField
:
10
,
}
}
if
err
:=
storeToEtcd
(
fakeClient
,
"/ThirdPartyResourceData/company.com/foos/default/test"
,
"test"
,
expectedObj
);
err
!=
nil
{
if
!
assert
.
NoError
(
storeToEtcd
(
fakeClient
,
"/ThirdPartyResourceData/company.com/foos/default/test"
,
"test"
,
expectedObj
))
{
t
.
Errorf
(
"unexpected error: %v"
,
err
)
t
.
FailNow
()
t
.
FailNow
()
return
return
}
}
resp
,
err
:=
http
.
Get
(
server
.
URL
+
"/thirdparty/company.com/"
+
version
+
"/namespaces/default/foos/test"
)
resp
,
err
:=
http
.
Get
(
server
.
URL
+
"/thirdparty/company.com/"
+
version
+
"/namespaces/default/foos/test"
)
if
err
!=
nil
{
if
!
assert
.
NoError
(
err
)
{
t
.
Errorf
(
"unexpected error: %v"
,
err
)
return
return
}
}
if
resp
.
StatusCode
!=
http
.
StatusOK
{
assert
.
Equal
(
http
.
StatusOK
,
resp
.
StatusCode
)
t
.
Errorf
(
"unexpected status: %v"
,
resp
)
}
item
:=
Foo
{}
item
:=
Foo
{}
if
err
:=
decodeResponse
(
resp
,
&
item
);
err
!=
nil
{
assert
.
NoError
(
decodeResponse
(
resp
,
&
item
))
t
.
Errorf
(
"unexpected error: %v"
,
err
)
if
!
assert
.
False
(
reflect
.
DeepEqual
(
item
,
expectedObj
))
{
t
.
Errorf
(
"expected objects to not be equal:
\n
%v
\n
saw:
\n
%v
\n
"
,
expectedObj
,
item
)
}
}
// Fill in data that the apiserver injects
// Fill in data that the apiserver injects
expectedObj
.
SelfLink
=
item
.
SelfLink
expectedObj
.
SelfLink
=
item
.
SelfLink
if
!
reflect
.
DeepEqual
(
item
,
expectedObj
)
{
if
!
assert
.
True
(
reflect
.
DeepEqual
(
item
,
expectedObj
)
)
{
t
.
Errorf
(
"expected:
\n
%#v
\n
saw:
\n
%#v
\n
"
,
expectedObj
,
item
)
t
.
Errorf
(
"expected:
\n
%#v
\n
saw:
\n
%#v
\n
"
,
expectedObj
,
item
)
}
}
}
}
...
@@ -251,7 +584,7 @@ func TestInstallThirdPartyAPIPost(t *testing.T) {
...
@@ -251,7 +584,7 @@ func TestInstallThirdPartyAPIPost(t *testing.T) {
}
}
func
testInstallThirdPartyAPIPostForVersion
(
t
*
testing
.
T
,
version
string
)
{
func
testInstallThirdPartyAPIPostForVersion
(
t
*
testing
.
T
,
version
string
)
{
fakeClient
,
server
:=
initThirdParty
(
t
,
version
)
fakeClient
,
server
,
assert
:=
initThirdParty
(
t
,
version
)
defer
server
.
Close
()
defer
server
.
Close
()
inputObj
:=
Foo
{
inputObj
:=
Foo
{
...
@@ -266,25 +599,19 @@ func testInstallThirdPartyAPIPostForVersion(t *testing.T, version string) {
...
@@ -266,25 +599,19 @@ func testInstallThirdPartyAPIPostForVersion(t *testing.T, version string) {
OtherField
:
10
,
OtherField
:
10
,
}
}
data
,
err
:=
json
.
Marshal
(
inputObj
)
data
,
err
:=
json
.
Marshal
(
inputObj
)
if
err
!=
nil
{
if
!
assert
.
NoError
(
err
)
{
t
.
Errorf
(
"unexpected error: %v"
)
return
return
}
}
resp
,
err
:=
http
.
Post
(
server
.
URL
+
"/thirdparty/company.com/"
+
version
+
"/namespaces/default/foos"
,
"application/json"
,
bytes
.
NewBuffer
(
data
))
resp
,
err
:=
http
.
Post
(
server
.
URL
+
"/thirdparty/company.com/"
+
version
+
"/namespaces/default/foos"
,
"application/json"
,
bytes
.
NewBuffer
(
data
))
if
err
!=
nil
{
if
!
assert
.
NoError
(
err
)
{
t
.
Errorf
(
"unexpected error: %v"
,
err
)
return
return
}
}
if
resp
.
StatusCode
!=
http
.
StatusCreated
{
assert
.
Equal
(
http
.
StatusCreated
,
resp
.
StatusCode
)
t
.
Errorf
(
"unexpected status: %v"
,
resp
)
}
item
:=
Foo
{}
item
:=
Foo
{}
if
err
:=
decodeResponse
(
resp
,
&
item
);
err
!=
nil
{
assert
.
NoError
(
decodeResponse
(
resp
,
&
item
))
t
.
Errorf
(
"unexpected error: %v"
,
err
)
}
// fill in fields set by the apiserver
// fill in fields set by the apiserver
expectedObj
:=
inputObj
expectedObj
:=
inputObj
...
@@ -292,29 +619,26 @@ func testInstallThirdPartyAPIPostForVersion(t *testing.T, version string) {
...
@@ -292,29 +619,26 @@ func testInstallThirdPartyAPIPostForVersion(t *testing.T, version string) {
expectedObj
.
Namespace
=
item
.
Namespace
expectedObj
.
Namespace
=
item
.
Namespace
expectedObj
.
UID
=
item
.
UID
expectedObj
.
UID
=
item
.
UID
expectedObj
.
CreationTimestamp
=
item
.
CreationTimestamp
expectedObj
.
CreationTimestamp
=
item
.
CreationTimestamp
if
!
reflect
.
DeepEqual
(
item
,
expectedObj
)
{
if
!
assert
.
True
(
reflect
.
DeepEqual
(
item
,
expectedObj
)
)
{
t
.
Errorf
(
"expected:
\n
%v
\n
saw:
\n
%v
\n
"
,
expectedObj
,
item
)
t
.
Errorf
(
"expected:
\n
%v
\n
saw:
\n
%v
\n
"
,
expectedObj
,
item
)
}
}
etcdResp
,
err
:=
fakeClient
.
Get
(
etcdtest
.
PathPrefix
()
+
"/ThirdPartyResourceData/company.com/foos/default/test"
,
false
,
false
)
etcdResp
,
err
:=
fakeClient
.
Get
(
etcdtest
.
PathPrefix
()
+
"/ThirdPartyResourceData/company.com/foos/default/test"
,
false
,
false
)
if
err
!=
nil
{
if
!
assert
.
NoError
(
err
)
{
t
.
Errorf
(
"unexpected error: %v"
,
err
)
t
.
FailNow
()
t
.
FailNow
()
}
}
obj
,
err
:=
explatest
.
Codec
.
Decode
([]
byte
(
etcdResp
.
Node
.
Value
))
obj
,
err
:=
explatest
.
Codec
.
Decode
([]
byte
(
etcdResp
.
Node
.
Value
))
if
err
!=
nil
{
assert
.
NoError
(
err
)
t
.
Errorf
(
"unexpected error: %v"
,
err
)
}
thirdPartyObj
,
ok
:=
obj
.
(
*
expapi
.
ThirdPartyResourceData
)
thirdPartyObj
,
ok
:=
obj
.
(
*
expapi
.
ThirdPartyResourceData
)
if
!
ok
{
if
!
assert
.
True
(
ok
)
{
t
.
Errorf
(
"unexpected object: %v"
,
obj
)
t
.
Errorf
(
"unexpected object: %v"
,
obj
)
}
}
item
=
Foo
{}
item
=
Foo
{}
if
err
:=
json
.
Unmarshal
(
thirdPartyObj
.
Data
,
&
item
);
err
!=
nil
{
assert
.
NoError
(
json
.
Unmarshal
(
thirdPartyObj
.
Data
,
&
item
))
t
.
Errorf
(
"unexpected error: %v"
,
err
)
}
if
!
reflect
.
DeepEqual
(
item
,
inputObj
)
{
if
!
assert
.
True
(
reflect
.
DeepEqual
(
item
,
inputObj
)
)
{
t
.
Errorf
(
"expected:
\n
%v
\n
saw:
\n
%v
\n
"
,
inputObj
,
item
)
t
.
Errorf
(
"expected:
\n
%v
\n
saw:
\n
%v
\n
"
,
inputObj
,
item
)
}
}
}
}
...
@@ -326,7 +650,7 @@ func TestInstallThirdPartyAPIDelete(t *testing.T) {
...
@@ -326,7 +650,7 @@ func TestInstallThirdPartyAPIDelete(t *testing.T) {
}
}
func
testInstallThirdPartyAPIDeleteVersion
(
t
*
testing
.
T
,
version
string
)
{
func
testInstallThirdPartyAPIDeleteVersion
(
t
*
testing
.
T
,
version
string
)
{
fakeClient
,
server
:=
initThirdParty
(
t
,
version
)
fakeClient
,
server
,
assert
:=
initThirdParty
(
t
,
version
)
defer
server
.
Close
()
defer
server
.
Close
()
expectedObj
:=
Foo
{
expectedObj
:=
Foo
{
...
@@ -340,55 +664,45 @@ func testInstallThirdPartyAPIDeleteVersion(t *testing.T, version string) {
...
@@ -340,55 +664,45 @@ func testInstallThirdPartyAPIDeleteVersion(t *testing.T, version string) {
SomeField
:
"test field"
,
SomeField
:
"test field"
,
OtherField
:
10
,
OtherField
:
10
,
}
}
if
err
:=
storeToEtcd
(
fakeClient
,
"/ThirdPartyResourceData/company.com/foos/default/test"
,
"test"
,
expectedObj
);
err
!=
nil
{
if
!
assert
.
NoError
(
storeToEtcd
(
fakeClient
,
"/ThirdPartyResourceData/company.com/foos/default/test"
,
"test"
,
expectedObj
))
{
t
.
Errorf
(
"unexpected error: %v"
,
err
)
t
.
FailNow
()
t
.
FailNow
()
return
return
}
}
resp
,
err
:=
http
.
Get
(
server
.
URL
+
"/thirdparty/company.com/"
+
version
+
"/namespaces/default/foos/test"
)
resp
,
err
:=
http
.
Get
(
server
.
URL
+
"/thirdparty/company.com/"
+
version
+
"/namespaces/default/foos/test"
)
if
err
!=
nil
{
if
!
assert
.
NoError
(
err
)
{
t
.
Errorf
(
"unexpected error: %v"
,
err
)
return
return
}
}
if
resp
.
StatusCode
!=
http
.
StatusOK
{
assert
.
Equal
(
http
.
StatusOK
,
resp
.
StatusCode
)
t
.
Errorf
(
"unexpected status: %v"
,
resp
)
}
item
:=
Foo
{}
item
:=
Foo
{}
if
err
:=
decodeResponse
(
resp
,
&
item
);
err
!=
nil
{
assert
.
NoError
(
decodeResponse
(
resp
,
&
item
))
t
.
Errorf
(
"unexpected error: %v"
,
err
)
}
// Fill in fields set by the apiserver
// Fill in fields set by the apiserver
expectedObj
.
SelfLink
=
item
.
SelfLink
expectedObj
.
SelfLink
=
item
.
SelfLink
expectedObj
.
Namespace
=
item
.
Namespace
expectedObj
.
Namespace
=
item
.
Namespace
if
!
reflect
.
DeepEqual
(
item
,
expectedObj
)
{
if
!
assert
.
True
(
reflect
.
DeepEqual
(
item
,
expectedObj
)
)
{
t
.
Errorf
(
"expected:
\n
%v
\n
saw:
\n
%v
\n
"
,
expectedObj
,
item
)
t
.
Errorf
(
"expected:
\n
%v
\n
saw:
\n
%v
\n
"
,
expectedObj
,
item
)
}
}
resp
,
err
=
httpDelete
(
server
.
URL
+
"/thirdparty/company.com/"
+
version
+
"/namespaces/default/foos/test"
)
resp
,
err
=
httpDelete
(
server
.
URL
+
"/thirdparty/company.com/"
+
version
+
"/namespaces/default/foos/test"
)
if
err
!=
nil
{
if
!
assert
.
NoError
(
err
)
{
t
.
Errorf
(
"unexpected error: %v"
,
err
)
return
return
}
}
if
resp
.
StatusCode
!=
http
.
StatusOK
{
assert
.
Equal
(
http
.
StatusOK
,
resp
.
StatusCode
)
t
.
Errorf
(
"unexpected status: %v"
,
resp
)
}
resp
,
err
=
http
.
Get
(
server
.
URL
+
"/thirdparty/company.com/"
+
version
+
"/namespaces/default/foos/test"
)
resp
,
err
=
http
.
Get
(
server
.
URL
+
"/thirdparty/company.com/"
+
version
+
"/namespaces/default/foos/test"
)
if
err
!=
nil
{
if
!
assert
.
NoError
(
err
)
{
t
.
Errorf
(
"unexpected error: %v"
,
err
)
return
return
}
}
if
resp
.
StatusCode
!=
http
.
StatusNotFound
{
assert
.
Equal
(
http
.
StatusNotFound
,
resp
.
StatusCode
)
t
.
Errorf
(
"unexpected status: %v"
,
resp
)
}
expectDeletedKeys
:=
[]
string
{
etcdtest
.
PathPrefix
()
+
"/ThirdPartyResourceData/company.com/foos/default/test"
}
expectDeletedKeys
:=
[]
string
{
etcdtest
.
PathPrefix
()
+
"/ThirdPartyResourceData/company.com/foos/default/test"
}
if
!
reflect
.
DeepEqual
(
fakeClient
.
DeletedKeys
,
expectDeletedKeys
)
{
if
!
assert
.
True
(
reflect
.
DeepEqual
(
fakeClient
.
DeletedKeys
,
expectDeletedKeys
))
{
t
.
Errorf
(
"unexpected deleted keys: %v"
,
fakeClient
.
DeletedKeys
)
t
.
Errorf
(
"unexpected deleted keys: %v"
,
fakeClient
.
DeletedKeys
)
}
}
}
}
...
...
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