github.com/containerd/nerdctl/v2@v2.0.0-beta.5.0.20240520001846-b5758f54fa28/pkg/mountutil/volumestore/volumestore.go (about)

     1  /*
     2     Copyright The containerd Authors.
     3  
     4     Licensed under the Apache License, Version 2.0 (the "License");
     5     you may not use this file except in compliance with the License.
     6     You may obtain a copy of the License at
     7  
     8         http://www.apache.org/licenses/LICENSE-2.0
     9  
    10     Unless required by applicable law or agreed to in writing, software
    11     distributed under the License is distributed on an "AS IS" BASIS,
    12     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13     See the License for the specific language governing permissions and
    14     limitations under the License.
    15  */
    16  
    17  package volumestore
    18  
    19  import (
    20  	"encoding/json"
    21  	"fmt"
    22  	"os"
    23  	"path/filepath"
    24  
    25  	"github.com/containerd/containerd/errdefs"
    26  	"github.com/containerd/containerd/identifiers"
    27  	"github.com/containerd/log"
    28  	"github.com/containerd/nerdctl/v2/pkg/inspecttypes/native"
    29  	"github.com/containerd/nerdctl/v2/pkg/lockutil"
    30  	"github.com/containerd/nerdctl/v2/pkg/strutil"
    31  )
    32  
    33  // Path returns a string like `/var/lib/nerdctl/1935db59/volumes/default`.
    34  func Path(dataStore, ns string) (string, error) {
    35  	if dataStore == "" || ns == "" {
    36  		return "", errdefs.ErrInvalidArgument
    37  	}
    38  	volStore := filepath.Join(dataStore, "volumes", ns)
    39  	return volStore, nil
    40  }
    41  
    42  // New returns a VolumeStore
    43  func New(dataStore, ns string) (VolumeStore, error) {
    44  	volStoreDir, err := Path(dataStore, ns)
    45  	if err != nil {
    46  		return nil, err
    47  	}
    48  	if err := os.MkdirAll(volStoreDir, 0700); err != nil {
    49  		return nil, err
    50  	}
    51  	vs := &volumeStore{
    52  		dir: volStoreDir,
    53  	}
    54  	return vs, nil
    55  }
    56  
    57  // DataDirName is "_data"
    58  const DataDirName = "_data"
    59  
    60  const volumeJSONFileName = "volume.json"
    61  
    62  type VolumeStore interface {
    63  	Dir() string
    64  	Create(name string, labels []string) (*native.Volume, error)
    65  	// Get may return ErrNotFound
    66  	Get(name string, size bool) (*native.Volume, error)
    67  	List(size bool) (map[string]native.Volume, error)
    68  	Remove(names []string) (removedNames []string, err error)
    69  }
    70  
    71  type volumeStore struct {
    72  	// dir is a string like `/var/lib/nerdctl/1935db59/volumes/default`.
    73  	// dir is guaranteed to exist.
    74  	dir string
    75  }
    76  
    77  func (vs *volumeStore) Dir() string {
    78  	return vs.dir
    79  }
    80  
    81  func (vs *volumeStore) Create(name string, labels []string) (*native.Volume, error) {
    82  	if err := identifiers.Validate(name); err != nil {
    83  		return nil, fmt.Errorf("malformed name %s: %w", name, err)
    84  	}
    85  	volPath := filepath.Join(vs.dir, name)
    86  	volDataPath := filepath.Join(volPath, DataDirName)
    87  	volFilePath := filepath.Join(volPath, volumeJSONFileName)
    88  	fn := func() (err error) {
    89  		if err := os.Mkdir(volPath, 0700); err != nil {
    90  			return err
    91  		}
    92  		defer func() {
    93  			if err != nil {
    94  				os.Remove(volPath)
    95  			}
    96  		}()
    97  		if err := os.Mkdir(volDataPath, 0755); err != nil {
    98  			return err
    99  		}
   100  		defer func() {
   101  			if err != nil {
   102  				os.Remove(volDataPath)
   103  			}
   104  		}()
   105  
   106  		type volumeOpts struct {
   107  			Labels map[string]string `json:"labels"`
   108  		}
   109  
   110  		labelsMap := strutil.ConvertKVStringsToMap(labels)
   111  
   112  		volOpts := volumeOpts{
   113  			Labels: labelsMap,
   114  		}
   115  
   116  		labelsJSON, err := json.MarshalIndent(volOpts, "", "    ")
   117  		if err != nil {
   118  			return err
   119  		}
   120  
   121  		defer func() {
   122  			if err != nil {
   123  				if _, statErr := os.Stat(volFilePath); statErr != nil && !os.IsNotExist(statErr) {
   124  					log.L.Warnf("failed to stat volume file: %v", statErr)
   125  					return
   126  				} else if statErr == nil {
   127  					os.Remove(volFilePath)
   128  				}
   129  			}
   130  		}()
   131  		return os.WriteFile(volFilePath, labelsJSON, 0644)
   132  	}
   133  
   134  	if err := lockutil.WithDirLock(vs.dir, fn); err != nil {
   135  		return nil, err
   136  	}
   137  
   138  	// If other new actions that might fail are added below, we should move the cleanup function out of fn.
   139  
   140  	vol := &native.Volume{
   141  		Name:       name,
   142  		Mountpoint: volDataPath,
   143  	}
   144  	return vol, nil
   145  }
   146  
   147  func (vs *volumeStore) Get(name string, size bool) (*native.Volume, error) {
   148  	if err := identifiers.Validate(name); err != nil {
   149  		return nil, fmt.Errorf("malformed name %s: %w", name, err)
   150  	}
   151  	dataPath := filepath.Join(vs.dir, name, DataDirName)
   152  	if _, err := os.Stat(dataPath); err != nil {
   153  		if os.IsNotExist(err) {
   154  			return nil, fmt.Errorf("volume %q not found: %w", name, errdefs.ErrNotFound)
   155  		}
   156  		return nil, err
   157  	}
   158  
   159  	volFilePath := filepath.Join(vs.dir, name, volumeJSONFileName)
   160  	volumeDataBytes, err := os.ReadFile(volFilePath)
   161  	if err != nil {
   162  		if !os.IsNotExist(err) {
   163  			return nil, err
   164  		} // on else, volume.json does not exists should not be blocking for inspect operation
   165  	}
   166  
   167  	entry := native.Volume{
   168  		Name:       name,
   169  		Mountpoint: dataPath,
   170  		Labels:     Labels(volumeDataBytes),
   171  	}
   172  	if size {
   173  		entry.Size, err = Size(&entry)
   174  		if err != nil {
   175  			return nil, err
   176  		}
   177  	}
   178  	return &entry, nil
   179  }
   180  
   181  func (vs *volumeStore) List(size bool) (map[string]native.Volume, error) {
   182  	dEnts, err := os.ReadDir(vs.dir)
   183  	if err != nil {
   184  		return nil, err
   185  	}
   186  
   187  	res := make(map[string]native.Volume, len(dEnts))
   188  	for _, dEnt := range dEnts {
   189  		name := dEnt.Name()
   190  		vol, err := vs.Get(name, size)
   191  		if err != nil {
   192  			return res, err
   193  		}
   194  		res[name] = *vol
   195  	}
   196  	return res, nil
   197  }
   198  
   199  func (vs *volumeStore) Remove(names []string) ([]string, error) {
   200  	var removed []string
   201  	fn := func() error {
   202  		for _, name := range names {
   203  			if err := identifiers.Validate(name); err != nil {
   204  				return fmt.Errorf("malformed name %s: %w", name, err)
   205  			}
   206  			dir := filepath.Join(vs.dir, name)
   207  			if err := os.RemoveAll(dir); err != nil {
   208  				return err
   209  			}
   210  			removed = append(removed, name)
   211  		}
   212  		return nil
   213  	}
   214  	err := lockutil.WithDirLock(vs.dir, fn)
   215  	return removed, err
   216  }
   217  
   218  func Labels(b []byte) *map[string]string {
   219  	type volumeOpts struct {
   220  		Labels *map[string]string `json:"labels,omitempty"`
   221  	}
   222  	var vo volumeOpts
   223  	if err := json.Unmarshal(b, &vo); err != nil {
   224  		return nil
   225  	}
   226  	return vo.Labels
   227  }
   228  
   229  func Size(volume *native.Volume) (int64, error) {
   230  	var size int64
   231  	var walkFn = func(_ string, info os.FileInfo, err error) error {
   232  		if err != nil {
   233  			return err
   234  		}
   235  		if !info.IsDir() {
   236  			size += info.Size()
   237  		}
   238  		return err
   239  	}
   240  	var err = filepath.Walk(volume.Mountpoint, walkFn)
   241  	if err != nil {
   242  		return 0, err
   243  	}
   244  	return size, nil
   245  }