Skip to content

Commit

Permalink
Fixing UTs to match the new behaviour.
Browse files Browse the repository at this point in the history
Added Update counter checks to the cnidel / setup TCs.
These extra assertation now cover the cases related to the IP leaking bug.
  • Loading branch information
Levovar committed Aug 8, 2019
1 parent 6fe4f85 commit 37dd874
Show file tree
Hide file tree
Showing 4 changed files with 57 additions and 42 deletions.
1 change: 1 addition & 0 deletions integration/cni_config/00-danm.conf
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"cniVersion": "0.3.1",
"name": "meta_cni",
"name_comment": "Mandatory parameter, but can be anything",
"type": "danm",
Expand Down
3 changes: 2 additions & 1 deletion pkg/cnidel/cnidel.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ func DelegateInterfaceSetup(netConf *datastructs.NetConf, danmClient danmclients
}
//As netInfo is only copied to IPAM above, the IP allocation is not refreshed in the original copy.
//Without re-reading the network body we risk leaking IPs if error happens later on within the same thread!
netInfo,_ = netcontrol.GetNetworkFromEp(danmClient, *ep)
netInfo,err = netcontrol.GetNetworkFromEp(danmClient, *ep)
if err != nil {log.Println("lofasz:" + err.Error())}
ipamOptions = getCniIpamConfig(netInfo, ep.Spec.Iface.Address, ep.Spec.Iface.AddressIPv6)
}
rawConfig, err := getCniPluginConfig(netConf, netInfo, ipamOptions, ep)
Expand Down
15 changes: 9 additions & 6 deletions test/stubs/danm/netclient_stub.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,17 +57,20 @@ func (netClient *NetClientStub) Update(obj *danmtypes.DanmNet) (*danmtypes.DanmN
}
}
}
if strings.Contains(obj.Spec.NetworkID, "conflict") && obj.ObjectMeta.ResourceVersion != magicVersion {
for index, net := range netClient.TestNets {
if net.Spec.NetworkID == obj.Spec.NetworkID {
netClient.TestNets[index].ObjectMeta.ResourceVersion = magicVersion
}
var netIndex int
for index, net := range netClient.TestNets {
if net.ObjectMeta.Name == obj.ObjectMeta.Name {
netIndex = index
}
}
if strings.Contains(obj.Spec.NetworkID, "conflict") && obj.ObjectMeta.ResourceVersion != magicVersion {
netClient.TestNets[netIndex].ObjectMeta.ResourceVersion = magicVersion
return nil, errors.New(datastructs.OptimisticLockErrorMsg)
}
if strings.Contains(obj.Spec.NetworkID, "error") {
return nil, errors.New("fatal error, don't retry")
}
netClient.TestNets[netIndex] = *obj
return obj, nil
}

Expand All @@ -84,7 +87,7 @@ func (netClient *NetClientStub) Get(netName string, options meta_v1.GetOptions)
return nil, errors.New("fatal error, don't retry")
}
for _, testNet := range netClient.TestNets {
if testNet.Spec.NetworkID == netName {
if testNet.ObjectMeta.Name == netName {
return &testNet, nil
}
}
Expand Down
80 changes: 45 additions & 35 deletions test/uts/cnidel_test/cnidel_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,15 +76,15 @@ var testNets = []danmtypes.DanmNet {
},
danmtypes.DanmNet {
ObjectMeta: meta_v1.ObjectMeta {Name: "macvlan-v4"},
Spec: danmtypes.DanmNetSpec{NetworkType: "macvlan", NetworkID: "macvlan", Options: danmtypes.DanmNetOption{Cidr: "192.168.1.64/26", Device: "ens1f0"}},
Spec: danmtypes.DanmNetSpec{NetworkType: "macvlan", NetworkID: "macvlan-v4", Options: danmtypes.DanmNetOption{Cidr: "192.168.1.64/26", Device: "ens1f0"}},
},
danmtypes.DanmNet {
ObjectMeta: meta_v1.ObjectMeta {Name: "macvlan-v6"},
Spec: danmtypes.DanmNetSpec{NetworkType: "macvlan", NetworkID: "macvlan", Options: danmtypes.DanmNetOption{Net6: "2a00:8a00:a000:1193::/64", Device: "ens1f1"}},
Spec: danmtypes.DanmNetSpec{NetworkType: "macvlan", NetworkID: "macvlan-v6", Options: danmtypes.DanmNetOption{Net6: "2a00:8a00:a000:1193::/64", Device: "ens1f1"}},
},
danmtypes.DanmNet {
ObjectMeta: meta_v1.ObjectMeta {Name: "macvlan-ds"},
Spec: danmtypes.DanmNetSpec{NetworkType: "macvlan", NetworkID: "macvlan", Options: danmtypes.DanmNetOption{Cidr: "192.168.1.64/26", Net6: "2a00:8a00:a000:1193::/64", Device: "ens1f1"}},
Spec: danmtypes.DanmNetSpec{NetworkType: "macvlan", NetworkID: "macvlan-ds", Options: danmtypes.DanmNetOption{Cidr: "192.168.1.64/26", Net6: "2a00:8a00:a000:1193::/64", Device: "ens1f1"}},
},
danmtypes.DanmNet {
ObjectMeta: meta_v1.ObjectMeta {Name: "sriov-test"},
Expand Down Expand Up @@ -131,22 +131,23 @@ var testNets = []danmtypes.DanmNet {
var expectedCniConfigs = []CniConf {
{"flannel", []byte(`{"cniexp":{"cnitype":"flannel"},"cniconf":{"name":"cbr0","type":"flannel","delegate":{"hairpinMode":true,"isDefaultGateway":true}}}`)},
{"flannel-ip", []byte(`{"cniexp":{"cnitype":"flannel","ip":"10.244.10.30/24","env":{"CNI_COMMAND":"ADD","CNI_IFNAME":"eth0"}},"cniconf":{"name":"cbr0","type":"flannel","delegate":{"hairpinMode":true,"isDefaultGateway":true}}}`)},
{"macvlan-ip4", []byte(`{"cniexp":{"cnitype":"macvlan","ip":"192.168.1.65/26","env":{"CNI_COMMAND":"ADD","CNI_IFNAME":"ens1f0"}},"cniconf":{"cniVersion":"0.3.1","name":"macvlan","master":"ens1f0","mode":"bridge","mtu":1500,"ipam":{"type":"fakeipam","ips":[{"ipcidr":"192.168.1.65/26","version":4}]}}}`)},
{"macvlan-ip6", []byte(`{"cniexp":{"cnitype":"macvlan","ip6":"2a00:8a00:a000:1193::/64","env":{"CNI_COMMAND":"ADD","CNI_IFNAME":"ens1f1"}},"cniconf":{"cniVersion":"0.3.1","name":"macvlan","master":"ens1f1","mode":"bridge","mtu":1500,"ipam":{"type":"fakeipam"}}}`)},
{"macvlan-dual-stack", []byte(`{"cniexp":{"cnitype":"macvlan","ip":"192.168.1.65/26","ip6":"2a00:8a00:a000:1193::/64","env":{"CNI_COMMAND":"ADD","CNI_IFNAME":"ens1f1"}},"cniconf":{"cniVersion":"0.3.1","name":"macvlan","master":"ens1f1","mode":"bridge","mtu":1500,"ipam":{"type":"fakeipam","ips":[{"ipcidr":"192.168.1.65/26","version":4}]}}}`)},
{"macvlan-ip4-type020", []byte(`{"cniexp":{"cnitype":"macvlan","ip":"192.168.1.65/26","env":{"CNI_COMMAND":"ADD","CNI_IFNAME":"ens1f0"},"return":"020"},"cniconf":{"cniVersion":"0.3.1","name":"macvlan","master":"ens1f0","mode":"bridge","mtu":1500,"ipam":{"type":"fakeipam","ips":[{"ipcidr":"192.168.1.65/26","version":4}]}}}`)},
{"macvlan-ip6-type020", []byte(`{"cniexp":{"cnitype":"macvlan","ip6":"2a00:8a00:a000:1193::/64","env":{"CNI_COMMAND":"ADD","CNI_IFNAME":"ens1f1"},"return":"020"},"cniconf":{"cniVersion":"0.3.1","name":"macvlan","master":"ens1f1","mode":"bridge","mtu":1500,"ipam":{"type":"fakeipam"}}}`)},
{"macvlan-ip4", []byte(`{"cniexp":{"cnitype":"macvlan","ip":"192.168.1.65/26","env":{"CNI_COMMAND":"ADD","CNI_IFNAME":"ens1f0"}},"cniconf":{"cniVersion":"0.3.1","name":"macvlan-v4","master":"ens1f0","mode":"bridge","mtu":1500,"ipam":{"type":"fakeipam","ips":[{"ipcidr":"192.168.1.65/26","version":4}]}}}`)},
{"macvlan-ip6", []byte(`{"cniexp":{"cnitype":"macvlan","ip6":"2a00:8a00:a000:1193::/64","env":{"CNI_COMMAND":"ADD","CNI_IFNAME":"ens1f1"}},"cniconf":{"cniVersion":"0.3.1","name":"macvlan-v6","master":"ens1f1","mode":"bridge","mtu":1500,"ipam":{"type":"fakeipam"}}}`)},
{"macvlan-dual-stack", []byte(`{"cniexp":{"cnitype":"macvlan","ip":"192.168.1.65/26","ip6":"2a00:8a00:a000:1193::/64","env":{"CNI_COMMAND":"ADD","CNI_IFNAME":"ens1f1"}},"cniconf":{"cniVersion":"0.3.1","name":"macvlan-ds","master":"ens1f1","mode":"bridge","mtu":1500,"ipam":{"type":"fakeipam","ips":[{"ipcidr":"192.168.1.65/26","version":4}]}}}`)},
{"macvlan-ip4-type020", []byte(`{"cniexp":{"cnitype":"macvlan","ip":"192.168.1.66/26","env":{"CNI_COMMAND":"ADD","CNI_IFNAME":"ens1f0"},"return":"020"},"cniconf":{"cniVersion":"0.3.1","name":"macvlan-v4","master":"ens1f0","mode":"bridge","mtu":1500,"ipam":{"type":"fakeipam","ips":[{"ipcidr":"192.168.1.66/26","version":4}]}}}`)},
{"macvlan-ip6-type020", []byte(`{"cniexp":{"cnitype":"macvlan","ip6":"2a00:8a00:a000:1193::/64","env":{"CNI_COMMAND":"ADD","CNI_IFNAME":"ens1f1"},"return":"020"},"cniconf":{"cniVersion":"0.3.1","name":"macvlan-v6","master":"ens1f1","mode":"bridge","mtu":1500,"ipam":{"type":"fakeipam"}}}`)},
{"sriov-l3", []byte(`{"cniexp":{"cnitype":"sriov","ip":"192.168.1.65/26","env":{"CNI_COMMAND":"ADD","CNI_IFNAME":"eth0"}},"cniconf":{"cniVersion":"0.3.1","name":"sriov-test","type":"sriov","master":"enp175s0f1","l2enable":false,"vlan":500,"deviceID":"0000:af:06.0","ipam":{"type":"fakeipam","ips":[{"ipcidr":"192.168.1.65/26","version":4}]}}}`)},
{"sriov-l2", []byte(`{"cniexp":{"cnitype":"sriov","env":{"CNI_COMMAND":"ADD","CNI_IFNAME":"eth0"}},"cniconf":{"cniVersion":"0.3.1","name":"sriov-test","type":"sriov","master":"enp175s0f1","l2enable":true,"vlan":500,"deviceID":"0000:af:06.0"}}`)},
{"deleteflannel", []byte(`{"cniexp":{"cnitype":"flannel","env":{"CNI_COMMAND":"DEL","CNI_IFNAME":"eth0"}},"cniconf":{"name":"cbr0","type":"flannel","delegate":{"hairpinMode":true,"isDefaultGateway":true}}}`)},
{"deletemacvlan", []byte(`{"cniexp":{"cnitype":"macvlan","env":{"CNI_COMMAND":"DEL","CNI_IFNAME":"ens1f0"}},"cniconf":{"cniVersion":"0.3.1","name":"full","master":"ens1f0","mode":"bridge","mtu":1500}}`)},
{"deletemacvlan", []byte(`{"cniexp":{"cnitype":"macvlan","env":{"CNI_COMMAND":"DEL","CNI_IFNAME":"ens1f0"}},"cniconf":{"cniVersion":"0.3.1","name":"full","master":"ens1f0","mode":"bridge","mtu":1500,"ipam": {"type": "fakeipam","ips":[{"ipcidr":"192.168.1.65/26","version":4}]}}}`)},
{"bridge-l3-ip4", []byte(`{"cniexp":{"cnitype":"macvlan","ip":"192.168.1.65/26","env":{"CNI_COMMAND":"ADD","CNI_IFNAME":"eth0"}},"cniconf":{"name": "mynet","type": "bridge","bridge": "mynet0","isDefaultGateway": true,"forceAddress": false,"ipMasq": true,"hairpinMode": true,"ipam": {"type": "fakeipam","ips":[{"ipcidr":"192.168.1.65/26","version":4}]}}}`)},
{"bridge-l2-ip4", []byte(`{"cniexp":{"cnitype":"macvlan","ip":"192.168.1.65/26","env":{"CNI_COMMAND":"ADD","CNI_IFNAME":"eth0"}},"cniconf":{"name": "mynet","type": "bridge","bridge": "mynet0","ipam": {"type": "fakeipam","ips":[{"ipcidr":"192.168.1.65/26","version":4}]}}}`)},
{"bridge-l3-orig", []byte(`{"cniexp":{"cnitype":"macvlan","env":{"CNI_COMMAND":"ADD","CNI_IFNAME":"eth0"}},"cniconf":{"name": "mynet","type": "bridge","bridge": "mynet0","isDefaultGateway": true,"forceAddress": false,"ipMasq": true,"hairpinMode": true,"ipam": {"type": "host-local","subnet": "10.10.0.0/16"}}}`)},
{"bridge-l2-orig", []byte(`{"cniexp":{"cnitype":"macvlan","env":{"CNI_COMMAND":"ADD","CNI_IFNAME":"eth0"}},"cniconf":{"name": "mynet","type": "bridge","bridge": "mynet0"}}`)},
{"bridge-l3-ip6", []byte(`{"cniexp":{"cnitype":"macvlan","ip6":"2a00:8a00:a000:1193::/64","env":{"CNI_COMMAND":"ADD","CNI_IFNAME":"eth0"}},"cniconf":{"name": "mynet","type": "bridge","bridge": "mynet0","isDefaultGateway": true,"forceAddress": false,"ipMasq": true,"hairpinMode": true,"ipam": {"type": "fakeipam"}}}`)},
{"bridge-l3-ds", []byte(`{"cniexp":{"cnitype":"macvlan","ip":"192.168.1.65/26","ip6":"2a00:8a00:a000:1193::/64","env":{"CNI_COMMAND":"ADD","CNI_IFNAME":"eth0"}},"cniconf":{"name": "mynet","type": "bridge","bridge": "mynet0","isDefaultGateway": true,"forceAddress": false,"ipMasq": true,"hairpinMode": true,"ipam": {"type": "fakeipam","ips":[{"ipcidr":"192.168.1.65/26","version":4}]}}}`)},
{"deletebridge", []byte(`{"cniexp":{"cnitype":"macvlan","env":{"CNI_COMMAND":"DEL","CNI_IFNAME":"eth0"}},"cniconf":{"name": "mynet","type": "bridge","bridge": "mynet0"}}`)},
{"deletebridge", []byte(`{"cniexp":{"cnitype":"macvlan","env":{"CNI_COMMAND":"DEL","CNI_IFNAME":"eth0"}},"cniconf":{"name": "mynet","type": "bridge","bridge": "mynet0","ipam": {"type": "fakeipam","ips":[{"ipcidr":"192.168.1.65/26","version":4}]}}}`)},
{"deletebridge-wo-ipam", []byte(`{"cniexp":{"cnitype":"macvlan","env":{"CNI_COMMAND":"DEL","CNI_IFNAME":"eth0"}},"cniconf":{"name": "mynet","type": "bridge","bridge": "mynet0"}}`)},
}

var testCniConfFiles = []CniConf {
Expand Down Expand Up @@ -239,29 +240,30 @@ var delSetupTcs = []struct {
expectedIp string
expectedIp6 string
isErrorExpected bool
timesUpdateShouldBeCalled int
}{
{"ipamNeededError", "ipamNeeded", "dynamicIpv4", "", "", "", true},
{"emptyIpamconfigError", "ipamNeeded", "noIps", "", "", "", true},
{"staticCniSuccess", "flannel-test", "noIps", "flannel", "", "", false},
{"staticCniNoConfig", "no-conf", "noIps", "", "", "", true},
{"staticCniNoBinary", "no-binary", "noIps", "flannel", "", "", true},
{"staticCniWithIp", "flannel-test", "noIps", "flannel-ip", "10.244.10.30", "", false},
{"dynamicMacvlanIpv4", "macvlan-v4", "dynamicIpv4", "macvlan-ip4", "192.168.1.65", "", false},
{"dynamicMacvlanIpv6", "macvlan-v6", "dynamicIpv6", "macvlan-ip6", "", "2a00:8a00:a000:1193", false},
{"dynamicMacvlanDualStack", "macvlan-ds", "dynamicDual", "macvlan-dual-stack", "192.168.1.65", "2a00:8a00:a000:1193", false},
{"dynamicMacvlanIpv4Type020Result", "macvlan-v4", "dynamicIpv4", "macvlan-ip4-type020", "192.168.1.65", "", false},
{"dynamicMacvlanIpv6Type020Result", "macvlan-v6", "dynamicIpv6", "macvlan-ip6-type020", "", "2a00:8a00:a000:1193", false},
{"dynamicSriovNoDeviceId", "sriov-test", "dynamicIpv4", "", "", "", true},
{"dynamicSriovL3", "sriov-test", "dynamicIpv4WithDeviceId", "sriov-l3", "", "", false},
{"dynamicSriovL2", "sriov-test", "noneWithDeviceId", "sriov-l2", "", "", false},
{"bridgeWithV4Overwrite", "bridge-ipam-ipv4", "simpleIpv4", "bridge-l3-ip4", "", "", false},
{"bridgeWithV4Add", "bridge-ipam-l2", "simpleIpv4", "bridge-l2-ip4", "", "", false},
{"bridgeWithInvalidAdd", "bridge-invalid", "simpleIpv4", "", "", "", true},
{"bridgeL3OriginalNoCidr", "bridge-noipam", "simpleIpv4", "bridge-l3-orig", "", "", false},
{"bridgeL3OriginalNoIp", "bridge-ipam-ipv4", "noIps", "bridge-l3-orig", "", "", false},
{"bridgeL2OriginalNoCidr", "bridge-noipam-l2", "simpleIpv4", "bridge-l2-orig", "", "", false},
{"bridgeWithV6Overwrite", "bridge-ipam-ipv6", "simpleIpv6", "bridge-l3-ip6", "", "", false},
{"bridgeWithDsOverwrite", "bridge-ipam-ds", "simpleDs", "bridge-l3-ds", "", "", false},
{"ipamNeededError", "ipamNeeded", "dynamicIpv4", "", "", "", true, 0},
{"emptyIpamconfigError", "ipamNeeded", "noIps", "", "", "", true, 0},
{"staticCniSuccess", "flannel-test", "noIps", "flannel", "", "", false, 0},
{"staticCniNoConfig", "no-conf", "noIps", "", "", "", true, 0},
{"staticCniNoBinary", "no-binary", "noIps", "flannel", "", "", true, 0},
{"staticCniWithIp", "flannel-test", "noIps", "flannel-ip", "10.244.10.30", "", false, 0},
{"dynamicMacvlanIpv4", "macvlan-v4", "dynamicIpv4", "macvlan-ip4", "192.168.1.65", "", false, 1},
{"dynamicMacvlanIpv6", "macvlan-v6", "dynamicIpv6", "macvlan-ip6", "", "2a00:8a00:a000:1193", false, 0},
{"dynamicMacvlanDualStack", "macvlan-ds", "dynamicDual", "macvlan-dual-stack", "192.168.1.65", "2a00:8a00:a000:1193", false, 1},
{"dynamicMacvlanIpv4Type020Result", "macvlan-v4", "dynamicIpv4", "macvlan-ip4-type020", "192.168.1.65", "", false, 1},
{"dynamicMacvlanIpv6Type020Result", "macvlan-v6", "dynamicIpv6", "macvlan-ip6-type020", "", "2a00:8a00:a000:1193", false, 0},
{"dynamicSriovNoDeviceId", "sriov-test", "dynamicIpv4", "", "", "", true, 2},
{"dynamicSriovL3", "sriov-test", "dynamicIpv4WithDeviceId", "sriov-l3", "", "", false, 1},
{"dynamicSriovL2", "sriov-test", "noneWithDeviceId", "sriov-l2", "", "", false, 0},
{"bridgeWithV4Overwrite", "bridge-ipam-ipv4", "simpleIpv4", "bridge-l3-ip4", "", "", false, 1},
{"bridgeWithV4Add", "bridge-ipam-l2", "simpleIpv4", "bridge-l2-ip4", "", "", false, 1},
{"bridgeWithInvalidAdd", "bridge-invalid", "simpleIpv4", "", "", "", true, 2},
{"bridgeL3OriginalNoCidr", "bridge-noipam", "simpleIpv4", "bridge-l3-orig", "", "", false, 0},
{"bridgeL3OriginalNoIp", "bridge-ipam-ipv4", "noIps", "bridge-l3-orig", "", "", false, 0},
{"bridgeL2OriginalNoCidr", "bridge-noipam-l2", "simpleIpv4", "bridge-l2-orig", "", "", false, 0},
{"bridgeWithV6Overwrite", "bridge-ipam-ipv6", "simpleIpv6", "bridge-l3-ip6", "", "", false, 0},
{"bridgeWithDsOverwrite", "bridge-ipam-ds", "simpleDs", "bridge-l3-ds", "", "", false, 1},
}

var delDeleteTcs = []struct {
Expand All @@ -275,7 +277,7 @@ var delDeleteTcs = []struct {
{"flannel", "flannel-test", "deleteFlannel", "deleteflannel", false, 0},
{"macvlan", "full-macvlan", "withAddress", "deletemacvlan", false, 1},
{"bridgeWithDanmIpam", "full-bridge", "withAddressSimple", "deletebridge", false, 1},
{"bridgeWithExternalIpam", "full-bridge", "withForeignAddressSimple", "deletebridge", false, 0},
{"bridgeWithExternalIpam", "full-bridge", "withForeignAddressSimple", "deletebridge-wo-ipam", false, 0},
}

func TestIsDelegationRequired(t *testing.T) {
Expand Down Expand Up @@ -345,16 +347,17 @@ func TestDelegateInterfaceSetup(t *testing.T) {
if err != nil {
t.Errorf("Test suite could not be set-up because:%s", err.Error())
}
testArtifacts := utils.TestArtifacts{TestNets: testNets}
netClientStub := stubs.NewClientSetStub(testArtifacts)
for _, tc := range delSetupTcs {
t.Run(tc.tcName, func(t *testing.T) {
err = setupDelTestTc(tc.cniConfName)
if err != nil {
t.Errorf("TC could not be set-up because:%s", err.Error())
}
testArtifacts := utils.TestArtifacts{TestNets: testNets}
netClientStub := stubs.NewClientSetStub(testArtifacts)
testNet := utils.GetTestNet(tc.netName, testNets)
testEp := getTestEp(tc.epName)
testEp.Spec.NetworkName = testNet.ObjectMeta.Name
cniRes, err := cnidel.DelegateInterfaceSetup(&cniConf,netClientStub,testNet,testEp)
if (err != nil && !tc.isErrorExpected) || (err == nil && tc.isErrorExpected) {
var detailedErrorMessage string
Expand All @@ -379,6 +382,13 @@ func TestDelegateInterfaceSetup(t *testing.T) {
t.Errorf("Expected IP:%s is not saved in DanmEp.Spec.Iface's respective address field:%s", tc.expectedIp6, testEp.Spec.Iface.AddressIPv6)
}
}
var timesUpdateWasCalled int
if netClientStub.DanmClient.NetClient != nil {
timesUpdateWasCalled = netClientStub.DanmClient.NetClient.TimesUpdateWasCalled
}
if tc.timesUpdateShouldBeCalled != timesUpdateWasCalled {
t.Errorf("Network manifest should have been updated:" + strconv.Itoa(tc.timesUpdateShouldBeCalled) + " times, but it happened:" + strconv.Itoa(timesUpdateWasCalled) + " times instead")
}
})
}
err = teardownDelTest()
Expand Down

0 comments on commit 37dd874

Please sign in to comment.