Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: filesystem to Docker volumes #26

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cmd/k2d.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ func main() {

kubeDockerAdapterOptions := &adapter.KubeDockerAdapterOptions{
DataPath: cfg.DataPath,
VolumePath: cfg.VolumePath,
DockerClientTimeout: cfg.DockerClientTimeout,
ServerConfiguration: serverConfiguration,
Logger: logger,
Expand Down
4 changes: 3 additions & 1 deletion internal/adapter/adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ type (
KubeDockerAdapterOptions struct {
// DataPath is the path to the data directory where the configmaps and secrets will be stored
DataPath string
// VolumePath is the path to the directory where the volumes will be stored
VolumePath string
// DockerClientTimeout is the timeout that will be used when communicating with the Docker API with the Docker client
// It is responsible for the timeout of the Docker API calls such as creating a container, pulling an image...
DockerClientTimeout time.Duration
Expand All @@ -58,7 +60,7 @@ func NewKubeDockerAdapter(options *KubeDockerAdapterOptions) (*KubeDockerAdapter
return nil, fmt.Errorf("unable to create docker client: %w", err)
}

filesystemStore, err := filesystem.NewFileSystemStore(options.DataPath)
filesystemStore, err := filesystem.NewFileSystemStore(options.VolumePath)
if err != nil {
return nil, fmt.Errorf("unable to create filesystem store: %w", err)
}
Expand Down
63 changes: 59 additions & 4 deletions internal/adapter/configmap.go
Original file line number Diff line number Diff line change
@@ -1,20 +1,59 @@
package adapter

import (
"context"
"fmt"

"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/volume"
k2dtypes "github.com/portainer/k2d/internal/adapter/types"

"github.com/portainer/k2d/internal/k8s"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/kubernetes/pkg/apis/core"
)

func (adapter *KubeDockerAdapter) CreateConfigMap(configMap *corev1.ConfigMap) error {
return adapter.fileSystemStore.StoreConfigMap(configMap)
func (adapter *KubeDockerAdapter) CreateConfigMap(ctx context.Context, configMap *corev1.ConfigMap) error {
_, err := adapter.CreateVolume(ctx, VolumeOptions{
VolumeName: configMap.Name,
Labels: map[string]string{
k2dtypes.GenericLabelKey: "configmap",
"kubectl.kubernetes.io/last-applied-configuration": configMap.Annotations["kubectl.kubernetes.io/last-applied-configuration"],
},
})
if err != nil {
return fmt.Errorf("unable to create volume: %w", err)
}

err = adapter.fileSystemStore.StoreConfigMap(configMap)
if err != nil {
return fmt.Errorf("unable to store configmap: %w", err)
}

return nil
}

func (adapter *KubeDockerAdapter) DeleteConfigMap(configMapName string) error {
return adapter.fileSystemStore.DeleteConfigMap(configMapName)
filter := filters.NewArgs()
filter.Add("label", fmt.Sprintf("%s=%s", k2dtypes.GenericLabelKey, "configmap"))
filter.Add("name", configMapName)

volume, err := adapter.cli.VolumeList(context.Background(), volume.ListOptions{
Filters: filter,
})
if err != nil {
return fmt.Errorf("unable to get the requested volume: %w", err)
}

if len(volume.Volumes) != 0 {
err = adapter.cli.VolumeRemove(context.Background(), volume.Volumes[0].Name, true)
if err != nil {
return fmt.Errorf("unable to remove volume: %w", err)
}
}

return nil
}

func (adapter *KubeDockerAdapter) GetConfigMap(configMapName string) (*corev1.ConfigMap, error) {
Expand Down Expand Up @@ -71,5 +110,21 @@ func (adapter *KubeDockerAdapter) ListConfigMaps() (corev1.ConfigMapList, error)
}

func (adapter *KubeDockerAdapter) listConfigMaps() (core.ConfigMapList, error) {
return adapter.fileSystemStore.GetConfigMaps()
labelFilter := filters.NewArgs()
labelFilter.Add("label", fmt.Sprintf("%s=%s", k2dtypes.GenericLabelKey, "configmap"))

volume, err := adapter.cli.VolumeList(context.Background(), volume.ListOptions{
Filters: labelFilter,
})

if err != nil {
return core.ConfigMapList{}, fmt.Errorf("unable to list volumes: %w", err)
}

mountPoints := []string{}
for _, v := range volume.Volumes {
mountPoints = append(mountPoints, v.Mountpoint)
}

return adapter.fileSystemStore.GetConfigMaps(mountPoints)
}
5 changes: 3 additions & 2 deletions internal/adapter/converter/pod.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package converter

import (
"fmt"
"path/filepath"
"strconv"
"strings"
"time"
Expand Down Expand Up @@ -347,7 +348,7 @@ func (converter *DockerAPIConverter) handleVolumeSource(hostConfig *container.Ho
if volume.VolumeSource.ConfigMap != nil {
configMap, err := converter.store.GetConfigMap(volume.VolumeSource.ConfigMap.Name)
if err != nil {
return fmt.Errorf("unable to get configmap %s: %w", volume.VolumeSource.ConfigMap.Name, err)
return fmt.Errorf("unable to get configmap %s: %w", configMap.Name, err)
}

converter.setBindsFromAnnotations(hostConfig, configMap.Annotations, volumeMount, "configmap.k2d.io/")
Expand All @@ -370,7 +371,7 @@ func (converter *DockerAPIConverter) handleVolumeSource(hostConfig *container.Ho
func (converter *DockerAPIConverter) setBindsFromAnnotations(hostConfig *container.HostConfig, annotations map[string]string, volumeMount core.VolumeMount, prefix string) {
for key, value := range annotations {
if strings.HasPrefix(key, prefix) {
bind := fmt.Sprintf("%s:%s", value, volumeMount.MountPath)
bind := fmt.Sprintf("%s:%s", value, filepath.Dir(volumeMount.MountPath))
hostConfig.Binds = append(hostConfig.Binds, bind)
}
}
Expand Down
180 changes: 53 additions & 127 deletions internal/adapter/filesystem/configmap.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,62 +19,20 @@ import (

var ErrConfigMapNotFound = errors.New("configmap file(s) not found")

func buildConfigMapMetadataFileName(configMapName string) string {
return fmt.Sprintf("%s-k2dcm.metadata", configMapName)
}

func (store *FileSystemStore) DeleteConfigMap(configMapName string) error {
func (store *FileSystemStore) GetConfigMap(configMapName string) (*core.ConfigMap, error) {
store.mutex.Lock()
defer store.mutex.Unlock()

files, err := os.ReadDir(store.configMapPath)
if err != nil {
return fmt.Errorf("unable to read configmap directory: %w", err)
}

fileNames := []string{}
for _, file := range files {
fileNames = append(fileNames, file.Name())
}

uniqueNames := str.UniquePrefixes(fileNames, CONFIGMAP_SEPARATOR)

if !str.IsStringInSlice(configMapName, uniqueNames) {
return fmt.Errorf("configmap %s not found", configMapName)
}

filePrefix := fmt.Sprintf("%s%s", configMapName, CONFIGMAP_SEPARATOR)

for _, file := range files {
if strings.HasPrefix(file.Name(), filePrefix) {
err := os.Remove(path.Join(store.configMapPath, file.Name()))
if err != nil {
return fmt.Errorf("unable to remove file %s: %w", file.Name(), err)
}
}
}

metadataFileName := buildConfigMapMetadataFileName(configMapName)
metadataFileFound, err := filesystem.FileExists(path.Join(store.configMapPath, metadataFileName))
if err != nil {
return fmt.Errorf("unable to check if metadata file exists: %w", err)
}

if metadataFileFound {
err := os.Remove(path.Join(store.configMapPath, metadataFileName))
// this is required for kubectl apply -f that executes kubectl get first
_, err := os.Stat(path.Join(store.path, configMapName, "_data"))
if os.IsNotExist(err) {
err = filesystem.CreateDir(path.Join(store.path, configMapName, "_data"))
if err != nil {
return fmt.Errorf("unable to remove file %s: %w", metadataFileName, err)
return nil, fmt.Errorf("unable to create directory %s: %w", store.path+"/"+configMapName+"/_data/", err)
}
}

return nil
}

func (store *FileSystemStore) GetConfigMap(configMapName string) (*core.ConfigMap, error) {
store.mutex.Lock()
defer store.mutex.Unlock()

files, err := os.ReadDir(store.configMapPath)
files, err := os.ReadDir(path.Join(store.path, configMapName, "_data/"))
if err != nil {
return &core.ConfigMap{}, fmt.Errorf("unable to read configmap directory: %w", err)
}
Expand Down Expand Up @@ -107,103 +65,74 @@ func (store *FileSystemStore) GetConfigMap(configMapName string) (*core.ConfigMa

for _, file := range files {
if strings.HasPrefix(file.Name(), filePrefix) {
data, err := os.ReadFile(path.Join(store.configMapPath, file.Name()))
data, err := os.ReadFile(path.Join(store.path, configMapName, "_data", file.Name()))
if err != nil {
return &core.ConfigMap{}, fmt.Errorf("unable to read file %s: %w", file.Name(), err)
}

configMap.Data[strings.TrimPrefix(file.Name(), configMapName+CONFIGMAP_SEPARATOR)] = string(data)
info, err := os.Stat(path.Join(store.configMapPath, file.Name()))
configMap.Data[strings.TrimPrefix(file.Name(), configMapName+CONFIGMAP_SEPARATOR)] = strings.TrimSuffix(string(data), "\n")
info, err := os.Stat(path.Join(store.path, configMapName, "_data", file.Name()))
if err != nil {
return &core.ConfigMap{}, fmt.Errorf("unable to get file info for %s: %w", file.Name(), err)
}

configMap.ObjectMeta.CreationTimestamp = metav1.NewTime(info.ModTime())
configMap.ObjectMeta.Annotations[fmt.Sprintf("configmap.k2d.io/%s", file.Name())] = path.Join(store.configMapPath, file.Name())
configMap.ObjectMeta.Annotations[fmt.Sprintf("configmap.k2d.io/%s", file.Name())] = configMapName
}
}

metadataFileName := buildConfigMapMetadataFileName(configMapName)
metadataFileFound, err := filesystem.FileExists(path.Join(store.configMapPath, metadataFileName))
if err != nil {
return &core.ConfigMap{}, fmt.Errorf("unable to check if metadata file exists: %w", err)
}

if metadataFileFound {
metadata, err := filesystem.LoadMetadataFromDisk(store.configMapPath, metadataFileName)
if err != nil {
return &core.ConfigMap{}, fmt.Errorf("unable to load configmap metadata from disk: %w", err)
}

configMap.Labels = metadata
}

return &configMap, nil
}

func (store *FileSystemStore) GetConfigMaps() (core.ConfigMapList, error) {
func (store *FileSystemStore) GetConfigMaps(mountPoints []string) (core.ConfigMapList, error) {
store.mutex.Lock()
defer store.mutex.Unlock()

files, err := os.ReadDir(store.configMapPath)
if err != nil {
return core.ConfigMapList{}, fmt.Errorf("unable to read configmap directory: %w", err)
}

fileNames := []string{}
for _, file := range files {
fileNames = append(fileNames, file.Name())
}

uniqueNames := str.UniquePrefixes(fileNames, CONFIGMAP_SEPARATOR)
uniqueNames = str.RemoveItemsWithSuffix(uniqueNames, ".metadata")

configMaps := []core.ConfigMap{}
for _, name := range uniqueNames {
configMap := core.ConfigMap{
TypeMeta: metav1.TypeMeta{
Kind: "ConfigMap",
APIVersion: "v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: "default",
},
Data: map[string]string{},
for _, mountPoint := range mountPoints {
files, err := os.ReadDir(mountPoint)
if err != nil {
return core.ConfigMapList{}, fmt.Errorf("unable to read configmap directory: %w", err)
}

fileNames := []string{}
for _, file := range files {
if strings.HasPrefix(file.Name(), fmt.Sprintf("%s%s", name, CONFIGMAP_SEPARATOR)) {
data, err := os.ReadFile(path.Join(store.configMapPath, file.Name()))
if err != nil {
return core.ConfigMapList{}, fmt.Errorf("unable to read file %s: %w", file.Name(), err)
}

configMap.Data[strings.TrimPrefix(file.Name(), name+CONFIGMAP_SEPARATOR)] = string(data)
info, err := os.Stat(path.Join(store.configMapPath, file.Name()))
if err != nil {
return core.ConfigMapList{}, fmt.Errorf("unable to get file info for %s: %w", file.Name(), err)
}
configMap.ObjectMeta.CreationTimestamp = metav1.NewTime(info.ModTime())
}
fileNames = append(fileNames, file.Name())
}

metadataFileName := buildConfigMapMetadataFileName(name)
metadataFileFound, err := filesystem.FileExists(path.Join(store.configMapPath, metadataFileName))
if err != nil {
return core.ConfigMapList{}, fmt.Errorf("unable to check if metadata file exists: %w", err)
}
uniqueNames := str.UniquePrefixes(fileNames, CONFIGMAP_SEPARATOR)

for _, name := range uniqueNames {
configMap := core.ConfigMap{
TypeMeta: metav1.TypeMeta{
Kind: "ConfigMap",
APIVersion: "v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: "default",
},
Data: map[string]string{},
}

if metadataFileFound {
metadata, err := filesystem.LoadMetadataFromDisk(store.configMapPath, metadataFileName)
if err != nil {
return core.ConfigMapList{}, fmt.Errorf("unable to load configmap metadata from disk: %w", err)
for _, file := range files {
if strings.HasPrefix(file.Name(), fmt.Sprintf("%s%s", name, CONFIGMAP_SEPARATOR)) {
data, err := os.ReadFile(path.Join(mountPoint, file.Name()))
if err != nil {
return core.ConfigMapList{}, fmt.Errorf("unable to read file %s: %w", file.Name(), err)
}

configMap.Data[strings.TrimPrefix(file.Name(), name+CONFIGMAP_SEPARATOR)] = string(data)
info, err := os.Stat(path.Join(mountPoint, file.Name()))
if err != nil {
return core.ConfigMapList{}, fmt.Errorf("unable to get file info for %s: %w", file.Name(), err)
}
configMap.ObjectMeta.CreationTimestamp = metav1.NewTime(info.ModTime())
}
}

configMap.Labels = metadata
configMaps = append(configMaps, configMap)
}

configMaps = append(configMaps, configMap)
}

return core.ConfigMapList{
Expand All @@ -219,18 +148,15 @@ func (store *FileSystemStore) StoreConfigMap(configMap *corev1.ConfigMap) error
store.mutex.Lock()
defer store.mutex.Unlock()

filePrefix := fmt.Sprintf("%s%s", configMap.Name, CONFIGMAP_SEPARATOR)
err := filesystem.StoreDataMapOnDisk(store.configMapPath, filePrefix, configMap.Data)
err := filesystem.CreateDir(path.Join(store.path, configMap.Name, "_data"))
if err != nil {
return err
return fmt.Errorf("unable to create directory %s: %w", store.path+"/"+configMap.Name+"/_data/", err)
}

if len(configMap.Labels) != 0 {
metadataFileName := buildConfigMapMetadataFileName(configMap.Name)
err = filesystem.StoreMetadataOnDisk(store.configMapPath, metadataFileName, configMap.Labels)
if err != nil {
return err
}
filePrefix := fmt.Sprintf("%s%s", configMap.Name, CONFIGMAP_SEPARATOR)
err = filesystem.StoreDataMapOnDisk(path.Join(store.path, configMap.Name, "_data"), filePrefix, configMap.Data)
if err != nil {
return err
}

return nil
Expand Down
Loading