Skip to content

Commit

Permalink
Address feedback
Browse files Browse the repository at this point in the history
Signed-off-by: torredil <[email protected]>
  • Loading branch information
torredil committed Apr 16, 2024
1 parent f99c5b0 commit f3da9b4
Show file tree
Hide file tree
Showing 13 changed files with 231 additions and 189 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ clean:

.PHONY: test
test:
go test -race ./cmd/... ./pkg/...
go test -v -race ./cmd/... ./pkg/...

.PHONY: test/coverage
test/coverage:
Expand Down
72 changes: 36 additions & 36 deletions cmd/hooks/prestop_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import (
"testing"

"github.com/golang/mock/gomock"
mockclient "github.com/kubernetes-sigs/aws-ebs-csi-driver/pkg/driver/mocks"
"github.com/kubernetes-sigs/aws-ebs-csi-driver/pkg/driver"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
v1 "k8s.io/api/core/v1"
Expand All @@ -19,22 +19,22 @@ func TestPreStopHook(t *testing.T) {
name string
nodeName string
expErr error
mockFunc func(string, *mockclient.MockKubernetesClient, *mockclient.MockCoreV1Interface, *mockclient.MockNodeInterface, *mockclient.MockVolumeAttachmentInterface, *mockclient.MockStorageV1Interface) error
mockFunc func(string, *driver.MockKubernetesClient, *driver.MockCoreV1Interface, *driver.MockNodeInterface, *driver.MockVolumeAttachmentInterface, *driver.MockStorageV1Interface) error
}{
{
name: "TestPreStopHook: CSI_NODE_NAME not set",
nodeName: "",
expErr: fmt.Errorf("PreStop: CSI_NODE_NAME missing"),
mockFunc: func(nodeName string, mockClient *mockclient.MockKubernetesClient, mockCoreV1 *mockclient.MockCoreV1Interface, mockNode *mockclient.MockNodeInterface, mockStorageV1 *mockclient.MockVolumeAttachmentInterface, mockStorageV1Interface *mockclient.MockStorageV1Interface) error {
mockFunc: func(nodeName string, driver *driver.MockKubernetesClient, mockCoreV1 *driver.MockCoreV1Interface, mockNode *driver.MockNodeInterface, mockStorageV1 *driver.MockVolumeAttachmentInterface, mockStorageV1Interface *driver.MockStorageV1Interface) error {
return nil
},
},
{
name: "TestPreStopHook: failed to retrieve node information",
nodeName: "test-node",
expErr: fmt.Errorf("fetchNode: failed to retrieve node information: non-existent node"),
mockFunc: func(nodeName string, mockClient *mockclient.MockKubernetesClient, mockCoreV1 *mockclient.MockCoreV1Interface, mockNode *mockclient.MockNodeInterface, mockStorageV1 *mockclient.MockVolumeAttachmentInterface, mockStorageV1Interface *mockclient.MockStorageV1Interface) error {
mockClient.EXPECT().CoreV1().Return(mockCoreV1).Times(1)
mockFunc: func(nodeName string, driver *driver.MockKubernetesClient, mockCoreV1 *driver.MockCoreV1Interface, mockNode *driver.MockNodeInterface, mockStorageV1 *driver.MockVolumeAttachmentInterface, mockStorageV1Interface *driver.MockStorageV1Interface) error {
driver.EXPECT().CoreV1().Return(mockCoreV1).Times(1)
mockCoreV1.EXPECT().Nodes().Return(mockNode).Times(1)
mockNode.EXPECT().Get(gomock.Any(), gomock.Eq(nodeName), gomock.Any()).Return(nil, fmt.Errorf("non-existent node")).Times(1)

Expand All @@ -45,14 +45,14 @@ func TestPreStopHook(t *testing.T) {
name: "TestPreStopHook: node is not being drained, skipping VolumeAttachments check - missing TaintNodeUnschedulable",
nodeName: "test-node",
expErr: nil,
mockFunc: func(nodeName string, mockClient *mockclient.MockKubernetesClient, mockCoreV1 *mockclient.MockCoreV1Interface, mockNode *mockclient.MockNodeInterface, mockStorageV1 *mockclient.MockVolumeAttachmentInterface, mockStorageV1Interface *mockclient.MockStorageV1Interface) error {
mockFunc: func(nodeName string, driver *driver.MockKubernetesClient, mockCoreV1 *driver.MockCoreV1Interface, mockNode *driver.MockNodeInterface, mockStorageV1 *driver.MockVolumeAttachmentInterface, mockStorageV1Interface *driver.MockStorageV1Interface) error {
mockNodeObj := &v1.Node{
Spec: v1.NodeSpec{
Taints: []v1.Taint{},
},
}

mockClient.EXPECT().CoreV1().Return(mockCoreV1).Times(1)
driver.EXPECT().CoreV1().Return(mockCoreV1).Times(1)
mockCoreV1.EXPECT().Nodes().Return(mockNode).Times(1)
mockNode.EXPECT().Get(gomock.Any(), gomock.Eq(nodeName), gomock.Any()).Return(mockNodeObj, nil).Times(1)

Expand All @@ -63,7 +63,7 @@ func TestPreStopHook(t *testing.T) {
name: "TestPreStopHook: node is not being drained, skipping VolumeAttachments check - missing TaintEffectNoSchedule",
nodeName: "test-node",
expErr: nil,
mockFunc: func(nodeName string, mockClient *mockclient.MockKubernetesClient, mockCoreV1 *mockclient.MockCoreV1Interface, mockNode *mockclient.MockNodeInterface, mockStorageV1 *mockclient.MockVolumeAttachmentInterface, mockStorageV1Interface *mockclient.MockStorageV1Interface) error {
mockFunc: func(nodeName string, driver *driver.MockKubernetesClient, mockCoreV1 *driver.MockCoreV1Interface, mockNode *driver.MockNodeInterface, mockStorageV1 *driver.MockVolumeAttachmentInterface, mockStorageV1Interface *driver.MockStorageV1Interface) error {
mockNodeObj := &v1.Node{
Spec: v1.NodeSpec{
Taints: []v1.Taint{
Expand All @@ -75,7 +75,7 @@ func TestPreStopHook(t *testing.T) {
},
}

mockClient.EXPECT().CoreV1().Return(mockCoreV1).Times(1)
driver.EXPECT().CoreV1().Return(mockCoreV1).Times(1)
mockCoreV1.EXPECT().Nodes().Return(mockNode).Times(1)
mockNode.EXPECT().Get(gomock.Any(), gomock.Eq(nodeName), gomock.Any()).Return(mockNodeObj, nil).Times(1)

Expand All @@ -86,7 +86,7 @@ func TestPreStopHook(t *testing.T) {
name: "TestPreStopHook: node is being drained, no volume attachments remain",
nodeName: "test-node",
expErr: nil,
mockFunc: func(nodeName string, mockClient *mockclient.MockKubernetesClient, mockCoreV1 *mockclient.MockCoreV1Interface, mockNode *mockclient.MockNodeInterface, mockVolumeAttachments *mockclient.MockVolumeAttachmentInterface, mockStorageV1Interface *mockclient.MockStorageV1Interface) error {
mockFunc: func(nodeName string, driver *driver.MockKubernetesClient, mockCoreV1 *driver.MockCoreV1Interface, mockNode *driver.MockNodeInterface, mockVolumeAttachments *driver.MockVolumeAttachmentInterface, mockStorageV1Interface *driver.MockStorageV1Interface) error {

fakeNode := &v1.Node{
Spec: v1.NodeSpec{
Expand All @@ -101,8 +101,8 @@ func TestPreStopHook(t *testing.T) {

emptyVolumeAttachments := &storagev1.VolumeAttachmentList{Items: []storagev1.VolumeAttachment{}}

mockClient.EXPECT().CoreV1().Return(mockCoreV1).AnyTimes()
mockClient.EXPECT().StorageV1().Return(mockStorageV1Interface).AnyTimes()
driver.EXPECT().CoreV1().Return(mockCoreV1).AnyTimes()
driver.EXPECT().StorageV1().Return(mockStorageV1Interface).AnyTimes()

mockCoreV1.EXPECT().Nodes().Return(mockNode).AnyTimes()
mockNode.EXPECT().Get(gomock.Any(), gomock.Eq(nodeName), gomock.Any()).Return(fakeNode, nil).AnyTimes()
Expand All @@ -118,7 +118,7 @@ func TestPreStopHook(t *testing.T) {
name: "TestPreStopHook: node is being drained, no volume attachments associated with node",
nodeName: "test-node",
expErr: nil,
mockFunc: func(nodeName string, mockClient *mockclient.MockKubernetesClient, mockCoreV1 *mockclient.MockCoreV1Interface, mockNode *mockclient.MockNodeInterface, mockVolumeAttachments *mockclient.MockVolumeAttachmentInterface, mockStorageV1Interface *mockclient.MockStorageV1Interface) error {
mockFunc: func(nodeName string, driver *driver.MockKubernetesClient, mockCoreV1 *driver.MockCoreV1Interface, mockNode *driver.MockNodeInterface, mockVolumeAttachments *driver.MockVolumeAttachmentInterface, mockStorageV1Interface *driver.MockStorageV1Interface) error {

fakeNode := &v1.Node{
Spec: v1.NodeSpec{
Expand All @@ -141,8 +141,8 @@ func TestPreStopHook(t *testing.T) {
},
}

mockClient.EXPECT().CoreV1().Return(mockCoreV1).AnyTimes()
mockClient.EXPECT().StorageV1().Return(mockStorageV1Interface).AnyTimes()
driver.EXPECT().CoreV1().Return(mockCoreV1).AnyTimes()
driver.EXPECT().StorageV1().Return(mockStorageV1Interface).AnyTimes()

mockCoreV1.EXPECT().Nodes().Return(mockNode).AnyTimes()
mockNode.EXPECT().Get(gomock.Any(), gomock.Eq(nodeName), gomock.Any()).Return(fakeNode, nil).AnyTimes()
Expand All @@ -158,7 +158,7 @@ func TestPreStopHook(t *testing.T) {
name: "TestPreStopHook: Node is drained before timeout",
nodeName: "test-node",
expErr: nil,
mockFunc: func(nodeName string, mockClient *mockclient.MockKubernetesClient, mockCoreV1 *mockclient.MockCoreV1Interface, mockNode *mockclient.MockNodeInterface, mockVolumeAttachments *mockclient.MockVolumeAttachmentInterface, mockStorageV1Interface *mockclient.MockStorageV1Interface) error {
mockFunc: func(nodeName string, driver *driver.MockKubernetesClient, mockCoreV1 *driver.MockCoreV1Interface, mockNode *driver.MockNodeInterface, mockVolumeAttachments *driver.MockVolumeAttachmentInterface, mockStorageV1Interface *driver.MockStorageV1Interface) error {

fakeNode := &v1.Node{
Spec: v1.NodeSpec{
Expand All @@ -184,8 +184,8 @@ func TestPreStopHook(t *testing.T) {
fakeWatcher := watch.NewFake()
deleteSignal := make(chan bool, 1)

mockClient.EXPECT().CoreV1().Return(mockCoreV1).AnyTimes()
mockClient.EXPECT().StorageV1().Return(mockStorageV1Interface).AnyTimes()
driver.EXPECT().CoreV1().Return(mockCoreV1).AnyTimes()
driver.EXPECT().StorageV1().Return(mockStorageV1Interface).AnyTimes()

mockCoreV1.EXPECT().Nodes().Return(mockNode).AnyTimes()
mockNode.EXPECT().Get(gomock.Any(), gomock.Eq(nodeName), gomock.Any()).Return(fakeNode, nil).AnyTimes()
Expand Down Expand Up @@ -215,7 +215,7 @@ func TestPreStopHook(t *testing.T) {
name: "TestPreStopHook: Karpenter node is not being drained, skipping VolumeAttachments check - missing TaintEffectNoSchedule",
nodeName: "test-karpenter-node",
expErr: nil,
mockFunc: func(nodeName string, mockClient *mockclient.MockKubernetesClient, mockCoreV1 *mockclient.MockCoreV1Interface, mockNode *mockclient.MockNodeInterface, mockStorageV1 *mockclient.MockVolumeAttachmentInterface, mockStorageV1Interface *mockclient.MockStorageV1Interface) error {
mockFunc: func(nodeName string, driver *driver.MockKubernetesClient, mockCoreV1 *driver.MockCoreV1Interface, mockNode *driver.MockNodeInterface, mockStorageV1 *driver.MockVolumeAttachmentInterface, mockStorageV1Interface *driver.MockStorageV1Interface) error {
mockNodeObj := &v1.Node{
Spec: v1.NodeSpec{
Taints: []v1.Taint{
Expand All @@ -227,7 +227,7 @@ func TestPreStopHook(t *testing.T) {
},
}

mockClient.EXPECT().CoreV1().Return(mockCoreV1).Times(1)
driver.EXPECT().CoreV1().Return(mockCoreV1).Times(1)
mockCoreV1.EXPECT().Nodes().Return(mockNode).Times(1)
mockNode.EXPECT().Get(gomock.Any(), gomock.Eq(nodeName), gomock.Any()).Return(mockNodeObj, nil).Times(1)

Expand All @@ -238,7 +238,7 @@ func TestPreStopHook(t *testing.T) {
name: "TestPreStopHook: Karpenter node is being drained, no volume attachments remain",
nodeName: "test-karpenter-node",
expErr: nil,
mockFunc: func(nodeName string, mockClient *mockclient.MockKubernetesClient, mockCoreV1 *mockclient.MockCoreV1Interface, mockNode *mockclient.MockNodeInterface, mockVolumeAttachments *mockclient.MockVolumeAttachmentInterface, mockStorageV1Interface *mockclient.MockStorageV1Interface) error {
mockFunc: func(nodeName string, driver *driver.MockKubernetesClient, mockCoreV1 *driver.MockCoreV1Interface, mockNode *driver.MockNodeInterface, mockVolumeAttachments *driver.MockVolumeAttachmentInterface, mockStorageV1Interface *driver.MockStorageV1Interface) error {

fakeNode := &v1.Node{
Spec: v1.NodeSpec{
Expand All @@ -253,8 +253,8 @@ func TestPreStopHook(t *testing.T) {

emptyVolumeAttachments := &storagev1.VolumeAttachmentList{Items: []storagev1.VolumeAttachment{}}

mockClient.EXPECT().CoreV1().Return(mockCoreV1).AnyTimes()
mockClient.EXPECT().StorageV1().Return(mockStorageV1Interface).AnyTimes()
driver.EXPECT().CoreV1().Return(mockCoreV1).AnyTimes()
driver.EXPECT().StorageV1().Return(mockStorageV1Interface).AnyTimes()

mockCoreV1.EXPECT().Nodes().Return(mockNode).AnyTimes()
mockNode.EXPECT().Get(gomock.Any(), gomock.Eq(nodeName), gomock.Any()).Return(fakeNode, nil).AnyTimes()
Expand All @@ -270,7 +270,7 @@ func TestPreStopHook(t *testing.T) {
name: "TestPreStopHook: Karpenter node is being drained, no volume attachments associated with node",
nodeName: "test-karpenter-node",
expErr: nil,
mockFunc: func(nodeName string, mockClient *mockclient.MockKubernetesClient, mockCoreV1 *mockclient.MockCoreV1Interface, mockNode *mockclient.MockNodeInterface, mockVolumeAttachments *mockclient.MockVolumeAttachmentInterface, mockStorageV1Interface *mockclient.MockStorageV1Interface) error {
mockFunc: func(nodeName string, driver *driver.MockKubernetesClient, mockCoreV1 *driver.MockCoreV1Interface, mockNode *driver.MockNodeInterface, mockVolumeAttachments *driver.MockVolumeAttachmentInterface, mockStorageV1Interface *driver.MockStorageV1Interface) error {

fakeNode := &v1.Node{
Spec: v1.NodeSpec{
Expand All @@ -293,8 +293,8 @@ func TestPreStopHook(t *testing.T) {
},
}

mockClient.EXPECT().CoreV1().Return(mockCoreV1).AnyTimes()
mockClient.EXPECT().StorageV1().Return(mockStorageV1Interface).AnyTimes()
driver.EXPECT().CoreV1().Return(mockCoreV1).AnyTimes()
driver.EXPECT().StorageV1().Return(mockStorageV1Interface).AnyTimes()

mockCoreV1.EXPECT().Nodes().Return(mockNode).AnyTimes()
mockNode.EXPECT().Get(gomock.Any(), gomock.Eq(nodeName), gomock.Any()).Return(fakeNode, nil).AnyTimes()
Expand All @@ -310,7 +310,7 @@ func TestPreStopHook(t *testing.T) {
name: "TestPreStopHook: Karpenter Node is drained before timeout",
nodeName: "test-karpenter-node",
expErr: nil,
mockFunc: func(nodeName string, mockClient *mockclient.MockKubernetesClient, mockCoreV1 *mockclient.MockCoreV1Interface, mockNode *mockclient.MockNodeInterface, mockVolumeAttachments *mockclient.MockVolumeAttachmentInterface, mockStorageV1Interface *mockclient.MockStorageV1Interface) error {
mockFunc: func(nodeName string, driver *driver.MockKubernetesClient, mockCoreV1 *driver.MockCoreV1Interface, mockNode *driver.MockNodeInterface, mockVolumeAttachments *driver.MockVolumeAttachmentInterface, mockStorageV1Interface *driver.MockStorageV1Interface) error {

fakeNode := &v1.Node{
Spec: v1.NodeSpec{
Expand All @@ -336,8 +336,8 @@ func TestPreStopHook(t *testing.T) {
fakeWatcher := watch.NewFake()
deleteSignal := make(chan bool, 1)

mockClient.EXPECT().CoreV1().Return(mockCoreV1).AnyTimes()
mockClient.EXPECT().StorageV1().Return(mockStorageV1Interface).AnyTimes()
driver.EXPECT().CoreV1().Return(mockCoreV1).AnyTimes()
driver.EXPECT().StorageV1().Return(mockStorageV1Interface).AnyTimes()

mockCoreV1.EXPECT().Nodes().Return(mockNode).AnyTimes()
mockNode.EXPECT().Get(gomock.Any(), gomock.Eq(nodeName), gomock.Any()).Return(fakeNode, nil).AnyTimes()
Expand Down Expand Up @@ -370,14 +370,14 @@ func TestPreStopHook(t *testing.T) {
mockCtl := gomock.NewController(t)
defer mockCtl.Finish()

mockClient := mockclient.NewMockKubernetesClient(mockCtl)
mockCoreV1 := mockclient.NewMockCoreV1Interface(mockCtl)
mockStorageV1 := mockclient.NewMockStorageV1Interface(mockCtl)
mockNode := mockclient.NewMockNodeInterface(mockCtl)
mockVolumeAttachments := mockclient.NewMockVolumeAttachmentInterface(mockCtl)
d := driver.NewMockKubernetesClient(mockCtl)
mockCoreV1 := driver.NewMockCoreV1Interface(mockCtl)
mockStorageV1 := driver.NewMockStorageV1Interface(mockCtl)
mockNode := driver.NewMockNodeInterface(mockCtl)
mockVolumeAttachments := driver.NewMockVolumeAttachmentInterface(mockCtl)

if tc.mockFunc != nil {
err := tc.mockFunc(tc.nodeName, mockClient, mockCoreV1, mockNode, mockVolumeAttachments, mockStorageV1)
err := tc.mockFunc(tc.nodeName, d, mockCoreV1, mockNode, mockVolumeAttachments, mockStorageV1)
if err != nil {
t.Fatalf("TestPreStopHook: mockFunc returned error: %v", err)
}
Expand All @@ -387,7 +387,7 @@ func TestPreStopHook(t *testing.T) {
t.Setenv("CSI_NODE_NAME", tc.nodeName)
}

err := PreStop(mockClient)
err := PreStop(d)

if tc.expErr != nil {
require.Error(t, err)
Expand Down
25 changes: 18 additions & 7 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,18 +139,29 @@ func main() {
}

region := os.Getenv("AWS_REGION")

md, metadataErr := metadata.NewMetadataService(cfg, region)
if metadataErr != nil {
klog.ErrorS(err, "Could not determine region from any metadata service. The region can be manually supplied via the AWS_REGION environment variable.")
panic(err)
}
var md metadata.MetadataService
var metadataErr error

if region == "" {
klog.V(5).InfoS("[Debug] Retrieving region from metadata service")
md, metadataErr = metadata.NewMetadataService(cfg, region)
if metadataErr != nil {
klog.ErrorS(metadataErr, "Could not determine region from any metadata service. The region can be manually supplied via the AWS_REGION environment variable.")
panic(metadataErr)
}
region = md.GetRegion()
}

if md == nil {
if options.Mode == driver.NodeMode || options.Mode == driver.AllMode {
md, metadataErr = metadata.NewMetadataService(cfg, region)
if metadataErr != nil {
klog.ErrorS(metadataErr, "failed to initialize metadata service")
klog.FlushAndExit(klog.ExitFlushTimeout, 1)
}
}
}

cloud, err := cloud.NewCloud(region, options.AwsSdkDebugLog, options.UserAgentExtra, options.Batching)
if err != nil {
klog.ErrorS(err, "failed to create cloud service")
Expand All @@ -164,7 +175,7 @@ func main() {

k8sClient, err := cfg.K8sAPIClient()
if err != nil {
klog.V(4).InfoS("Failed to setup k8s client")
klog.V(2).InfoS("Failed to setup k8s client")
}

drv, err := driver.NewDriver(cloud, &options, m, md, k8sClient)
Expand Down
12 changes: 6 additions & 6 deletions hack/update-mockgen.sh
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,12 @@ BIN="$(dirname "$(realpath "${BASH_SOURCE[0]}")")/../bin"
# Source-based mocking for internal interfaces
"${BIN}/mockgen" -package cloud -destination=./pkg/cloud/mock_cloud.go -source pkg/cloud/interface.go
"${BIN}/mockgen" -package metadata -destination=./pkg/cloud/metadata/mock_metadata.go -source pkg/cloud/metadata/interface.go
"${BIN}/mockgen" -package mounter -destination=./pkg/mounter/mocks/mock_mount.go -source pkg/mounter/mount.go
"${BIN}/mockgen" -package mounter -destination=./pkg/mounter/mocks/mock_mount_windows.go -source pkg/mounter/safe_mounter_windows.go
"${BIN}/mockgen" -package mounter -destination=./pkg/mounter/mock_mount.go -source pkg/mounter/mount.go
"${BIN}/mockgen" -package mounter -destination=./pkg/mounter/mock_mount_windows.go -source pkg/mounter/safe_mounter_windows.go
"${BIN}/mockgen" -package cloud -destination=./pkg/cloud/mock_ec2.go -source pkg/cloud/ec2_interface.go EC2API

# Reflection-based mocking for external dependencies
"${BIN}/mockgen" -package driver -destination=./pkg/driver/mocks/mock_k8s_client.go -mock_names='Interface=MockKubernetesClient' k8s.io/client-go/kubernetes Interface
"${BIN}/mockgen" -package driver -destination=./pkg/driver/mocks/mock_k8s_corev1.go k8s.io/client-go/kubernetes/typed/core/v1 CoreV1Interface,NodeInterface
"${BIN}/mockgen" -package driver -destination=./pkg/driver/mocks/mock_k8s_storagev1.go k8s.io/client-go/kubernetes/typed/storage/v1 VolumeAttachmentInterface,StorageV1Interface
"${BIN}/mockgen" -package driver -destination=./pkg/driver/mocks/mock_k8s_storagev1_csinode.go k8s.io/client-go/kubernetes/typed/storage/v1 CSINodeInterface
"${BIN}/mockgen" -package driver -destination=./pkg/driver/mock_k8s_client.go -mock_names='Interface=MockKubernetesClient' k8s.io/client-go/kubernetes Interface
"${BIN}/mockgen" -package driver -destination=./pkg/driver/mock_k8s_corev1.go k8s.io/client-go/kubernetes/typed/core/v1 CoreV1Interface,NodeInterface
"${BIN}/mockgen" -package driver -destination=./pkg/driver/mock_k8s_storagev1.go k8s.io/client-go/kubernetes/typed/storage/v1 VolumeAttachmentInterface,StorageV1Interface
"${BIN}/mockgen" -package driver -destination=./pkg/driver/mock_k8s_storagev1_csinode.go k8s.io/client-go/kubernetes/typed/storage/v1 CSINodeInterface
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Loading

0 comments on commit f3da9b4

Please sign in to comment.