github.com/pf-qiu/concourse/v6@v6.7.3-0.20201207032516-1f455d73275f/atc/db/volume.go (about)

     1  package db
     2  
     3  import (
     4  	"database/sql"
     5  	"encoding/json"
     6  	"errors"
     7  	"fmt"
     8  
     9  	sq "github.com/Masterminds/squirrel"
    10  	"github.com/pf-qiu/concourse/v6/atc"
    11  	"github.com/lib/pq"
    12  	uuid "github.com/nu7hatch/gouuid"
    13  )
    14  
    15  var (
    16  	ErrVolumeCannotBeDestroyedWithChildrenPresent = errors.New("volume cannot be destroyed as children are present")
    17  	ErrVolumeStateTransitionFailed                = errors.New("could not transition volume state")
    18  	ErrVolumeMissing                              = errors.New("volume no longer in db")
    19  	ErrInvalidResourceCache                       = errors.New("invalid resource cache")
    20  )
    21  
    22  type ErrVolumeMarkStateFailed struct {
    23  	State VolumeState
    24  }
    25  
    26  func (e ErrVolumeMarkStateFailed) Error() string {
    27  	return fmt.Sprintf("could not mark volume as %s", e.State)
    28  }
    29  
    30  type ErrVolumeMarkCreatedFailed struct {
    31  	Handle string
    32  }
    33  
    34  func (e ErrVolumeMarkCreatedFailed) Error() string {
    35  	return fmt.Sprintf("failed to mark volume as created %s", e.Handle)
    36  }
    37  
    38  type VolumeState string
    39  
    40  const (
    41  	VolumeStateCreating   VolumeState = "creating"
    42  	VolumeStateCreated    VolumeState = "created"
    43  	VolumeStateDestroying VolumeState = "destroying"
    44  	VolumeStateFailed     VolumeState = "failed"
    45  )
    46  
    47  type VolumeType string
    48  
    49  const (
    50  	VolumeTypeContainer     VolumeType = "container"
    51  	VolumeTypeResource      VolumeType = "resource"
    52  	VolumeTypeResourceType  VolumeType = "resource-type"
    53  	VolumeTypeResourceCerts VolumeType = "resource-certs"
    54  	VolumeTypeTaskCache     VolumeType = "task-cache"
    55  	VolumeTypeArtifact      VolumeType = "artifact"
    56  	VolumeTypeUknown        VolumeType = "unknown" // for migration to life
    57  )
    58  
    59  //go:generate counterfeiter . CreatingVolume
    60  
    61  type CreatingVolume interface {
    62  	Handle() string
    63  	ID() int
    64  	Created() (CreatedVolume, error)
    65  	Failed() (FailedVolume, error)
    66  }
    67  
    68  type creatingVolume struct {
    69  	id                       int
    70  	workerName               string
    71  	handle                   string
    72  	path                     string
    73  	teamID                   int
    74  	typ                      VolumeType
    75  	containerHandle          string
    76  	parentHandle             string
    77  	resourceCacheID          int
    78  	workerBaseResourceTypeID int
    79  	workerTaskCacheID        int
    80  	workerResourceCertsID    int
    81  	workerArtifactID         int
    82  	conn                     Conn
    83  }
    84  
    85  func (volume *creatingVolume) ID() int { return volume.id }
    86  
    87  func (volume *creatingVolume) Handle() string { return volume.handle }
    88  
    89  func (volume *creatingVolume) Created() (CreatedVolume, error) {
    90  	err := volumeStateTransition(
    91  		volume.id,
    92  		volume.conn,
    93  		VolumeStateCreating,
    94  		VolumeStateCreated,
    95  	)
    96  	if err != nil {
    97  		if err == ErrVolumeStateTransitionFailed {
    98  			return nil, ErrVolumeMarkCreatedFailed{Handle: volume.handle}
    99  		}
   100  		return nil, err
   101  	}
   102  
   103  	return &createdVolume{
   104  		id:                       volume.id,
   105  		workerName:               volume.workerName,
   106  		typ:                      volume.typ,
   107  		handle:                   volume.handle,
   108  		path:                     volume.path,
   109  		teamID:                   volume.teamID,
   110  		conn:                     volume.conn,
   111  		containerHandle:          volume.containerHandle,
   112  		parentHandle:             volume.parentHandle,
   113  		resourceCacheID:          volume.resourceCacheID,
   114  		workerBaseResourceTypeID: volume.workerBaseResourceTypeID,
   115  		workerTaskCacheID:        volume.workerTaskCacheID,
   116  		workerResourceCertsID:    volume.workerResourceCertsID,
   117  	}, nil
   118  }
   119  
   120  func (volume *creatingVolume) Failed() (FailedVolume, error) {
   121  	err := volumeStateTransition(
   122  		volume.id,
   123  		volume.conn,
   124  		VolumeStateCreating,
   125  		VolumeStateFailed,
   126  	)
   127  	if err != nil {
   128  		if err == ErrVolumeStateTransitionFailed {
   129  			return nil, ErrVolumeMarkStateFailed{VolumeStateFailed}
   130  		}
   131  		return nil, err
   132  	}
   133  
   134  	return &failedVolume{
   135  		id:         volume.id,
   136  		workerName: volume.workerName,
   137  		handle:     volume.handle,
   138  		conn:       volume.conn,
   139  	}, nil
   140  }
   141  
   142  //go:generate counterfeiter . CreatedVolume
   143  // TODO-Later Consider separating CORE & Runtime concerns by breaking this abstraction up.
   144  type CreatedVolume interface {
   145  	Handle() string
   146  	Path() string
   147  	Type() VolumeType
   148  	TeamID() int
   149  	WorkerArtifactID() int
   150  	CreateChildForContainer(CreatingContainer, string) (CreatingVolume, error)
   151  	Destroying() (DestroyingVolume, error)
   152  	WorkerName() string
   153  
   154  	InitializeResourceCache(UsedResourceCache) error
   155  	GetResourceCacheID() int
   156  	InitializeArtifact(name string, buildID int) (WorkerArtifact, error)
   157  	InitializeTaskCache(jobID int, stepName string, path string) error
   158  
   159  	ContainerHandle() string
   160  	ParentHandle() string
   161  	ResourceType() (*VolumeResourceType, error)
   162  	BaseResourceType() (*UsedWorkerBaseResourceType, error)
   163  	TaskIdentifier() (int, atc.PipelineRef, string, string, error)
   164  }
   165  
   166  type createdVolume struct {
   167  	id                       int
   168  	workerName               string
   169  	handle                   string
   170  	path                     string
   171  	teamID                   int
   172  	typ                      VolumeType
   173  	containerHandle          string
   174  	parentHandle             string
   175  	resourceCacheID          int
   176  	workerBaseResourceTypeID int
   177  	workerTaskCacheID        int
   178  	workerResourceCertsID    int
   179  	workerArtifactID         int
   180  	conn                     Conn
   181  }
   182  
   183  type VolumeResourceType struct {
   184  	WorkerBaseResourceType *UsedWorkerBaseResourceType
   185  	ResourceType           *VolumeResourceType
   186  	Version                atc.Version
   187  }
   188  
   189  func (volume *createdVolume) Handle() string          { return volume.handle }
   190  func (volume *createdVolume) Path() string            { return volume.path }
   191  func (volume *createdVolume) WorkerName() string      { return volume.workerName }
   192  func (volume *createdVolume) Type() VolumeType        { return volume.typ }
   193  func (volume *createdVolume) TeamID() int             { return volume.teamID }
   194  func (volume *createdVolume) ContainerHandle() string { return volume.containerHandle }
   195  func (volume *createdVolume) ParentHandle() string    { return volume.parentHandle }
   196  func (volume *createdVolume) WorkerArtifactID() int   { return volume.workerArtifactID }
   197  
   198  func (volume *createdVolume) ResourceType() (*VolumeResourceType, error) {
   199  	if volume.resourceCacheID == 0 {
   200  		return nil, nil
   201  	}
   202  
   203  	return volume.findVolumeResourceTypeByCacheID(volume.resourceCacheID)
   204  }
   205  
   206  func (volume *createdVolume) BaseResourceType() (*UsedWorkerBaseResourceType, error) {
   207  	if volume.workerBaseResourceTypeID == 0 {
   208  		return nil, nil
   209  	}
   210  
   211  	return volume.findWorkerBaseResourceTypeByID(volume.workerBaseResourceTypeID)
   212  }
   213  
   214  func (volume *createdVolume) TaskIdentifier() (int, atc.PipelineRef, string, string, error) {
   215  	if volume.workerTaskCacheID == 0 {
   216  		return 0, atc.PipelineRef{}, "", "", nil
   217  	}
   218  
   219  	var pipelineID int
   220  	var pipelineName string
   221  	var pipelineInstanceVars sql.NullString
   222  	var jobName string
   223  	var stepName string
   224  
   225  	err := psql.Select("p.id, p.name, p.instance_vars, j.name, tc.step_name").
   226  		From("worker_task_caches wtc").
   227  		LeftJoin("task_caches tc on tc.id = wtc.task_cache_id").
   228  		LeftJoin("jobs j ON j.id = tc.job_id").
   229  		LeftJoin("pipelines p ON p.id = j.pipeline_id").
   230  		Where(sq.Eq{
   231  			"wtc.id": volume.workerTaskCacheID,
   232  		}).
   233  		RunWith(volume.conn).
   234  		QueryRow().
   235  		Scan(&pipelineID, &pipelineName, &pipelineInstanceVars, &jobName, &stepName)
   236  	if err != nil {
   237  		return 0, atc.PipelineRef{}, "", "", err
   238  	}
   239  
   240  	pipelineRef := atc.PipelineRef{Name: pipelineName}
   241  	if pipelineInstanceVars.Valid {
   242  		err = json.Unmarshal([]byte(pipelineInstanceVars.String), &pipelineRef.InstanceVars)
   243  		if err != nil {
   244  			return 0, atc.PipelineRef{}, "", "", err
   245  		}
   246  	}
   247  
   248  	return pipelineID, pipelineRef, jobName, stepName, nil
   249  }
   250  
   251  func (volume *createdVolume) findVolumeResourceTypeByCacheID(resourceCacheID int) (*VolumeResourceType, error) {
   252  	var versionString []byte
   253  	var sqBaseResourceTypeID sql.NullInt64
   254  	var sqResourceCacheID sql.NullInt64
   255  
   256  	err := psql.Select("rc.version, rcfg.base_resource_type_id, rcfg.resource_cache_id").
   257  		From("resource_caches rc").
   258  		LeftJoin("resource_configs rcfg ON rcfg.id = rc.resource_config_id").
   259  		Where(sq.Eq{
   260  			"rc.id": resourceCacheID,
   261  		}).
   262  		RunWith(volume.conn).
   263  		QueryRow().
   264  		Scan(&versionString, &sqBaseResourceTypeID, &sqResourceCacheID)
   265  	if err != nil {
   266  		return nil, err
   267  	}
   268  
   269  	var version atc.Version
   270  	err = json.Unmarshal(versionString, &version)
   271  	if err != nil {
   272  		return nil, err
   273  	}
   274  
   275  	if sqBaseResourceTypeID.Valid {
   276  		workerBaseResourceType, err := volume.findWorkerBaseResourceTypeByBaseResourceTypeID(int(sqBaseResourceTypeID.Int64))
   277  		if err != nil {
   278  			return nil, err
   279  		}
   280  
   281  		return &VolumeResourceType{
   282  			WorkerBaseResourceType: workerBaseResourceType,
   283  			Version:                version,
   284  		}, nil
   285  	}
   286  
   287  	if sqResourceCacheID.Valid {
   288  		resourceType, err := volume.findVolumeResourceTypeByCacheID(int(sqResourceCacheID.Int64))
   289  		if err != nil {
   290  			return nil, err
   291  		}
   292  
   293  		return &VolumeResourceType{
   294  			ResourceType: resourceType,
   295  			Version:      version,
   296  		}, nil
   297  	}
   298  
   299  	return nil, ErrInvalidResourceCache
   300  }
   301  
   302  func (volume *createdVolume) findWorkerBaseResourceTypeByID(workerBaseResourceTypeID int) (*UsedWorkerBaseResourceType, error) {
   303  	var name string
   304  	var version string
   305  
   306  	err := psql.Select("brt.name, wbrt.version").
   307  		From("worker_base_resource_types wbrt").
   308  		LeftJoin("base_resource_types brt ON brt.id = wbrt.base_resource_type_id").
   309  		Where(sq.Eq{
   310  			"wbrt.id":          workerBaseResourceTypeID,
   311  			"wbrt.worker_name": volume.workerName,
   312  		}).
   313  		RunWith(volume.conn).
   314  		QueryRow().
   315  		Scan(&name, &version)
   316  	if err != nil {
   317  		return nil, err
   318  	}
   319  
   320  	return &UsedWorkerBaseResourceType{
   321  		ID:         workerBaseResourceTypeID,
   322  		Name:       name,
   323  		Version:    version,
   324  		WorkerName: volume.workerName,
   325  	}, nil
   326  }
   327  
   328  func (volume *createdVolume) findWorkerBaseResourceTypeByBaseResourceTypeID(baseResourceTypeID int) (*UsedWorkerBaseResourceType, error) {
   329  	var id int
   330  	var name string
   331  	var version string
   332  
   333  	err := psql.Select("wbrt.id, brt.name, wbrt.version").
   334  		From("worker_base_resource_types wbrt").
   335  		LeftJoin("base_resource_types brt ON brt.id = wbrt.base_resource_type_id").
   336  		Where(sq.Eq{
   337  			"brt.id":           baseResourceTypeID,
   338  			"wbrt.worker_name": volume.workerName,
   339  		}).
   340  		RunWith(volume.conn).
   341  		QueryRow().
   342  		Scan(&id, &name, &version)
   343  	if err != nil {
   344  		return nil, err
   345  	}
   346  
   347  	return &UsedWorkerBaseResourceType{
   348  		ID:         id,
   349  		Name:       name,
   350  		Version:    version,
   351  		WorkerName: volume.workerName,
   352  	}, nil
   353  }
   354  
   355  func (volume *createdVolume) InitializeResourceCache(resourceCache UsedResourceCache) error {
   356  	tx, err := volume.conn.Begin()
   357  	if err != nil {
   358  		return err
   359  	}
   360  
   361  	defer tx.Rollback()
   362  
   363  	workerResourceCache, err := WorkerResourceCache{
   364  		WorkerName:    volume.WorkerName(),
   365  		ResourceCache: resourceCache,
   366  	}.FindOrCreate(tx)
   367  	if err != nil {
   368  		return err
   369  	}
   370  
   371  	rows, err := psql.Update("volumes").
   372  		Set("worker_resource_cache_id", workerResourceCache.ID).
   373  		Set("team_id", nil).
   374  		Where(sq.Eq{"id": volume.id}).
   375  		RunWith(tx).
   376  		Exec()
   377  	if err != nil {
   378  		if pqErr, ok := err.(*pq.Error); ok && pqErr.Code.Name() == pqUniqueViolationErrCode {
   379  			// another volume was 'blessed' as the cache volume - leave this one
   380  			// owned by the container so it just expires when the container is GCed
   381  			return nil
   382  		}
   383  
   384  		return err
   385  	}
   386  
   387  	affected, err := rows.RowsAffected()
   388  	if err != nil {
   389  		return err
   390  	}
   391  
   392  	if affected == 0 {
   393  		return ErrVolumeMissing
   394  	}
   395  
   396  	err = tx.Commit()
   397  	if err != nil {
   398  		return err
   399  	}
   400  
   401  	volume.resourceCacheID = resourceCache.ID()
   402  	volume.typ = VolumeTypeResource
   403  
   404  	return nil
   405  }
   406  
   407  func (volume *createdVolume) GetResourceCacheID() int {
   408  	return volume.resourceCacheID
   409  }
   410  
   411  func (volume *createdVolume) InitializeArtifact(name string, buildID int) (WorkerArtifact, error) {
   412  	tx, err := volume.conn.Begin()
   413  	if err != nil {
   414  		return nil, err
   415  	}
   416  
   417  	defer Rollback(tx)
   418  
   419  	atcWorkerArtifact := atc.WorkerArtifact{
   420  		Name:    name,
   421  		BuildID: buildID,
   422  	}
   423  
   424  	workerArtifact, err := saveWorkerArtifact(tx, volume.conn, atcWorkerArtifact)
   425  	if err != nil {
   426  		return nil, err
   427  	}
   428  
   429  	rows, err := psql.Update("volumes").
   430  		Set("worker_artifact_id", workerArtifact.ID()).
   431  		Where(sq.Eq{"id": volume.id}).
   432  		RunWith(tx).
   433  		Exec()
   434  	if err != nil {
   435  		return nil, err
   436  	}
   437  
   438  	affected, err := rows.RowsAffected()
   439  	if err != nil {
   440  		return nil, err
   441  	}
   442  
   443  	if affected == 0 {
   444  		return nil, ErrVolumeMissing
   445  	}
   446  
   447  	err = tx.Commit()
   448  	if err != nil {
   449  		return nil, err
   450  	}
   451  
   452  	return workerArtifact, nil
   453  }
   454  
   455  func (volume *createdVolume) InitializeTaskCache(jobID int, stepName string, path string) error {
   456  	tx, err := volume.conn.Begin()
   457  	if err != nil {
   458  		return err
   459  	}
   460  
   461  	defer Rollback(tx)
   462  
   463  	usedTaskCache, err := usedTaskCache{
   464  		jobID:    jobID,
   465  		stepName: stepName,
   466  		path:     path,
   467  	}.findOrCreate(tx)
   468  	if err != nil {
   469  		return err
   470  	}
   471  
   472  	usedWorkerTaskCache, err := WorkerTaskCache{
   473  		WorkerName: volume.WorkerName(),
   474  		TaskCache:  usedTaskCache,
   475  	}.findOrCreate(tx)
   476  	if err != nil {
   477  		return err
   478  	}
   479  
   480  	// release other old volumes for gc
   481  	_, err = psql.Update("volumes").
   482  		Set("worker_task_cache_id", nil).
   483  		Where(sq.Eq{"worker_task_cache_id": usedWorkerTaskCache.ID}).
   484  		RunWith(tx).
   485  		Exec()
   486  	if err != nil {
   487  		return err
   488  	}
   489  
   490  	rows, err := psql.Update("volumes").
   491  		Set("worker_task_cache_id", usedWorkerTaskCache.ID).
   492  		Where(sq.Eq{"id": volume.id}).
   493  		RunWith(tx).
   494  		Exec()
   495  	if err != nil {
   496  		if pqErr, ok := err.(*pq.Error); ok && pqErr.Code.Name() == pqUniqueViolationErrCode {
   497  			// another volume was 'blessed' as the cache volume - leave this one
   498  			// owned by the container so it just expires when the container is GCed
   499  			return nil
   500  		}
   501  
   502  		return err
   503  	}
   504  
   505  	affected, err := rows.RowsAffected()
   506  	if err != nil {
   507  		return err
   508  	}
   509  
   510  	if affected == 0 {
   511  		return ErrVolumeMissing
   512  	}
   513  
   514  	err = tx.Commit()
   515  	if err != nil {
   516  		return err
   517  	}
   518  
   519  	return nil
   520  }
   521  
   522  func (volume *createdVolume) CreateChildForContainer(container CreatingContainer, mountPath string) (CreatingVolume, error) {
   523  	tx, err := volume.conn.Begin()
   524  	if err != nil {
   525  		return nil, err
   526  	}
   527  
   528  	defer Rollback(tx)
   529  
   530  	handle, err := uuid.NewV4()
   531  	if err != nil {
   532  		return nil, err
   533  	}
   534  
   535  	columnNames := []string{
   536  		"worker_name",
   537  		"parent_id",
   538  		"parent_state",
   539  		"handle",
   540  		"container_id",
   541  		"path",
   542  	}
   543  	columnValues := []interface{}{
   544  		volume.workerName,
   545  		volume.id,
   546  		VolumeStateCreated,
   547  		handle.String(),
   548  		container.ID(),
   549  		mountPath,
   550  	}
   551  
   552  	if volume.teamID != 0 {
   553  		columnNames = append(columnNames, "team_id")
   554  		columnValues = append(columnValues, volume.teamID)
   555  	}
   556  
   557  	var volumeID int
   558  	err = psql.Insert("volumes").
   559  		Columns(columnNames...).
   560  		Values(columnValues...).
   561  		Suffix("RETURNING id").
   562  		RunWith(tx).
   563  		QueryRow().
   564  		Scan(&volumeID)
   565  	if err != nil {
   566  		return nil, err
   567  	}
   568  
   569  	err = tx.Commit()
   570  	if err != nil {
   571  		return nil, err
   572  	}
   573  
   574  	return &creatingVolume{
   575  		id:              volumeID,
   576  		workerName:      volume.workerName,
   577  		handle:          handle.String(),
   578  		path:            mountPath,
   579  		teamID:          volume.teamID,
   580  		typ:             VolumeTypeContainer,
   581  		containerHandle: container.Handle(),
   582  		parentHandle:    volume.Handle(),
   583  		conn:            volume.conn,
   584  	}, nil
   585  }
   586  
   587  func (volume *createdVolume) Destroying() (DestroyingVolume, error) {
   588  	err := volumeStateTransition(
   589  		volume.id,
   590  		volume.conn,
   591  		VolumeStateCreated,
   592  		VolumeStateDestroying,
   593  	)
   594  	if err != nil {
   595  		if err == ErrVolumeStateTransitionFailed {
   596  			return nil, ErrVolumeMarkStateFailed{VolumeStateDestroying}
   597  
   598  		}
   599  
   600  		if pqErr, ok := err.(*pq.Error); ok &&
   601  			pqErr.Code.Name() == pqFKeyViolationErrCode &&
   602  			pqErr.Constraint == "volumes_parent_id_fkey" {
   603  			return nil, ErrVolumeCannotBeDestroyedWithChildrenPresent
   604  		}
   605  
   606  		return nil, err
   607  	}
   608  
   609  	return &destroyingVolume{
   610  		id:         volume.id,
   611  		workerName: volume.workerName,
   612  		handle:     volume.handle,
   613  		conn:       volume.conn,
   614  	}, nil
   615  }
   616  
   617  //go:generate counterfeiter . DestroyingVolume
   618  type DestroyingVolume interface {
   619  	Handle() string
   620  	Destroy() (bool, error)
   621  	WorkerName() string
   622  }
   623  
   624  type destroyingVolume struct {
   625  	id         int
   626  	workerName string
   627  	handle     string
   628  	conn       Conn
   629  }
   630  
   631  func (volume *destroyingVolume) Handle() string     { return volume.handle }
   632  func (volume *destroyingVolume) WorkerName() string { return volume.workerName }
   633  
   634  func (volume *destroyingVolume) Destroy() (bool, error) {
   635  	rows, err := psql.Delete("volumes").
   636  		Where(sq.Eq{
   637  			"id":    volume.id,
   638  			"state": VolumeStateDestroying,
   639  		}).
   640  		RunWith(volume.conn).
   641  		Exec()
   642  	if err != nil {
   643  		return false, err
   644  	}
   645  
   646  	affected, err := rows.RowsAffected()
   647  	if err != nil {
   648  		return false, err
   649  	}
   650  
   651  	if affected == 0 {
   652  		return false, nil
   653  	}
   654  
   655  	return true, nil
   656  }
   657  
   658  type FailedVolume interface {
   659  	Handle() string
   660  	Destroy() (bool, error)
   661  	WorkerName() string
   662  }
   663  
   664  type failedVolume struct {
   665  	id         int
   666  	workerName string
   667  	handle     string
   668  	conn       Conn
   669  }
   670  
   671  func (volume *failedVolume) Handle() string     { return volume.handle }
   672  func (volume *failedVolume) WorkerName() string { return volume.workerName }
   673  
   674  func (volume *failedVolume) Destroy() (bool, error) {
   675  	rows, err := psql.Delete("volumes").
   676  		Where(sq.Eq{
   677  			"id":    volume.id,
   678  			"state": VolumeStateFailed,
   679  		}).
   680  		RunWith(volume.conn).
   681  		Exec()
   682  	if err != nil {
   683  		return false, err
   684  	}
   685  
   686  	affected, err := rows.RowsAffected()
   687  	if err != nil {
   688  		return false, err
   689  	}
   690  
   691  	if affected == 0 {
   692  		return false, nil
   693  	}
   694  
   695  	return true, nil
   696  }
   697  
   698  func volumeStateTransition(volumeID int, conn Conn, from, to VolumeState) error {
   699  	rows, err := psql.Update("volumes").
   700  		Set("state", string(to)).
   701  		Where(sq.And{
   702  			sq.Eq{"id": volumeID},
   703  			sq.Or{
   704  				sq.Eq{"state": string(from)},
   705  				sq.Eq{"state": string(to)},
   706  			},
   707  		}).
   708  		RunWith(conn).
   709  		Exec()
   710  	if err != nil {
   711  		return err
   712  	}
   713  
   714  	affected, err := rows.RowsAffected()
   715  	if err != nil {
   716  		return err
   717  	}
   718  
   719  	if affected == 0 {
   720  		return ErrVolumeStateTransitionFailed
   721  	}
   722  
   723  	return nil
   724  }