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
f40a5d11
Unverified
Commit
f40a5d11
authored
Nov 06, 2018
by
k8s-ci-robot
Committed by
GitHub
Nov 06, 2018
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #67435 from feiskyer/dns-cap
Add dns capabilities for Windows CNI plugins
parents
cfd53d6d
32cf0a90
Hide whitespace changes
Inline
Side-by-side
Showing
12 changed files
with
78 additions
and
23 deletions
+78
-23
docker_sandbox.go
pkg/kubelet/dockershim/docker_sandbox.go
+11
-2
docker_service_test.go
pkg/kubelet/dockershim/docker_service_test.go
+0
-1
BUILD
pkg/kubelet/dockershim/network/cni/BUILD
+1
-0
cni.go
pkg/kubelet/dockershim/network/cni/cni.go
+33
-7
cni_others.go
pkg/kubelet/dockershim/network/cni/cni_others.go
+6
-0
cni_test.go
pkg/kubelet/dockershim/network/cni/cni_test.go
+1
-1
cni_windows.go
pkg/kubelet/dockershim/network/cni/cni_windows.go
+15
-1
kubenet_linux.go
pkg/kubelet/dockershim/network/kubenet/kubenet_linux.go
+1
-1
kubenet_unsupported.go
...kubelet/dockershim/network/kubenet/kubenet_unsupported.go
+1
-1
plugins.go
pkg/kubelet/dockershim/network/plugins.go
+4
-4
mock_network_plugin.go
...kubelet/dockershim/network/testing/mock_network_plugin.go
+1
-1
plugins_test.go
pkg/kubelet/dockershim/network/testing/plugins_test.go
+4
-4
No files found.
pkg/kubelet/dockershim/docker_sandbox.go
View file @
f40a5d11
...
...
@@ -18,6 +18,7 @@ package dockershim
import
(
"context"
"encoding/json"
"fmt"
"os"
"strings"
...
...
@@ -27,7 +28,6 @@ import (
dockercontainer
"github.com/docker/docker/api/types/container"
dockerfilters
"github.com/docker/docker/api/types/filters"
"github.com/golang/glog"
utilerrors
"k8s.io/apimachinery/pkg/util/errors"
runtimeapi
"k8s.io/kubernetes/pkg/kubelet/apis/cri/runtime/v1alpha2"
"k8s.io/kubernetes/pkg/kubelet/checkpointmanager"
...
...
@@ -165,7 +165,16 @@ func (ds *dockerService) RunPodSandbox(ctx context.Context, r *runtimeapi.RunPod
// on the host as well, to satisfy parts of the pod spec that aren't
// recognized by the CNI standard yet.
cID
:=
kubecontainer
.
BuildContainerID
(
runtimeName
,
createResp
.
ID
)
err
=
ds
.
network
.
SetUpPod
(
config
.
GetMetadata
()
.
Namespace
,
config
.
GetMetadata
()
.
Name
,
cID
,
config
.
Annotations
)
networkOptions
:=
make
(
map
[
string
]
string
)
if
dnsConfig
:=
config
.
GetDnsConfig
();
dnsConfig
!=
nil
{
// Build DNS options.
dnsOption
,
err
:=
json
.
Marshal
(
dnsConfig
)
if
err
!=
nil
{
return
nil
,
fmt
.
Errorf
(
"failed to marshal dns config for pod %q: %v"
,
config
.
Metadata
.
Name
,
err
)
}
networkOptions
[
"dns"
]
=
string
(
dnsOption
)
}
err
=
ds
.
network
.
SetUpPod
(
config
.
GetMetadata
()
.
Namespace
,
config
.
GetMetadata
()
.
Name
,
cID
,
config
.
Annotations
,
networkOptions
)
if
err
!=
nil
{
errList
:=
[]
error
{
fmt
.
Errorf
(
"failed to set up sandbox container %q network for pod %q: %v"
,
createResp
.
ID
,
config
.
Metadata
.
Name
,
err
)}
...
...
pkg/kubelet/dockershim/docker_service_test.go
View file @
f40a5d11
...
...
@@ -27,7 +27,6 @@ import (
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/util/clock"
runtimeapi
"k8s.io/kubernetes/pkg/kubelet/apis/cri/runtime/v1alpha2"
"k8s.io/kubernetes/pkg/kubelet/checkpointmanager"
...
...
pkg/kubelet/dockershim/network/cni/BUILD
View file @
f40a5d11
...
...
@@ -16,6 +16,7 @@ go_library(
importpath = "k8s.io/kubernetes/pkg/kubelet/dockershim/network/cni",
deps = [
"//pkg/kubelet/apis/config:go_default_library",
"//pkg/kubelet/apis/cri/runtime/v1alpha2:go_default_library",
"//pkg/kubelet/container:go_default_library",
"//pkg/kubelet/dockershim/network:go_default_library",
"//pkg/util/bandwidth:go_default_library",
...
...
pkg/kubelet/dockershim/network/cni/cni.go
View file @
f40a5d11
...
...
@@ -17,6 +17,7 @@ limitations under the License.
package
cni
import
(
"encoding/json"
"errors"
"fmt"
"math"
...
...
@@ -28,6 +29,7 @@ import (
cnitypes
"github.com/containernetworking/cni/pkg/types"
"github.com/golang/glog"
kubeletconfig
"k8s.io/kubernetes/pkg/kubelet/apis/config"
runtimeapi
"k8s.io/kubernetes/pkg/kubelet/apis/cri/runtime/v1alpha2"
kubecontainer
"k8s.io/kubernetes/pkg/kubelet/container"
"k8s.io/kubernetes/pkg/kubelet/dockershim/network"
"k8s.io/kubernetes/pkg/util/bandwidth"
...
...
@@ -90,6 +92,18 @@ type cniIpRange struct {
Subnet
string
`json:"subnet"`
}
// cniDNSConfig maps to the windows CNI dns Capability.
// see: https://github.com/containernetworking/cni/blob/master/CONVENTIONS.md
// Note that dns capability is only used for Windows containers.
type
cniDNSConfig
struct
{
// List of DNS servers of the cluster.
Servers
[]
string
`json:"servers,omitempty"`
// List of DNS search domains of the cluster.
Searches
[]
string
`json:"searches,omitempty"`
// List of DNS options.
Options
[]
string
`json:"options,omitempty"`
}
func
SplitDirs
(
dirs
string
)
[]
string
{
// Use comma rather than colon to work better with Windows too
return
strings
.
Split
(
dirs
,
","
)
...
...
@@ -257,7 +271,7 @@ func (plugin *cniNetworkPlugin) Status() error {
return
plugin
.
checkInitialized
()
}
func
(
plugin
*
cniNetworkPlugin
)
SetUpPod
(
namespace
string
,
name
string
,
id
kubecontainer
.
ContainerID
,
annotations
map
[
string
]
string
)
error
{
func
(
plugin
*
cniNetworkPlugin
)
SetUpPod
(
namespace
string
,
name
string
,
id
kubecontainer
.
ContainerID
,
annotations
,
options
map
[
string
]
string
)
error
{
if
err
:=
plugin
.
checkInitialized
();
err
!=
nil
{
return
err
}
...
...
@@ -268,12 +282,12 @@ func (plugin *cniNetworkPlugin) SetUpPod(namespace string, name string, id kubec
// Windows doesn't have loNetwork. It comes only with Linux
if
plugin
.
loNetwork
!=
nil
{
if
_
,
err
=
plugin
.
addToNetwork
(
plugin
.
loNetwork
,
name
,
namespace
,
id
,
netnsPath
,
annotations
);
err
!=
nil
{
if
_
,
err
=
plugin
.
addToNetwork
(
plugin
.
loNetwork
,
name
,
namespace
,
id
,
netnsPath
,
annotations
,
options
);
err
!=
nil
{
return
err
}
}
_
,
err
=
plugin
.
addToNetwork
(
plugin
.
getDefaultNetwork
(),
name
,
namespace
,
id
,
netnsPath
,
annotations
)
_
,
err
=
plugin
.
addToNetwork
(
plugin
.
getDefaultNetwork
(),
name
,
namespace
,
id
,
netnsPath
,
annotations
,
options
)
return
err
}
...
...
@@ -295,8 +309,8 @@ func podDesc(namespace, name string, id kubecontainer.ContainerID) string {
return
fmt
.
Sprintf
(
"%s_%s/%s"
,
namespace
,
name
,
id
.
ID
)
}
func
(
plugin
*
cniNetworkPlugin
)
addToNetwork
(
network
*
cniNetwork
,
podName
string
,
podNamespace
string
,
podSandboxID
kubecontainer
.
ContainerID
,
podNetnsPath
string
,
annotations
map
[
string
]
string
)
(
cnitypes
.
Result
,
error
)
{
rt
,
err
:=
plugin
.
buildCNIRuntimeConf
(
podName
,
podNamespace
,
podSandboxID
,
podNetnsPath
,
annotations
)
func
(
plugin
*
cniNetworkPlugin
)
addToNetwork
(
network
*
cniNetwork
,
podName
string
,
podNamespace
string
,
podSandboxID
kubecontainer
.
ContainerID
,
podNetnsPath
string
,
annotations
,
options
map
[
string
]
string
)
(
cnitypes
.
Result
,
error
)
{
rt
,
err
:=
plugin
.
buildCNIRuntimeConf
(
podName
,
podNamespace
,
podSandboxID
,
podNetnsPath
,
annotations
,
options
)
if
err
!=
nil
{
glog
.
Errorf
(
"Error adding network when building cni runtime conf: %v"
,
err
)
return
nil
,
err
...
...
@@ -315,7 +329,7 @@ func (plugin *cniNetworkPlugin) addToNetwork(network *cniNetwork, podName string
}
func
(
plugin
*
cniNetworkPlugin
)
deleteFromNetwork
(
network
*
cniNetwork
,
podName
string
,
podNamespace
string
,
podSandboxID
kubecontainer
.
ContainerID
,
podNetnsPath
string
,
annotations
map
[
string
]
string
)
error
{
rt
,
err
:=
plugin
.
buildCNIRuntimeConf
(
podName
,
podNamespace
,
podSandboxID
,
podNetnsPath
,
annotations
)
rt
,
err
:=
plugin
.
buildCNIRuntimeConf
(
podName
,
podNamespace
,
podSandboxID
,
podNetnsPath
,
annotations
,
nil
)
if
err
!=
nil
{
glog
.
Errorf
(
"Error deleting network when building cni runtime conf: %v"
,
err
)
return
err
...
...
@@ -335,7 +349,7 @@ func (plugin *cniNetworkPlugin) deleteFromNetwork(network *cniNetwork, podName s
return
nil
}
func
(
plugin
*
cniNetworkPlugin
)
buildCNIRuntimeConf
(
podName
string
,
podNs
string
,
podSandboxID
kubecontainer
.
ContainerID
,
podNetnsPath
string
,
annotations
map
[
string
]
string
)
(
*
libcni
.
RuntimeConf
,
error
)
{
func
(
plugin
*
cniNetworkPlugin
)
buildCNIRuntimeConf
(
podName
string
,
podNs
string
,
podSandboxID
kubecontainer
.
ContainerID
,
podNetnsPath
string
,
annotations
,
options
map
[
string
]
string
)
(
*
libcni
.
RuntimeConf
,
error
)
{
rt
:=
&
libcni
.
RuntimeConf
{
ContainerID
:
podSandboxID
.
ID
,
NetNS
:
podNetnsPath
,
...
...
@@ -390,5 +404,17 @@ func (plugin *cniNetworkPlugin) buildCNIRuntimeConf(podName string, podNs string
// Set the PodCIDR
rt
.
CapabilityArgs
[
"ipRanges"
]
=
[][]
cniIpRange
{{{
Subnet
:
plugin
.
podCidr
}}}
// Set dns capability args.
if
dnsOptions
,
ok
:=
options
[
"dns"
];
ok
{
dnsConfig
:=
runtimeapi
.
DNSConfig
{}
err
:=
json
.
Unmarshal
([]
byte
(
dnsOptions
),
&
dnsConfig
)
if
err
!=
nil
{
return
nil
,
fmt
.
Errorf
(
"failed to unmarshal dns config %q: %v"
,
dnsOptions
,
err
)
}
if
dnsParam
:=
buildDNSCapabilities
(
&
dnsConfig
);
dnsParam
!=
nil
{
rt
.
CapabilityArgs
[
"dns"
]
=
*
dnsParam
}
}
return
rt
,
nil
}
pkg/kubelet/dockershim/network/cni/cni_others.go
View file @
f40a5d11
...
...
@@ -22,6 +22,7 @@ import (
"fmt"
"github.com/containernetworking/cni/libcni"
runtimeapi
"k8s.io/kubernetes/pkg/kubelet/apis/cri/runtime/v1alpha2"
kubecontainer
"k8s.io/kubernetes/pkg/kubelet/container"
"k8s.io/kubernetes/pkg/kubelet/dockershim/network"
)
...
...
@@ -75,3 +76,8 @@ func (plugin *cniNetworkPlugin) GetPodNetworkStatus(namespace string, name strin
return
&
network
.
PodNetworkStatus
{
IP
:
ip
},
nil
}
// buildDNSCapabilities builds cniDNSConfig from runtimeapi.DNSConfig.
func
buildDNSCapabilities
(
dnsConfig
*
runtimeapi
.
DNSConfig
)
*
cniDNSConfig
{
return
nil
}
pkg/kubelet/dockershim/network/cni/cni_test.go
View file @
f40a5d11
...
...
@@ -254,7 +254,7 @@ func TestCNIPlugin(t *testing.T) {
bandwidthAnnotation
[
"kubernetes.io/egress-bandwidth"
]
=
"1M"
// Set up the pod
err
=
plug
.
SetUpPod
(
"podNamespace"
,
"podName"
,
containerID
,
bandwidthAnnotation
)
err
=
plug
.
SetUpPod
(
"podNamespace"
,
"podName"
,
containerID
,
bandwidthAnnotation
,
nil
)
if
err
!=
nil
{
t
.
Errorf
(
"Expected nil: %v"
,
err
)
}
...
...
pkg/kubelet/dockershim/network/cni/cni_windows.go
View file @
f40a5d11
...
...
@@ -23,6 +23,7 @@ import (
cniTypes020
"github.com/containernetworking/cni/pkg/types/020"
"github.com/golang/glog"
runtimeapi
"k8s.io/kubernetes/pkg/kubelet/apis/cri/runtime/v1alpha2"
kubecontainer
"k8s.io/kubernetes/pkg/kubelet/container"
"k8s.io/kubernetes/pkg/kubelet/dockershim/network"
)
...
...
@@ -42,7 +43,7 @@ func (plugin *cniNetworkPlugin) GetPodNetworkStatus(namespace string, name strin
return
nil
,
fmt
.
Errorf
(
"CNI failed to retrieve network namespace path: %v"
,
err
)
}
result
,
err
:=
plugin
.
addToNetwork
(
plugin
.
getDefaultNetwork
(),
name
,
namespace
,
id
,
netnsPath
,
nil
)
result
,
err
:=
plugin
.
addToNetwork
(
plugin
.
getDefaultNetwork
(),
name
,
namespace
,
id
,
netnsPath
,
nil
,
nil
)
glog
.
V
(
5
)
.
Infof
(
"GetPodNetworkStatus result %+v"
,
result
)
if
err
!=
nil
{
...
...
@@ -59,3 +60,16 @@ func (plugin *cniNetworkPlugin) GetPodNetworkStatus(namespace string, name strin
}
return
&
network
.
PodNetworkStatus
{
IP
:
result020
.
IP4
.
IP
.
IP
},
nil
}
// buildDNSCapabilities builds cniDNSConfig from runtimeapi.DNSConfig.
func
buildDNSCapabilities
(
dnsConfig
*
runtimeapi
.
DNSConfig
)
*
cniDNSConfig
{
if
dnsConfig
!=
nil
{
return
&
cniDNSConfig
{
Servers
:
dnsConfig
.
Servers
,
Searches
:
dnsConfig
.
Searches
,
Options
:
dnsConfig
.
Options
,
}
}
return
nil
}
pkg/kubelet/dockershim/network/kubenet/kubenet_linux.go
View file @
f40a5d11
...
...
@@ -379,7 +379,7 @@ func (plugin *kubenetNetworkPlugin) setup(namespace string, name string, id kube
return
nil
}
func
(
plugin
*
kubenetNetworkPlugin
)
SetUpPod
(
namespace
string
,
name
string
,
id
kubecontainer
.
ContainerID
,
annotations
map
[
string
]
string
)
error
{
func
(
plugin
*
kubenetNetworkPlugin
)
SetUpPod
(
namespace
string
,
name
string
,
id
kubecontainer
.
ContainerID
,
annotations
,
options
map
[
string
]
string
)
error
{
plugin
.
mu
.
Lock
()
defer
plugin
.
mu
.
Unlock
()
...
...
pkg/kubelet/dockershim/network/kubenet/kubenet_unsupported.go
View file @
f40a5d11
...
...
@@ -42,7 +42,7 @@ func (plugin *kubenetNetworkPlugin) Name() string {
return
"kubenet"
}
func
(
plugin
*
kubenetNetworkPlugin
)
SetUpPod
(
namespace
string
,
name
string
,
id
kubecontainer
.
ContainerID
,
annotations
map
[
string
]
string
)
error
{
func
(
plugin
*
kubenetNetworkPlugin
)
SetUpPod
(
namespace
string
,
name
string
,
id
kubecontainer
.
ContainerID
,
annotations
,
options
map
[
string
]
string
)
error
{
return
fmt
.
Errorf
(
"Kubenet is not supported in this build"
)
}
...
...
pkg/kubelet/dockershim/network/plugins.go
View file @
f40a5d11
...
...
@@ -63,7 +63,7 @@ type NetworkPlugin interface {
// SetUpPod is the method called after the infra container of
// the pod has been created but before the other containers of the
// pod are launched.
SetUpPod
(
namespace
string
,
name
string
,
podSandboxID
kubecontainer
.
ContainerID
,
annotations
map
[
string
]
string
)
error
SetUpPod
(
namespace
string
,
name
string
,
podSandboxID
kubecontainer
.
ContainerID
,
annotations
,
options
map
[
string
]
string
)
error
// TearDownPod is the method called before a pod's infra container will be deleted
TearDownPod
(
namespace
string
,
name
string
,
podSandboxID
kubecontainer
.
ContainerID
)
error
...
...
@@ -207,7 +207,7 @@ func (plugin *NoopNetworkPlugin) Capabilities() utilsets.Int {
return
utilsets
.
NewInt
()
}
func
(
plugin
*
NoopNetworkPlugin
)
SetUpPod
(
namespace
string
,
name
string
,
id
kubecontainer
.
ContainerID
,
annotations
map
[
string
]
string
)
error
{
func
(
plugin
*
NoopNetworkPlugin
)
SetUpPod
(
namespace
string
,
name
string
,
id
kubecontainer
.
ContainerID
,
annotations
,
options
map
[
string
]
string
)
error
{
return
nil
}
...
...
@@ -368,14 +368,14 @@ func (pm *PluginManager) GetPodNetworkStatus(podNamespace, podName string, id ku
return
netStatus
,
nil
}
func
(
pm
*
PluginManager
)
SetUpPod
(
podNamespace
,
podName
string
,
id
kubecontainer
.
ContainerID
,
annotations
map
[
string
]
string
)
error
{
func
(
pm
*
PluginManager
)
SetUpPod
(
podNamespace
,
podName
string
,
id
kubecontainer
.
ContainerID
,
annotations
,
options
map
[
string
]
string
)
error
{
defer
recordOperation
(
"set_up_pod"
,
time
.
Now
())
fullPodName
:=
kubecontainer
.
BuildPodFullName
(
podName
,
podNamespace
)
pm
.
podLock
(
fullPodName
)
.
Lock
()
defer
pm
.
podUnlock
(
fullPodName
)
glog
.
V
(
3
)
.
Infof
(
"Calling network plugin %s to set up pod %q"
,
pm
.
plugin
.
Name
(),
fullPodName
)
if
err
:=
pm
.
plugin
.
SetUpPod
(
podNamespace
,
podName
,
id
,
annotations
);
err
!=
nil
{
if
err
:=
pm
.
plugin
.
SetUpPod
(
podNamespace
,
podName
,
id
,
annotations
,
options
);
err
!=
nil
{
return
fmt
.
Errorf
(
"NetworkPlugin %s failed to set up pod %q network: %v"
,
pm
.
plugin
.
Name
(),
fullPodName
,
err
)
}
...
...
pkg/kubelet/dockershim/network/testing/mock_network_plugin.go
View file @
f40a5d11
...
...
@@ -102,7 +102,7 @@ func (_mr *_MockNetworkPluginRecorder) Name() *gomock.Call {
return
_mr
.
mock
.
ctrl
.
RecordCall
(
_mr
.
mock
,
"Name"
)
}
func
(
_m
*
MockNetworkPlugin
)
SetUpPod
(
_param0
string
,
_param1
string
,
_param2
container
.
ContainerID
,
annotations
map
[
string
]
string
)
error
{
func
(
_m
*
MockNetworkPlugin
)
SetUpPod
(
_param0
string
,
_param1
string
,
_param2
container
.
ContainerID
,
annotations
,
options
map
[
string
]
string
)
error
{
ret
:=
_m
.
ctrl
.
Call
(
_m
,
"SetUpPod"
,
_param0
,
_param1
,
_param2
)
ret0
,
_
:=
ret
[
0
]
.
(
error
)
return
ret0
...
...
pkg/kubelet/dockershim/network/testing/plugins_test.go
View file @
f40a5d11
...
...
@@ -108,7 +108,7 @@ func TestPluginManager(t *testing.T) {
// concurrently.
allCreatedWg
.
Wait
()
if
err
:=
pm
.
SetUpPod
(
""
,
name
,
id
,
nil
);
err
!=
nil
{
if
err
:=
pm
.
SetUpPod
(
""
,
name
,
id
,
nil
,
nil
);
err
!=
nil
{
t
.
Errorf
(
"Failed to set up pod %q: %v"
,
name
,
err
)
return
}
...
...
@@ -159,7 +159,7 @@ func (p *hookableFakeNetworkPlugin) Capabilities() utilsets.Int {
return
utilsets
.
NewInt
()
}
func
(
p
*
hookableFakeNetworkPlugin
)
SetUpPod
(
namespace
string
,
name
string
,
id
kubecontainer
.
ContainerID
,
annotations
map
[
string
]
string
)
error
{
func
(
p
*
hookableFakeNetworkPlugin
)
SetUpPod
(
namespace
string
,
name
string
,
id
kubecontainer
.
ContainerID
,
annotations
,
options
map
[
string
]
string
)
error
{
if
p
.
setupHook
!=
nil
{
p
.
setupHook
(
namespace
,
name
,
id
)
}
...
...
@@ -210,7 +210,7 @@ func TestMultiPodParallelNetworkOps(t *testing.T) {
// Setup will block on the runner pod completing. If network
// operations locking isn't correct (eg pod network operations
// block other pods) setUpPod() will never return.
if
err
:=
pm
.
SetUpPod
(
""
,
podName
,
containerID
,
nil
);
err
!=
nil
{
if
err
:=
pm
.
SetUpPod
(
""
,
podName
,
containerID
,
nil
,
nil
);
err
!=
nil
{
t
.
Errorf
(
"Failed to set up waiter pod: %v"
,
err
)
return
}
...
...
@@ -230,7 +230,7 @@ func TestMultiPodParallelNetworkOps(t *testing.T) {
podName
:=
"runner"
containerID
:=
kubecontainer
.
ContainerID
{
ID
:
podName
}
if
err
:=
pm
.
SetUpPod
(
""
,
podName
,
containerID
,
nil
);
err
!=
nil
{
if
err
:=
pm
.
SetUpPod
(
""
,
podName
,
containerID
,
nil
,
nil
);
err
!=
nil
{
t
.
Errorf
(
"Failed to set up runner pod: %v"
,
err
)
return
}
...
...
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