github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/provider/oci/storage_volumes_integration_test.go (about)

     1  // Copyright 2018 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package oci_test
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  	"net/http"
    10  
    11  	"github.com/juju/names/v5"
    12  	jc "github.com/juju/testing/checkers"
    13  	ociCore "github.com/oracle/oci-go-sdk/v65/core"
    14  	gc "gopkg.in/check.v1"
    15  
    16  	"github.com/juju/juju/core/instance"
    17  	envcontext "github.com/juju/juju/environs/context"
    18  	"github.com/juju/juju/environs/tags"
    19  	"github.com/juju/juju/provider/oci"
    20  	"github.com/juju/juju/storage"
    21  )
    22  
    23  type storageVolumeSuite struct {
    24  	commonSuite
    25  
    26  	provider   storage.Provider
    27  	environCtx envcontext.ProviderCallContext
    28  }
    29  
    30  var _ = gc.Suite(&storageVolumeSuite{})
    31  
    32  func (s *storageVolumeSuite) SetUpTest(c *gc.C) {
    33  	s.commonSuite.SetUpTest(c)
    34  
    35  	s.environCtx = envcontext.NewEmptyCloudCallContext()
    36  	var err error
    37  	s.provider, err = s.env.StorageProvider(oci.OciStorageProviderType)
    38  	c.Assert(err, gc.IsNil)
    39  }
    40  
    41  func (s *storageVolumeSuite) newVolumeSource(c *gc.C) storage.VolumeSource {
    42  	cfg, err := storage.NewConfig("iscsi", oci.OciStorageProviderType,
    43  		map[string]interface{}{
    44  			oci.OciVolumeType: oci.IscsiPool,
    45  		})
    46  	c.Assert(err, gc.IsNil)
    47  	c.Assert(cfg, gc.NotNil)
    48  
    49  	source, err := s.provider.VolumeSource(cfg)
    50  	c.Assert(err, gc.IsNil)
    51  	return source
    52  }
    53  
    54  func (s *storageVolumeSuite) setupCreateVolumesExpectations(tag names.VolumeTag, size int64) {
    55  	name := tag.String()
    56  	volTags := map[string]string{
    57  		tags.JujuModel: s.env.Config().UUID(),
    58  	}
    59  
    60  	volume := ociCore.Volume{
    61  		AvailabilityDomain: makeStringPointer("fakeZone1"),
    62  		CompartmentId:      &s.testCompartment,
    63  		Id:                 makeStringPointer("fakeVolumeId"),
    64  		LifecycleState:     ociCore.VolumeLifecycleStateProvisioning,
    65  		FreeformTags:       volTags,
    66  		SizeInGBs:          &size,
    67  	}
    68  
    69  	requestDetails := ociCore.CreateVolumeDetails{
    70  		AvailabilityDomain: makeStringPointer("fakeZone1"),
    71  		CompartmentId:      &s.testCompartment,
    72  		DisplayName:        &name,
    73  		SizeInMBs:          &size,
    74  		FreeformTags:       volTags,
    75  	}
    76  
    77  	request := ociCore.CreateVolumeRequest{
    78  		CreateVolumeDetails: requestDetails,
    79  	}
    80  
    81  	response := ociCore.CreateVolumeResponse{
    82  		RawResponse: &http.Response{
    83  			StatusCode: 200,
    84  		},
    85  		Volume: volume,
    86  	}
    87  
    88  	volumeAvailable := volume
    89  	volumeAvailable.LifecycleState = ociCore.VolumeLifecycleStateAvailable
    90  
    91  	getVolumeRequest := ociCore.GetVolumeRequest{VolumeId: volumeAvailable.Id}
    92  	getVolumeResponse := ociCore.GetVolumeResponse{
    93  		Volume: volumeAvailable,
    94  	}
    95  	s.storage.EXPECT().CreateVolume(context.Background(), request).Return(response, nil)
    96  	s.storage.EXPECT().GetVolume(context.Background(), getVolumeRequest).Return(getVolumeResponse, nil).AnyTimes()
    97  
    98  }
    99  
   100  func (s *storageVolumeSuite) TestCreateVolumes(c *gc.C) {
   101  	ctrl := s.patchEnv(c)
   102  	defer ctrl.Finish()
   103  
   104  	source := s.newVolumeSource(c)
   105  	volumeTag := names.NewVolumeTag("1")
   106  	s.setupListInstancesExpectations(s.testInstanceID, ociCore.InstanceLifecycleStateRunning, 0)
   107  	s.setupCreateVolumesExpectations(volumeTag, 61440)
   108  
   109  	results, err := source.CreateVolumes(s.environCtx, []storage.VolumeParams{
   110  		{
   111  			Size:     uint64(61440),
   112  			Tag:      names.NewVolumeTag("1"),
   113  			Provider: oci.OciStorageProviderType,
   114  			Attachment: &storage.VolumeAttachmentParams{
   115  				AttachmentParams: storage.AttachmentParams{
   116  					InstanceId: instance.Id(s.testInstanceID),
   117  				},
   118  			},
   119  		},
   120  	})
   121  	c.Assert(err, gc.IsNil)
   122  	c.Assert(results, gc.HasLen, 1)
   123  	c.Assert(results[0].Error, jc.ErrorIsNil)
   124  }
   125  
   126  func (s *storageVolumeSuite) TestCreateVolumesInvalidSize(c *gc.C) {
   127  	source := s.newVolumeSource(c)
   128  	results, err := source.CreateVolumes(s.environCtx, []storage.VolumeParams{
   129  		{
   130  			Size:     uint64(2048),
   131  			Tag:      names.NewVolumeTag("1"),
   132  			Provider: oci.OciStorageProviderType,
   133  			Attachment: &storage.VolumeAttachmentParams{
   134  				AttachmentParams: storage.AttachmentParams{
   135  					InstanceId: instance.Id(s.testInstanceID),
   136  				},
   137  			},
   138  		},
   139  	})
   140  	c.Assert(err, gc.IsNil)
   141  	c.Assert(results, gc.HasLen, 1)
   142  	c.Check(results[0].Error, gc.ErrorMatches, "invalid volume size 2. Valid range is.*")
   143  }
   144  
   145  func (s *storageVolumeSuite) TestCreateVolumesNilParams(c *gc.C) {
   146  	source := s.newVolumeSource(c)
   147  	results, err := source.CreateVolumes(s.environCtx, nil)
   148  	c.Assert(err, gc.IsNil)
   149  	c.Assert(results, gc.HasLen, 0)
   150  }
   151  
   152  func (s *storageVolumeSuite) setupListVolumesExpectations(size int64) map[string]ociCore.Volume {
   153  	volTags := map[string]string{
   154  		tags.JujuModel: s.env.Config().UUID(),
   155  	}
   156  	volumes := []ociCore.Volume{
   157  		{
   158  			AvailabilityDomain: makeStringPointer("fakeZone1"),
   159  			CompartmentId:      &s.testCompartment,
   160  			Id:                 makeStringPointer("fakeVolumeId"),
   161  			LifecycleState:     ociCore.VolumeLifecycleStateAvailable,
   162  			FreeformTags:       volTags,
   163  			SizeInGBs:          &size,
   164  		},
   165  		{
   166  			AvailabilityDomain: makeStringPointer("fakeZone1"),
   167  			CompartmentId:      &s.testCompartment,
   168  			Id:                 makeStringPointer("fakeVolumeId2"),
   169  			LifecycleState:     ociCore.VolumeLifecycleStateAvailable,
   170  			FreeformTags:       volTags,
   171  			SizeInGBs:          &size,
   172  		},
   173  	}
   174  
   175  	s.storage.EXPECT().ListVolumes(context.Background(), &s.testCompartment).Return(volumes, nil).AnyTimes()
   176  	asMap := map[string]ociCore.Volume{}
   177  	for _, vol := range volumes {
   178  		asMap[*vol.Id] = vol
   179  	}
   180  	return asMap
   181  }
   182  
   183  func (s *storageVolumeSuite) TestListVolumes(c *gc.C) {
   184  	ctrl := s.patchEnv(c)
   185  	defer ctrl.Finish()
   186  
   187  	s.setupListVolumesExpectations(60)
   188  
   189  	source := s.newVolumeSource(c)
   190  
   191  	volumes, err := source.ListVolumes(s.environCtx)
   192  	c.Assert(err, gc.IsNil)
   193  	c.Assert(len(volumes), gc.Equals, 2)
   194  	c.Assert(volumes, jc.SameContents, []string{"fakeVolumeId", "fakeVolumeId2"})
   195  }
   196  
   197  func (s *storageVolumeSuite) TestDescribeVolumes(c *gc.C) {
   198  	ctrl := s.patchEnv(c)
   199  	defer ctrl.Finish()
   200  
   201  	s.setupListVolumesExpectations(60)
   202  
   203  	source := s.newVolumeSource(c)
   204  
   205  	results, err := source.DescribeVolumes(s.environCtx, []string{"fakeVolumeId"})
   206  	c.Assert(err, gc.IsNil)
   207  	c.Assert(len(results), gc.Equals, 1)
   208  	c.Assert(results[0].VolumeInfo.VolumeId, gc.Equals, "fakeVolumeId")
   209  	c.Assert(results[0].VolumeInfo.Size, gc.Equals, uint64(60*1024))
   210  	c.Assert(results[0].VolumeInfo.Persistent, gc.Equals, true)
   211  
   212  	results, err = source.DescribeVolumes(s.environCtx, []string{"fakeVolumeId", "fakeVolumeId2"})
   213  	c.Assert(err, gc.IsNil)
   214  	c.Assert(len(results), gc.Equals, 2)
   215  
   216  	results, err = source.DescribeVolumes(s.environCtx, []string{"IDontExist", "fakeVolumeId2"})
   217  	c.Assert(err, gc.IsNil)
   218  	c.Assert(len(results), gc.Equals, 2)
   219  	c.Assert(results[0].Error, gc.NotNil)
   220  	c.Assert(results[1].Error, gc.IsNil)
   221  }
   222  
   223  func (s *storageVolumeSuite) TestValidateVolumeParams(c *gc.C) {
   224  	source := s.newVolumeSource(c)
   225  	params := storage.VolumeParams{
   226  		Size:     uint64(2048),
   227  		Tag:      names.NewVolumeTag("1"),
   228  		Provider: oci.OciStorageProviderType,
   229  		Attachment: &storage.VolumeAttachmentParams{
   230  			AttachmentParams: storage.AttachmentParams{
   231  				InstanceId: instance.Id(s.testInstanceID),
   232  			},
   233  		},
   234  	}
   235  
   236  	err := source.ValidateVolumeParams(params)
   237  	c.Assert(err, gc.ErrorMatches, "invalid volume size 2. Valid range is.*")
   238  
   239  	params.Size = 61440
   240  	err = source.ValidateVolumeParams(params)
   241  	c.Assert(err, gc.IsNil)
   242  }
   243  
   244  func (s *storageVolumeSuite) setupDeleteVolumesExpectations(size int64, id string) {
   245  	volumes := s.setupListVolumesExpectations(size)
   246  
   247  	request := ociCore.DeleteVolumeRequest{
   248  		VolumeId: &id,
   249  	}
   250  	terminatedVol := volumes[id]
   251  	terminatedVol.LifecycleState = ociCore.VolumeLifecycleStateTerminated
   252  	response := ociCore.DeleteVolumeResponse{
   253  		RawResponse: &http.Response{
   254  			StatusCode: 200,
   255  		},
   256  	}
   257  	s.storage.EXPECT().DeleteVolume(context.Background(), request).Return(response, nil).AnyTimes()
   258  
   259  	getVolumeRequest := ociCore.GetVolumeRequest{VolumeId: terminatedVol.Id}
   260  	getVolumeResponse := ociCore.GetVolumeResponse{
   261  		Volume: terminatedVol,
   262  	}
   263  	s.storage.EXPECT().GetVolume(context.Background(), getVolumeRequest).Return(getVolumeResponse, nil).AnyTimes()
   264  }
   265  
   266  func (s *storageVolumeSuite) TestDestroyVolumes(c *gc.C) {
   267  	ctrl := s.patchEnv(c)
   268  	defer ctrl.Finish()
   269  
   270  	s.setupDeleteVolumesExpectations(60, "fakeVolumeId")
   271  
   272  	source := s.newVolumeSource(c)
   273  
   274  	results, err := source.DestroyVolumes(s.environCtx, []string{"fakeVolumeId"})
   275  	c.Assert(err, gc.IsNil)
   276  	c.Assert(len(results), gc.Equals, 1)
   277  	c.Assert(results[0], gc.IsNil)
   278  
   279  	results, err = source.DestroyVolumes(s.environCtx, []string{"bogusId"})
   280  	c.Assert(err, gc.IsNil)
   281  	c.Assert(len(results), gc.Equals, 1)
   282  	c.Assert(results[0], gc.ErrorMatches, "no such volume.*")
   283  }
   284  
   285  func (s *storageVolumeSuite) setupUpdateVolumesExpectations(id string) {
   286  	volumes := s.setupListVolumesExpectations(60)
   287  	vol := volumes[id]
   288  	volTags := map[string]string{
   289  		tags.JujuModel: "",
   290  	}
   291  
   292  	requestDetails := ociCore.UpdateVolumeDetails{
   293  		FreeformTags: volTags,
   294  	}
   295  	request := ociCore.UpdateVolumeRequest{
   296  		UpdateVolumeDetails: requestDetails,
   297  		VolumeId:            vol.Id,
   298  	}
   299  	s.storage.EXPECT().UpdateVolume(context.Background(), request).Return(ociCore.UpdateVolumeResponse{}, nil).AnyTimes()
   300  }
   301  
   302  func (s *storageVolumeSuite) TestReleaseVolumes(c *gc.C) {
   303  	ctrl := s.patchEnv(c)
   304  	defer ctrl.Finish()
   305  
   306  	s.setupUpdateVolumesExpectations("fakeVolumeId")
   307  	source := s.newVolumeSource(c)
   308  
   309  	results, err := source.ReleaseVolumes(s.environCtx, []string{"fakeVolumeId"})
   310  	c.Assert(err, gc.IsNil)
   311  	c.Assert(len(results), gc.Equals, 1)
   312  	c.Assert(results[0], gc.IsNil)
   313  
   314  	results, err = source.ReleaseVolumes(s.environCtx, []string{"IAmNotHereWhatIsHereIsntHereJustThereButWithoutTheT"})
   315  	c.Assert(err, gc.IsNil)
   316  	c.Assert(len(results), gc.Equals, 1)
   317  	c.Assert(results[0], gc.ErrorMatches, "no such volume.*")
   318  }
   319  
   320  func (s *storageVolumeSuite) setupGetInstanceExpectations(instance string, state ociCore.InstanceLifecycleStateEnum) {
   321  	requestMachine1, responseMachine1 := makeGetInstanceRequestResponse(
   322  		ociCore.Instance{
   323  			AvailabilityDomain: makeStringPointer("fakeZone1"),
   324  			CompartmentId:      &s.testCompartment,
   325  			Id:                 makeStringPointer(instance),
   326  			LifecycleState:     state,
   327  			Region:             makeStringPointer("us-phoenix-1"),
   328  			Shape:              makeStringPointer("VM.Standard1.1"),
   329  			DisplayName:        makeStringPointer("fakeName"),
   330  			FreeformTags:       s.tags,
   331  		},
   332  	)
   333  	s.compute.EXPECT().GetInstance(
   334  		context.Background(), requestMachine1).Return(
   335  		responseMachine1, nil).AnyTimes()
   336  }
   337  
   338  func (s *storageVolumeSuite) makeListVolumeAttachmentExpectations(instance string, volumeId string, returnEmpty bool, times int) {
   339  
   340  	port := 3260
   341  	response := []ociCore.VolumeAttachment{}
   342  
   343  	if returnEmpty == false {
   344  		response = []ociCore.VolumeAttachment{
   345  			ociCore.IScsiVolumeAttachment{
   346  				AvailabilityDomain: makeStringPointer("fakeZone1"),
   347  				InstanceId:         &instance,
   348  				CompartmentId:      &s.testCompartment,
   349  				Iqn:                makeStringPointer("bogus"),
   350  				Id:                 makeStringPointer("fakeVolumeAttachment1"),
   351  				VolumeId:           &volumeId,
   352  				Ipv4:               makeStringPointer("192.168.1.1"),
   353  				Port:               &port,
   354  				DisplayName:        makeStringPointer("fakeVolumeAttachment"),
   355  				ChapSecret:         makeStringPointer("superSecretPassword"),
   356  				ChapUsername:       makeStringPointer("JohnDoe"),
   357  				LifecycleState:     ociCore.VolumeAttachmentLifecycleStateAttached,
   358  			},
   359  		}
   360  	}
   361  	expect := s.compute.EXPECT().ListVolumeAttachments(context.Background(), &s.testCompartment, &instance).Return(response, nil)
   362  	if times == 0 {
   363  		expect.AnyTimes()
   364  	} else {
   365  		expect.Times(times)
   366  	}
   367  }
   368  
   369  func (s *storageVolumeSuite) TestAttachVolumeWithExistingAttachment(c *gc.C) {
   370  	ctrl := s.patchEnv(c)
   371  	defer ctrl.Finish()
   372  
   373  	volumeId := "fakeVolumeId"
   374  	s.setupListInstancesExpectations(s.testInstanceID, ociCore.InstanceLifecycleStateRunning, 0)
   375  	s.setupGetInstanceExpectations(s.testInstanceID, ociCore.InstanceLifecycleStateRunning)
   376  	s.makeListVolumeAttachmentExpectations(s.testInstanceID, volumeId, false, 0)
   377  
   378  	source := s.newVolumeSource(c)
   379  
   380  	result, err := source.AttachVolumes(s.environCtx, []storage.VolumeAttachmentParams{
   381  		{
   382  			AttachmentParams: storage.AttachmentParams{
   383  				Provider:   oci.OciStorageProviderType,
   384  				InstanceId: instance.Id(s.testInstanceID),
   385  				ReadOnly:   false,
   386  				Machine:    names.NewMachineTag("1"),
   387  			},
   388  			VolumeId: volumeId,
   389  			Volume:   names.NewVolumeTag("1"),
   390  		},
   391  	})
   392  	c.Assert(err, gc.IsNil)
   393  	c.Assert(len(result), gc.Equals, 1)
   394  	c.Assert(result[0].Error, gc.IsNil)
   395  	planInfo := result[0].VolumeAttachment.VolumeAttachmentInfo.PlanInfo
   396  	c.Assert(planInfo.DeviceAttributes["iqn"], gc.Equals, "bogus")
   397  	c.Assert(planInfo.DeviceAttributes["address"], gc.Equals, "192.168.1.1")
   398  	c.Assert(planInfo.DeviceAttributes["port"], gc.Equals, "3260")
   399  	c.Assert(planInfo.DeviceAttributes["chap-user"], gc.Equals, "JohnDoe")
   400  	c.Assert(planInfo.DeviceAttributes["chap-secret"], gc.Equals, "superSecretPassword")
   401  
   402  }
   403  
   404  func (s *storageVolumeSuite) TestAttachVolumeWithInvalidInstanceState(c *gc.C) {
   405  	ctrl := s.patchEnv(c)
   406  	defer ctrl.Finish()
   407  
   408  	volumeId := "fakeVolumeId"
   409  	s.setupListInstancesExpectations(s.testInstanceID, ociCore.InstanceLifecycleStateTerminated, 0)
   410  
   411  	source := s.newVolumeSource(c)
   412  
   413  	result, err := source.AttachVolumes(s.environCtx, []storage.VolumeAttachmentParams{
   414  		{
   415  			AttachmentParams: storage.AttachmentParams{
   416  				Provider:   oci.OciStorageProviderType,
   417  				InstanceId: instance.Id(s.testInstanceID),
   418  				ReadOnly:   false,
   419  				Machine:    names.NewMachineTag("1"),
   420  			},
   421  			VolumeId: volumeId,
   422  			Volume:   names.NewVolumeTag("1"),
   423  		},
   424  	})
   425  	c.Assert(err, gc.IsNil)
   426  	c.Assert(len(result), gc.Equals, 1)
   427  	c.Assert(result[0].Error, gc.ErrorMatches, "invalid instance state for volume attachment:.*")
   428  }
   429  
   430  func (s *storageVolumeSuite) setupAttachNewVolumeExpectations(instance, volumeId, attachmentId string) {
   431  	useChap := true
   432  	displayName := fmt.Sprintf("%s_%s", instance, volumeId)
   433  	attachDetails := ociCore.AttachIScsiVolumeDetails{
   434  		InstanceId:  &instance,
   435  		VolumeId:    &volumeId,
   436  		UseChap:     &useChap,
   437  		DisplayName: &displayName,
   438  	}
   439  	request := ociCore.AttachVolumeRequest{
   440  		AttachVolumeDetails: attachDetails,
   441  	}
   442  
   443  	attachment := s.getVolumeAttachmentTemplate(instance, volumeId, attachmentId)
   444  	attachment.LifecycleState = ociCore.VolumeAttachmentLifecycleStateAttaching
   445  	response := ociCore.AttachVolumeResponse{
   446  		RawResponse: &http.Response{
   447  			StatusCode: 200,
   448  		},
   449  		VolumeAttachment: attachment,
   450  	}
   451  	s.compute.EXPECT().AttachVolume(context.Background(), request).Return(response, nil)
   452  
   453  }
   454  
   455  func (s *storageVolumeSuite) getVolumeAttachmentTemplate(instance, volume, attachment string) ociCore.IScsiVolumeAttachment {
   456  	port := 3260
   457  	return ociCore.IScsiVolumeAttachment{
   458  		AvailabilityDomain: makeStringPointer("fakeZone1"),
   459  		InstanceId:         &instance,
   460  		CompartmentId:      &s.testCompartment,
   461  		Iqn:                makeStringPointer("bogus"),
   462  		Id:                 &attachment,
   463  		VolumeId:           &volume,
   464  		Ipv4:               makeStringPointer("192.168.1.1"),
   465  		Port:               &port,
   466  		DisplayName:        makeStringPointer("fakeVolumeAttachment"),
   467  		ChapSecret:         makeStringPointer("superSecretPassword"),
   468  		ChapUsername:       makeStringPointer("JohnDoe"),
   469  		LifecycleState:     ociCore.VolumeAttachmentLifecycleStateAttaching,
   470  	}
   471  }
   472  
   473  func (s *storageVolumeSuite) setupGetVolumeAttachmentExpectations(
   474  	instance, volumeId, attachmentId string, state ociCore.VolumeAttachmentLifecycleStateEnum) {
   475  	request := ociCore.GetVolumeAttachmentRequest{
   476  		VolumeAttachmentId: &attachmentId,
   477  	}
   478  	attachment := s.getVolumeAttachmentTemplate(instance, volumeId, attachmentId)
   479  	attachment.LifecycleState = state
   480  	response := ociCore.GetVolumeAttachmentResponse{
   481  		VolumeAttachment: attachment,
   482  	}
   483  	s.compute.EXPECT().GetVolumeAttachment(context.Background(), request).Return(response, nil).AnyTimes()
   484  }
   485  
   486  func (s *storageVolumeSuite) TestAttachVolume(c *gc.C) {
   487  	ctrl := s.patchEnv(c)
   488  	defer ctrl.Finish()
   489  
   490  	volumeId := "fakeVolumeId"
   491  	attachId := "fakeVolumeAttachmentId"
   492  	s.setupListInstancesExpectations(s.testInstanceID, ociCore.InstanceLifecycleStateRunning, 0)
   493  	s.setupGetInstanceExpectations(s.testInstanceID, ociCore.InstanceLifecycleStateRunning)
   494  	s.makeListVolumeAttachmentExpectations(s.testInstanceID, volumeId, true, 1)
   495  	s.setupAttachNewVolumeExpectations(s.testInstanceID, volumeId, attachId)
   496  	s.setupGetVolumeAttachmentExpectations(
   497  		s.testInstanceID, volumeId, attachId,
   498  		ociCore.VolumeAttachmentLifecycleStateAttached)
   499  
   500  	source := s.newVolumeSource(c)
   501  
   502  	result, err := source.AttachVolumes(s.environCtx, []storage.VolumeAttachmentParams{
   503  		{
   504  			AttachmentParams: storage.AttachmentParams{
   505  				Provider:   oci.OciStorageProviderType,
   506  				InstanceId: instance.Id(s.testInstanceID),
   507  				ReadOnly:   false,
   508  				Machine:    names.NewMachineTag("1"),
   509  			},
   510  			VolumeId: volumeId,
   511  			Volume:   names.NewVolumeTag("1"),
   512  		},
   513  	})
   514  	c.Assert(err, gc.IsNil)
   515  	c.Assert(len(result), gc.Equals, 1)
   516  	c.Assert(result[0].Error, gc.IsNil)
   517  }
   518  
   519  func (s *storageVolumeSuite) setupDetachVolumesExpectations(attachmentId string) {
   520  	request := ociCore.DetachVolumeRequest{
   521  		VolumeAttachmentId: &attachmentId,
   522  	}
   523  	response := ociCore.DetachVolumeResponse{
   524  		RawResponse: &http.Response{
   525  			StatusCode: 200,
   526  		},
   527  	}
   528  	s.compute.EXPECT().DetachVolume(context.Background(), request).Return(response, nil).AnyTimes()
   529  }
   530  
   531  func (s *storageVolumeSuite) TestDetachVolume(c *gc.C) {
   532  	ctrl := s.patchEnv(c)
   533  	defer ctrl.Finish()
   534  
   535  	volumeId := "fakeVolumeId"
   536  	attachId := "fakeVolumeAttachment1"
   537  	s.setupListInstancesExpectations(s.testInstanceID, ociCore.InstanceLifecycleStateRunning, 0)
   538  	s.makeListVolumeAttachmentExpectations(s.testInstanceID, volumeId, false, 1)
   539  	s.setupDetachVolumesExpectations(attachId)
   540  	s.setupGetVolumeAttachmentExpectations(
   541  		s.testInstanceID, volumeId, attachId,
   542  		ociCore.VolumeAttachmentLifecycleStateDetached)
   543  
   544  	source := s.newVolumeSource(c)
   545  
   546  	result, err := source.DetachVolumes(s.environCtx, []storage.VolumeAttachmentParams{
   547  		{
   548  			AttachmentParams: storage.AttachmentParams{
   549  				Provider:   oci.OciStorageProviderType,
   550  				InstanceId: instance.Id(s.testInstanceID),
   551  				ReadOnly:   false,
   552  				Machine:    names.NewMachineTag("1"),
   553  			},
   554  			VolumeId: volumeId,
   555  			Volume:   names.NewVolumeTag("1"),
   556  		},
   557  	})
   558  
   559  	c.Assert(err, gc.IsNil)
   560  	c.Assert(len(result), gc.Equals, 1)
   561  }