api/handler/service_volume_test.go
Rama main · réplica de lectura del repositorio original.
package handler
import (
"context"
"strings"
"testing"
"github.com/DATA-DOG/go-sqlmock"
apimodel "github.com/goodrain/rainbond/api/model"
"github.com/goodrain/rainbond/api/util/bcode"
"github.com/goodrain/rainbond/db"
dbdao "github.com/goodrain/rainbond/db/dao"
dbmodel "github.com/goodrain/rainbond/db/model"
"github.com/jinzhu/gorm"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes/fake"
kubevirtv1 "kubevirt.io/api/core/v1"
)
type volumeUpdateTestManager struct {
db.Manager
tx *gorm.DB
volumeDao dbdao.TenantServiceVolumeDao
serviceDao dbdao.TenantServiceDao
}
func (m volumeUpdateTestManager) Begin() *gorm.DB {
return m.tx
}
func (m volumeUpdateTestManager) TenantServiceVolumeDaoTransactions(*gorm.DB) dbdao.TenantServiceVolumeDao {
return m.volumeDao
}
func (m volumeUpdateTestManager) TenantServiceDao() dbdao.TenantServiceDao {
return m.serviceDao
}
type volumeUpdateTenantServiceDao struct {
dbdao.TenantServiceDao
service *dbmodel.TenantServices
}
func (d *volumeUpdateTenantServiceDao) GetServiceByID(string) (*dbmodel.TenantServices, error) {
return d.service, nil
}
type volumeUpdateTenantServiceVolumeDao struct {
dbdao.TenantServiceVolumeDao
volume *dbmodel.TenantServiceVolume
requestSID string
requestName string
updatedVolume *dbmodel.TenantServiceVolume
}
func (d *volumeUpdateTenantServiceVolumeDao) GetVolumeByServiceIDAndName(serviceID, name string) (*dbmodel.TenantServiceVolume, error) {
d.requestSID = serviceID
d.requestName = name
return d.volume, nil
}
func (d *volumeUpdateTenantServiceVolumeDao) UpdateModel(arg dbmodel.Interface) error {
volume, ok := arg.(*dbmodel.TenantServiceVolume)
if !ok {
return nil
}
copied := *volume
d.updatedVolume = &copied
return nil
}
type volumeDeleteSharedMountTestManager struct {
db.Manager
tx *gorm.DB
volumeDao dbdao.TenantServiceVolumeDao
mountRelationDao dbdao.TenantServiceMountRelationDao
configFileDao dbdao.TenantServiceConfigFileDao
}
func (m volumeDeleteSharedMountTestManager) Begin() *gorm.DB {
return m.tx
}
func (m volumeDeleteSharedMountTestManager) TenantServiceVolumeDaoTransactions(*gorm.DB) dbdao.TenantServiceVolumeDao {
return m.volumeDao
}
func (m volumeDeleteSharedMountTestManager) TenantServiceMountRelationDaoTransactions(*gorm.DB) dbdao.TenantServiceMountRelationDao {
return m.mountRelationDao
}
func (m volumeDeleteSharedMountTestManager) TenantServiceConfigFileDaoTransactions(*gorm.DB) dbdao.TenantServiceConfigFileDao {
return m.configFileDao
}
type volumeDeleteTenantServiceVolumeDao struct {
dbdao.TenantServiceVolumeDao
volume *dbmodel.TenantServiceVolume
deleted bool
}
func (d *volumeDeleteTenantServiceVolumeDao) GetVolumeByServiceIDAndName(serviceID, name string) (*dbmodel.TenantServiceVolume, error) {
return d.volume, nil
}
func (d *volumeDeleteTenantServiceVolumeDao) DeleteModel(serviceID string, args ...interface{}) error {
d.deleted = true
return nil
}
type volumeDeleteMountRelationDao struct {
dbdao.TenantServiceMountRelationDao
relations []*dbmodel.TenantServiceMountRelation
}
func (d *volumeDeleteMountRelationDao) GetTenantServiceMountRelationsByDepServiceAndVolumeName(serviceID, volumeName string) ([]*dbmodel.TenantServiceMountRelation, error) {
return d.relations, nil
}
type volumeDeleteConfigFileDao struct {
dbdao.TenantServiceConfigFileDao
deleted bool
}
func (d *volumeDeleteConfigFileDao) DelByVolumeID(serviceID, volumeName string) error {
d.deleted = true
return nil
}
// capability_id: rainbond.component.volume-update-persists-capacity
func TestServiceActionUpdVolumeUpdatesVolumeCapacity(t *testing.T) {
sqlDB, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("create sqlmock: %v", err)
}
defer sqlDB.Close()
gdb, err := gorm.Open("mysql", sqlDB)
if err != nil {
t.Fatalf("open gorm db: %v", err)
}
defer gdb.Close()
mock.ExpectBegin()
tx := gdb.Begin()
if err := tx.Error; err != nil {
t.Fatalf("begin tx: %v", err)
}
mock.ExpectCommit()
volumeDao := &volumeUpdateTenantServiceVolumeDao{
volume: &dbmodel.TenantServiceVolume{
Model: dbmodel.Model{ID: 1},
ServiceID: "service-1",
VolumeName: "data",
VolumePath: "/data",
VolumeCapacity: 10,
},
}
db.SetTestManager(volumeUpdateTestManager{
tx: tx,
volumeDao: volumeDao,
serviceDao: &volumeUpdateTenantServiceDao{service: &dbmodel.TenantServices{
ServiceID: "service-1",
}},
})
defer db.SetTestManager(nil)
volumeCapacity := int64(20)
err = (&ServiceAction{}).UpdVolume("service-1", &apimodel.UpdVolumeReq{
VolumeName: "data",
VolumeType: "share-file",
VolumePath: "/data",
VolumeCapacity: &volumeCapacity,
})
if err != nil {
t.Fatalf("update volume: %v", err)
}
if volumeDao.requestSID != "service-1" || volumeDao.requestName != "data" {
t.Fatalf("expected volume lookup by service-1/data, got %s/%s", volumeDao.requestSID, volumeDao.requestName)
}
if volumeDao.updatedVolume == nil {
t.Fatal("expected updated volume to be saved")
}
if volumeDao.updatedVolume.VolumeCapacity != 20 {
t.Fatalf("expected volume capacity 20, got %d", volumeDao.updatedVolume.VolumeCapacity)
}
if volumeDao.updatedVolume.VolumePath != "/data" {
t.Fatalf("expected volume path /data, got %s", volumeDao.updatedVolume.VolumePath)
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Fatalf("unmet sql expectations: %v", err)
}
}
// capability_id: rainbond.component.volume-path-update-without-expansion
func TestServiceActionUpdVolumePathDoesNotRequireExpansion(t *testing.T) {
for _, test := range []struct {
name string
volumeType string
extendMethod string
enableSubpath string
}{
{name: "non-expandable StorageClass", volumeType: "fixed"},
{name: "memory filesystem", volumeType: dbmodel.MemoryFSVolumeType.String()},
{name: "virtual machine", volumeType: "fixed", extendMethod: "vm"},
{name: "shared subpath", volumeType: dbmodel.ShareFileVolumeType.String(), enableSubpath: "true"},
} {
t.Run(test.name, func(t *testing.T) {
t.Setenv("ENABLE_SUBPATH", test.enableSubpath)
sqlDB, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("create sqlmock: %v", err)
}
defer sqlDB.Close()
gdb, err := gorm.Open("mysql", sqlDB)
if err != nil {
t.Fatalf("open gorm db: %v", err)
}
defer gdb.Close()
mock.ExpectBegin()
tx := gdb.Begin()
if err := tx.Error; err != nil {
t.Fatalf("begin tx: %v", err)
}
mock.ExpectCommit()
capacity := int64(20)
volumeDao := &volumeUpdateTenantServiceVolumeDao{volume: &dbmodel.TenantServiceVolume{
Model: dbmodel.Model{ID: 7},
ServiceID: "service-1",
VolumeName: "data",
VolumeType: test.volumeType,
VolumePath: "/data",
VolumeCapacity: capacity,
}}
client := fake.NewSimpleClientset(
expansionStorageClass("fixed", false),
expansionPVC("tenant-ns", "manual7", "service-1", "data", "fixed", "20Gi", "20Gi"),
)
queriedVMServiceID := ""
action := &ServiceAction{
dbmanager: volumeUpdateTestManager{
tx: tx,
volumeDao: volumeDao,
serviceDao: &volumeUpdateTenantServiceDao{service: &dbmodel.TenantServices{
ServiceID: "service-1",
Namespace: "tenant-ns",
ExtendMethod: test.extendMethod,
}},
},
kubeClient: client,
getVirtualMachineByServiceIDHook: func(serviceID string) (*kubevirtv1.VirtualMachine, error) {
queriedVMServiceID = serviceID
return nil, nil
},
}
if err := action.UpdVolume("service-1", &apimodel.UpdVolumeReq{
VolumeName: "data",
VolumeType: test.volumeType,
VolumePath: "/new-data",
VolumeCapacity: &capacity,
}); err != nil {
t.Fatalf("path update with unchanged capacity should succeed: %v", err)
}
if volumeDao.updatedVolume == nil || volumeDao.updatedVolume.VolumePath != "/new-data" ||
volumeDao.updatedVolume.VolumeCapacity != capacity {
t.Fatalf("expected new path and unchanged capacity, got %#v", volumeDao.updatedVolume)
}
if len(client.Actions()) != 0 {
t.Fatalf("path update should not inspect or expand PVCs, got %v", client.Actions())
}
if test.extendMethod == "vm" && queriedVMServiceID != "service-1" {
t.Fatalf("expected existing VM sync to query service-1, got %q", queriedVMServiceID)
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Fatalf("unmet SQL expectations: %v", err)
}
})
}
}
// capability_id: rainbond.component.volume-expansion-reconciles-drift
func TestServiceActionUpdVolumeReconcilesStoredCapacity(t *testing.T) {
sqlDB, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("create sqlmock: %v", err)
}
defer sqlDB.Close()
gdb, err := gorm.Open("mysql", sqlDB)
if err != nil {
t.Fatalf("open gorm db: %v", err)
}
defer gdb.Close()
mock.ExpectBegin()
tx := gdb.Begin()
if err := tx.Error; err != nil {
t.Fatalf("begin tx: %v", err)
}
mock.ExpectCommit()
service := &dbmodel.TenantServices{
TenantID: "tenant-uuid",
ServiceID: "service-1",
Namespace: "goodrain",
}
volumeDao := &volumeUpdateTenantServiceVolumeDao{volume: &dbmodel.TenantServiceVolume{
Model: dbmodel.Model{ID: 1},
ServiceID: service.ServiceID,
VolumeName: "data",
VolumeType: "fast",
VolumePath: "/data",
VolumeCapacity: 20,
}}
manager := volumeUpdateTestManager{
tx: tx,
volumeDao: volumeDao,
serviceDao: &volumeUpdateTenantServiceDao{service: service},
}
client := fake.NewSimpleClientset(
expansionStorageClass("fast", true),
expansionPVC("default", "manual1", "service-1", "data", "fast", "10Gi", "10Gi"),
)
action := &ServiceAction{
dbmanager: manager,
kubeClient: client,
resolveTenantNamespaceHook: func(string) (string, error) {
return "default", nil
},
}
targetCapacity := int64(20)
if err := action.UpdVolume("service-1", &apimodel.UpdVolumeReq{
VolumeName: "data",
VolumeType: "fast",
VolumePath: "/data",
VolumeCapacity: &targetCapacity,
}); err != nil {
t.Fatalf("update volume: %v", err)
}
pvc, err := client.CoreV1().PersistentVolumeClaims("default").Get(
context.Background(), "manual1", metav1.GetOptions{})
if err != nil {
t.Fatalf("get PVC: %v", err)
}
if got := pvc.Spec.Resources.Requests[corev1.ResourceStorage]; got.String() != "20Gi" {
t.Fatalf("PVC request = %s, want 20Gi", got.String())
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Fatalf("unmet SQL expectations: %v", err)
}
}
// capability_id: rainbond.component.volume-expansion-only-grows
func TestServiceActionUpdVolumeRejectsShrink(t *testing.T) {
sqlDB, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("create sqlmock: %v", err)
}
defer sqlDB.Close()
gdb, err := gorm.Open("mysql", sqlDB)
if err != nil {
t.Fatalf("open gorm db: %v", err)
}
defer gdb.Close()
mock.ExpectBegin()
tx := gdb.Begin()
if err := tx.Error; err != nil {
t.Fatalf("begin tx: %v", err)
}
mock.ExpectRollback()
volumeDao := &volumeUpdateTenantServiceVolumeDao{
volume: &dbmodel.TenantServiceVolume{
Model: dbmodel.Model{ID: 1},
ServiceID: "service-1",
VolumeName: "data",
VolumeType: "share-file",
VolumePath: "/data",
VolumeCapacity: 20,
},
}
db.SetTestManager(volumeUpdateTestManager{tx: tx, volumeDao: volumeDao})
defer db.SetTestManager(nil)
volumeCapacity := int64(10)
err = (&ServiceAction{}).UpdVolume("service-1", &apimodel.UpdVolumeReq{
VolumeName: "data",
VolumeType: "share-file",
VolumePath: "/data",
VolumeCapacity: &volumeCapacity,
})
if err == nil {
t.Fatal("expected shrinking a volume to fail")
}
if got := bcode.Err2Coder(err).GetStatus(); got != 400 {
t.Fatalf("error status = %d, want 400: %v", got, err)
}
if volumeDao.updatedVolume != nil {
t.Fatalf("volume was updated after rejected shrink: %#v", volumeDao.updatedVolume)
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Fatalf("unmet sql expectations: %v", err)
}
}
// capability_id: rainbond.component.volume-delete-blocks-shared-mount
func TestServiceActionVolumnVarDeleteRejectsSharedMountedVolume(t *testing.T) {
sqlDB, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("create sqlmock: %v", err)
}
defer sqlDB.Close()
gdb, err := gorm.Open("mysql", sqlDB)
if err != nil {
t.Fatalf("open gorm db: %v", err)
}
defer gdb.Close()
mock.ExpectBegin()
tx := gdb.Begin()
if err := tx.Error; err != nil {
t.Fatalf("begin tx: %v", err)
}
mock.ExpectRollback()
volumeDao := &volumeDeleteTenantServiceVolumeDao{
volume: &dbmodel.TenantServiceVolume{
Model: dbmodel.Model{ID: 7},
ServiceID: "component-a",
VolumeName: "shared-data",
VolumePath: "/data",
VolumeType: dbmodel.ShareFileVolumeType.String(),
},
}
mountRelationDao := &volumeDeleteMountRelationDao{
relations: []*dbmodel.TenantServiceMountRelation{
{
ServiceID: "component-b",
DependServiceID: "component-a",
VolumeName: "shared-data",
},
},
}
configFileDao := &volumeDeleteConfigFileDao{}
db.SetTestManager(volumeDeleteSharedMountTestManager{
tx: tx,
volumeDao: volumeDao,
mountRelationDao: mountRelationDao,
configFileDao: configFileDao,
})
defer db.SetTestManager(nil)
mq := &recordingMQClient{}
apiErr := (&ServiceAction{
MQClient: mq,
hotunplugVMDataDiskHook: func(volume *dbmodel.TenantServiceVolume) error {
t.Fatal("did not expect hotunplug when volume is shared mounted")
return nil
},
syncVirtualMachineSpecHook: func(serviceID string) error {
t.Fatal("did not expect vm spec sync when volume is shared mounted")
return nil
},
}).VolumnVar(&dbmodel.TenantServiceVolume{
ServiceID: "component-a",
VolumeName: "shared-data",
}, "tenant-1", "", "delete")
if apiErr == nil {
t.Fatal("expected shared mounted volume deletion to be rejected")
}
if apiErr.Code != 400 {
t.Fatalf("expected 400 error, got %d: %v", apiErr.Code, apiErr)
}
if !strings.Contains(apiErr.Error(), "共享挂载") {
t.Fatalf("expected shared mount error, got %q", apiErr.Error())
}
if volumeDao.deleted {
t.Fatal("did not expect volume delete to be called")
}
if configFileDao.deleted {
t.Fatal("did not expect config file cleanup to be called")
}
if len(mq.tasks) != 0 {
t.Fatalf("did not expect volume_gc task, got %d", len(mq.tasks))
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Fatalf("unmet sql expectations: %v", err)
}
}