package configmaps import ( "os" "path/filepath" "regexp" "strings" "time" "github.com/containers/common/pkg/configmaps/filedriver" "github.com/containers/storage/pkg/lockfile" "github.com/containers/storage/pkg/stringid" "github.com/pkg/errors" ) // maxConfigMapSize is the max size for configMap data - 512kB const maxConfigMapSize = 512000 // configMapIDLength is the character length of a configMap ID - 25 const configMapIDLength = 25 // errInvalidPath indicates that the configMaps path is invalid var errInvalidPath = errors.New("invalid configmaps path") // ErrNoSuchConfigMap indicates that the configMap does not exist var ErrNoSuchConfigMap = errors.New("no such configmap") // errConfigMapNameInUse indicates that the configMap name is already in use var errConfigMapNameInUse = errors.New("configmap name in use") // errInvalidConfigMapName indicates that the configMap name is invalid var errInvalidConfigMapName = errors.New("invalid configmap name") // errInvalidDriver indicates that the driver type is invalid var errInvalidDriver = errors.New("invalid driver") // errInvalidDriverOpt indicates that a driver option is invalid var errInvalidDriverOpt = errors.New("invalid driver option") // errAmbiguous indicates that a configMap is ambiguous var errAmbiguous = errors.New("configmap is ambiguous") // errDataSize indicates that the configMap data is too large or too small var errDataSize = errors.New("configmap data must be larger than 0 and less than 512000 bytes") // configMapsFile is the name of the file that the configMaps database will be stored in var configMapsFile = "configmaps.json" // configMapNameRegexp matches valid configMap names // Allowed: 64 [a-zA-Z0-9-_.] characters, and the start and end character must be [a-zA-Z0-9] var configMapNameRegexp = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9_.-]*$`) // ConfigMapManager holds information on handling configmaps type ConfigMapManager struct { // configMapDBPath is the path to the db file where configmaps are stored configMapDBPath string // lockfile is the locker for the configmap file lockfile lockfile.Locker // db is an in-memory cache of the database of configMaps db *db } // ConfigMap defines a configMap type ConfigMap struct { // Name is the name of the configmap Name string `json:"name"` // ID is the unique configMap ID ID string `json:"id"` // Metadata stores other metadata on the configMap Metadata map[string]string `json:"metadata,omitempty"` // CreatedAt is when the configMap was created CreatedAt time.Time `json:"createdAt"` // Driver is the driver used to store configMap data Driver string `json:"driver"` // DriverOptions is other metadata needed to use the driver DriverOptions map[string]string `json:"driverOptions"` } // ConfigMapsDriver interfaces with the configMaps data store. // The driver stores the actual bytes of configMap data, as opposed to // the configMap metadata. // // revive does not like the name because the package is already called configmaps //nolint:revive type ConfigMapsDriver interface { // List lists all configMap ids in the configMaps data store List() ([]string, error) // Lookup gets the configMap's data bytes Lookup(id string) ([]byte, error) // Store stores the configMap's data bytes Store(id string, data []byte) error // Delete deletes a configMap's data from the driver Delete(id string) error } // NewManager creates a new configMaps manager // rootPath is the directory where the configMaps data file resides func NewManager(rootPath string) (*ConfigMapManager, error) { manager := new(ConfigMapManager) if !filepath.IsAbs(rootPath) { return nil, errors.Wrapf(errInvalidPath, "path must be absolute: %s", rootPath) } // the lockfile functions require that the rootPath dir is executable if err := os.MkdirAll(rootPath, 0o700); err != nil { return nil, err } lock, err := lockfile.GetLockfile(filepath.Join(rootPath, "configMaps.lock")) if err != nil { return nil, err } manager.lockfile = lock manager.configMapDBPath = filepath.Join(rootPath, configMapsFile) manager.db = new(db) manager.db.ConfigMaps = make(map[string]ConfigMap) manager.db.NameToID = make(map[string]string) manager.db.IDToName = make(map[string]string) return manager, nil } // Store takes a name, creates a configMap and stores the configMap metadata and the configMap payload. // It returns a generated ID that is associated with the configMap. // The max size for configMap data is 512kB. func (s *ConfigMapManager) Store(name string, data []byte, driverType string, driverOpts map[string]string) (string, error) { err := validateConfigMapName(name) if err != nil { return "", err } if !(len(data) > 0 && len(data) < maxConfigMapSize) { return "", errDataSize } s.lockfile.Lock() defer s.lockfile.Unlock() exist, err := s.exactConfigMapExists(name) if err != nil { return "", err } if exist { return "", errors.Wrapf(errConfigMapNameInUse, name) } secr := new(ConfigMap) secr.Name = name for { newID := stringid.GenerateNonCryptoID() // GenerateNonCryptoID() gives 64 characters, so we truncate to correct length newID = newID[0:configMapIDLength] _, err := s.lookupConfigMap(newID) if err != nil { if errors.Cause(err) == ErrNoSuchConfigMap { secr.ID = newID break } else { return "", err } } } secr.Driver = driverType secr.Metadata = make(map[string]string) secr.CreatedAt = time.Now() secr.DriverOptions = driverOpts driver, err := getDriver(driverType, driverOpts) if err != nil { return "", err } err = driver.Store(secr.ID, data) if err != nil { return "", errors.Wrapf(err, "error creating configMap %s", name) } err = s.store(secr) if err != nil { return "", errors.Wrapf(err, "error creating configMap %s", name) } return secr.ID, nil } // Delete removes all configMap metadata and configMap data associated with the specified configMap. // Delete takes a name, ID, or partial ID. func (s *ConfigMapManager) Delete(nameOrID string) (string, error) { err := validateConfigMapName(nameOrID) if err != nil { return "", err } s.lockfile.Lock() defer s.lockfile.Unlock() configMap, err := s.lookupConfigMap(nameOrID) if err != nil { return "", err } configMapID := configMap.ID driver, err := getDriver(configMap.Driver, configMap.DriverOptions) if err != nil { return "", err } err = driver.Delete(configMapID) if err != nil { return "", errors.Wrapf(err, "error deleting configMap %s", nameOrID) } err = s.delete(configMapID) if err != nil { return "", errors.Wrapf(err, "error deleting configMap %s", nameOrID) } return configMapID, nil } // Lookup gives a configMap's metadata given its name, ID, or partial ID. func (s *ConfigMapManager) Lookup(nameOrID string) (*ConfigMap, error) { s.lockfile.Lock() defer s.lockfile.Unlock() return s.lookupConfigMap(nameOrID) } // List lists all configMaps. func (s *ConfigMapManager) List() ([]ConfigMap, error) { s.lockfile.Lock() defer s.lockfile.Unlock() configMaps, err := s.lookupAll() if err != nil { return nil, err } var ls []ConfigMap for _, v := range configMaps { ls = append(ls, v) } return ls, nil } // LookupConfigMapData returns configMap metadata as well as configMap data in bytes. // The configMap data can be looked up using its name, ID, or partial ID. func (s *ConfigMapManager) LookupConfigMapData(nameOrID string) (*ConfigMap, []byte, error) { s.lockfile.Lock() defer s.lockfile.Unlock() configMap, err := s.lookupConfigMap(nameOrID) if err != nil { return nil, nil, err } driver, err := getDriver(configMap.Driver, configMap.DriverOptions) if err != nil { return nil, nil, err } data, err := driver.Lookup(configMap.ID) if err != nil { return nil, nil, err } return configMap, data, nil } // validateConfigMapName checks if the configMap name is valid. func validateConfigMapName(name string) error { if !configMapNameRegexp.MatchString(name) || len(name) > 64 || strings.HasSuffix(name, "-") || strings.HasSuffix(name, ".") { return errors.Wrapf(errInvalidConfigMapName, "only 64 [a-zA-Z0-9-_.] characters allowed, and the start and end character must be [a-zA-Z0-9]: %s", name) } return nil } // getDriver creates a new driver. func getDriver(name string, opts map[string]string) (ConfigMapsDriver, error) { if name == "file" { if path, ok := opts["path"]; ok { return filedriver.NewDriver(path) } return nil, errors.Wrap(errInvalidDriverOpt, "need path for filedriver") } return nil, errInvalidDriver }