diff --git a/cmd/k2d.go b/cmd/k2d.go index 76e2b50..7a8d94e 100644 --- a/cmd/k2d.go +++ b/cmd/k2d.go @@ -83,6 +83,7 @@ func main() { kubeDockerAdapterOptions := &adapter.KubeDockerAdapterOptions{ DataPath: cfg.DataPath, + VolumePath: cfg.VolumePath, DockerClientTimeout: cfg.DockerClientTimeout, ServerConfiguration: serverConfiguration, Logger: logger, diff --git a/internal/adapter/adapter.go b/internal/adapter/adapter.go index 8d82bdf..75dbc16 100644 --- a/internal/adapter/adapter.go +++ b/internal/adapter/adapter.go @@ -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 @@ -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) } diff --git a/internal/adapter/configmap.go b/internal/adapter/configmap.go index 6b5b52e..f76eb76 100644 --- a/internal/adapter/configmap.go +++ b/internal/adapter/configmap.go @@ -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) { @@ -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) } diff --git a/internal/adapter/converter/pod.go b/internal/adapter/converter/pod.go index 9b9ac82..9425b81 100644 --- a/internal/adapter/converter/pod.go +++ b/internal/adapter/converter/pod.go @@ -2,6 +2,7 @@ package converter import ( "fmt" + "path/filepath" "strconv" "strings" "time" @@ -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/") @@ -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) } } diff --git a/internal/adapter/filesystem/configmap.go b/internal/adapter/filesystem/configmap.go index 6038369..b9beba2 100644 --- a/internal/adapter/filesystem/configmap.go +++ b/internal/adapter/filesystem/configmap.go @@ -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) } @@ -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{ @@ -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 diff --git a/internal/adapter/filesystem/secret.go b/internal/adapter/filesystem/secret.go index 17a8fd2..08d6001 100644 --- a/internal/adapter/filesystem/secret.go +++ b/internal/adapter/filesystem/secret.go @@ -12,68 +12,25 @@ import ( str "github.com/portainer/k2d/pkg/strings" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/labels" "k8s.io/kubernetes/pkg/apis/core" ) var ErrSecretNotFound = errors.New("secret file(s) not found") -func buildSecretMetadataFileName(secretName string) string { - return fmt.Sprintf("%s-k2dsec.metadata", secretName) -} - -func (store *FileSystemStore) DeleteSecret(secretName string) error { +func (store *FileSystemStore) GetSecret(secretName string) (*core.Secret, error) { store.mutex.Lock() defer store.mutex.Unlock() - files, err := os.ReadDir(store.secretPath) - if err != nil { - return fmt.Errorf("unable to read secret directory: %w", err) - } - - fileNames := []string{} - for _, file := range files { - fileNames = append(fileNames, file.Name()) - } - - uniqueNames := str.UniquePrefixes(fileNames, SECRET_SEPARATOR) - - if !str.IsStringInSlice(secretName, uniqueNames) { - return fmt.Errorf("secret %s not found", secretName) - } - - filePrefix := fmt.Sprintf("%s%s", secretName, SECRET_SEPARATOR) - - for _, file := range files { - if strings.HasPrefix(file.Name(), filePrefix) { - err := os.Remove(path.Join(store.secretPath, file.Name())) - if err != nil { - return fmt.Errorf("unable to remove file %s: %w", file.Name(), err) - } - } - } - - metadataFileName := buildSecretMetadataFileName(secretName) - metadataFileFound, err := filesystem.FileExists(path.Join(store.secretPath, metadataFileName)) - if err != nil { - return fmt.Errorf("unable to check if metadata file exists: %w", err) - } - - if metadataFileFound { - err := os.Remove(path.Join(store.secretPath, metadataFileName)) + // this is required for kubectl apply -f that executes kubectl get first + _, err := os.Stat(path.Join(store.path, secretName, "_data")) + if os.IsNotExist(err) { + err = filesystem.CreateDir(path.Join(store.path, secretName, "_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+"/"+secretName+"/_data/", err) } } - return nil -} - -func (store *FileSystemStore) GetSecret(secretName string) (*core.Secret, error) { - store.mutex.Lock() - defer store.mutex.Unlock() - - files, err := os.ReadDir(store.secretPath) + files, err := os.ReadDir(path.Join(store.path, secretName, "_data")) if err != nil { return &core.Secret{}, fmt.Errorf("unable to read secret directory: %w", err) } @@ -104,111 +61,75 @@ func (store *FileSystemStore) GetSecret(secretName string) (*core.Secret, error) } filePrefix := fmt.Sprintf("%s%s", secretName, SECRET_SEPARATOR) - for _, file := range files { if strings.HasPrefix(file.Name(), filePrefix) { - data, err := os.ReadFile(path.Join(store.secretPath, file.Name())) + data, err := os.ReadFile(path.Join(store.path, secretName, "_data", file.Name())) if err != nil { return &core.Secret{}, fmt.Errorf("unable to read file %s: %w", file.Name(), err) } secret.Data[strings.TrimPrefix(file.Name(), secretName+SECRET_SEPARATOR)] = bytes.TrimSuffix(data, []byte("\n")) - info, err := os.Stat(path.Join(store.secretPath, file.Name())) + info, err := os.Stat(path.Join(store.path, secretName, "_data", file.Name())) if err != nil { return &core.Secret{}, fmt.Errorf("unable to get file info for %s: %w", file.Name(), err) } secret.ObjectMeta.CreationTimestamp = metav1.NewTime(info.ModTime()) - secret.ObjectMeta.Annotations[fmt.Sprintf("secret.k2d.io/%s", file.Name())] = path.Join(store.secretPath, file.Name()) + secret.ObjectMeta.Annotations[fmt.Sprintf("secret.k2d.io/%s", file.Name())] = secretName } } - metadataFileName := buildSecretMetadataFileName(secretName) - metadataFileFound, err := filesystem.FileExists(path.Join(store.secretPath, metadataFileName)) - if err != nil { - return &core.Secret{}, fmt.Errorf("unable to check if metadata file exists: %w", err) - } - - if metadataFileFound { - metadata, err := filesystem.LoadMetadataFromDisk(store.secretPath, metadataFileName) - if err != nil { - return &core.Secret{}, fmt.Errorf("unable to load secret metadata from disk: %w", err) - } - - secret.Labels = metadata - } - return &secret, nil } -func (store *FileSystemStore) GetSecrets(selector labels.Selector) (core.SecretList, error) { +func (store *FileSystemStore) GetSecrets(mountPoints []string) (core.SecretList, error) { store.mutex.Lock() defer store.mutex.Unlock() - files, err := os.ReadDir(store.secretPath) - if err != nil { - return core.SecretList{}, fmt.Errorf("unable to read secret directory: %w", err) - } - - fileNames := []string{} - for _, file := range files { - fileNames = append(fileNames, file.Name()) - } - - uniqueNames := str.UniquePrefixes(fileNames, SECRET_SEPARATOR) - uniqueNames = str.RemoveItemsWithSuffix(uniqueNames, ".metadata") - secrets := []core.Secret{} - - for _, name := range uniqueNames { - secret := core.Secret{ - TypeMeta: metav1.TypeMeta{ - Kind: "Secret", - APIVersion: "v1", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: "default", - }, - Data: map[string][]byte{}, - Type: core.SecretTypeOpaque, - } - - metadataFileName := buildSecretMetadataFileName(secret.Name) - metadataFileFound, err := filesystem.FileExists(path.Join(store.secretPath, metadataFileName)) + for _, mountPoint := range mountPoints { + files, err := os.ReadDir(mountPoint) if err != nil { - return core.SecretList{}, fmt.Errorf("unable to check if metadata file exists: %w", err) + return core.SecretList{}, fmt.Errorf("unable to read secret directory: %w", err) } - if metadataFileFound { - metadata, err := filesystem.LoadMetadataFromDisk(store.secretPath, metadataFileName) - if err != nil { - return core.SecretList{}, fmt.Errorf("unable to load secret metadata from disk: %w", err) - } - - secret.Labels = metadata - } - - if !selector.Matches(labels.Set(secret.Labels)) { - continue + fileNames := []string{} + for _, file := range files { + fileNames = append(fileNames, file.Name()) } - for _, file := range files { - if strings.HasPrefix(file.Name(), fmt.Sprintf("%s%s", name, SECRET_SEPARATOR)) { - data, err := os.ReadFile(path.Join(store.secretPath, file.Name())) - if err != nil { - return core.SecretList{}, fmt.Errorf("unable to read file %s: %w", file.Name(), err) - } + uniqueNames := str.UniquePrefixes(fileNames, SECRET_SEPARATOR) + for _, name := range uniqueNames { + secret := core.Secret{ + TypeMeta: metav1.TypeMeta{ + Kind: "Secret", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: "default", + }, + Data: map[string][]byte{}, + Type: core.SecretTypeOpaque, + } - secret.Data[strings.TrimPrefix(file.Name(), name+SECRET_SEPARATOR)] = data - info, err := os.Stat(path.Join(store.secretPath, file.Name())) - if err != nil { - return core.SecretList{}, fmt.Errorf("unable to get file info for %s: %w", file.Name(), err) + for _, file := range files { + if strings.HasPrefix(file.Name(), fmt.Sprintf("%s%s", name, SECRET_SEPARATOR)) { + data, err := os.ReadFile(path.Join(mountPoint, file.Name())) + if err != nil { + return core.SecretList{}, fmt.Errorf("unable to read file %s: %w", file.Name(), err) + } + + secret.Data[strings.TrimPrefix(file.Name(), name+SECRET_SEPARATOR)] = data + info, err := os.Stat(path.Join(mountPoint, file.Name())) + if err != nil { + return core.SecretList{}, fmt.Errorf("unable to get file info for %s: %w", file.Name(), err) + } + secret.ObjectMeta.CreationTimestamp = metav1.NewTime(info.ModTime()) } - secret.ObjectMeta.CreationTimestamp = metav1.NewTime(info.ModTime()) } - } - secrets = append(secrets, secret) + secrets = append(secrets, secret) + } } return core.SecretList{ @@ -234,18 +155,15 @@ func (store *FileSystemStore) StoreSecret(secret *corev1.Secret) error { data[key] = value } - filePrefix := fmt.Sprintf("%s%s", secret.Name, SECRET_SEPARATOR) - err := filesystem.StoreDataMapOnDisk(store.secretPath, filePrefix, data) + err := filesystem.CreateDir(path.Join(store.path, secret.Name, "_data")) if err != nil { - return err + return fmt.Errorf("unable to create directory %s: %w", store.path+"/"+secret.Name+"/_data/", err) } - if len(secret.Labels) != 0 { - metadataFileName := buildSecretMetadataFileName(secret.Name) - err = filesystem.StoreMetadataOnDisk(store.secretPath, metadataFileName, secret.Labels) - if err != nil { - return err - } + filePrefix := fmt.Sprintf("%s%s", secret.Name, SECRET_SEPARATOR) + err = filesystem.StoreDataMapOnDisk(path.Join(store.path, secret.Name, "_data"), filePrefix, data) + if err != nil { + return err } return nil diff --git a/internal/adapter/filesystem/store.go b/internal/adapter/filesystem/store.go index cd65640..5427606 100644 --- a/internal/adapter/filesystem/store.go +++ b/internal/adapter/filesystem/store.go @@ -2,11 +2,8 @@ package filesystem import ( - "fmt" "path" "sync" - - "github.com/portainer/k2d/pkg/filesystem" ) // Constants representing folder names and separators used in file paths. @@ -25,6 +22,7 @@ type ( configMapPath string secretPath string mutex sync.Mutex + path string } ) @@ -32,18 +30,10 @@ type ( // It receives a data path where the directories for configMaps and secrets are created. // If the directories cannot be created, an error is returned. func NewFileSystemStore(dataPath string) (*FileSystemStore, error) { - folders := []string{CONFIGMAP_FOLDER, SECRET_FOLDER} - - for _, folder := range folders { - err := filesystem.CreateDir(path.Join(dataPath, folder)) - if err != nil { - return nil, fmt.Errorf("unable to create directory %s: %w", folder, err) - } - } - return &FileSystemStore{ configMapPath: path.Join(dataPath, CONFIGMAP_FOLDER), secretPath: path.Join(dataPath, SECRET_FOLDER), mutex: sync.Mutex{}, + path: dataPath, }, nil } diff --git a/internal/adapter/secret.go b/internal/adapter/secret.go index f0cdfae..c9c0cca 100644 --- a/internal/adapter/secret.go +++ b/internal/adapter/secret.go @@ -1,8 +1,12 @@ 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" @@ -10,12 +14,46 @@ import ( "k8s.io/kubernetes/pkg/apis/core" ) -func (adapter *KubeDockerAdapter) CreateSecret(secret *corev1.Secret) error { - return adapter.fileSystemStore.StoreSecret(secret) +func (adapter *KubeDockerAdapter) CreateSecret(ctx context.Context, secret *corev1.Secret) error { + // return adapter.fileSystemStore.StoreSecret(secret) + _, err := adapter.CreateVolume(ctx, VolumeOptions{ + VolumeName: secret.Name, + Labels: map[string]string{ + k2dtypes.GenericLabelKey: "secret", + }, + }) + if err != nil { + return fmt.Errorf("unable to create volume: %w", err) + } + + err = adapter.fileSystemStore.StoreSecret(secret) + if err != nil { + return fmt.Errorf("unable to store secret: %w", err) + } + + return nil } func (adapter *KubeDockerAdapter) DeleteSecret(secretName string) error { - return adapter.fileSystemStore.DeleteSecret(secretName) + filter := filters.NewArgs() + filter.Add("label", fmt.Sprintf("%s=%s", k2dtypes.GenericLabelKey, "secret")) + filter.Add("name", secretName) + + 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) GetSecret(secretName string) (*corev1.Secret, error) { @@ -42,8 +80,7 @@ func (adapter *KubeDockerAdapter) GetSecret(secretName string) (*corev1.Secret, } func (adapter *KubeDockerAdapter) ListSecrets(selector labels.Selector) (corev1.SecretList, error) { - - secretList, err := adapter.listSecrets(selector) + secretList, err := adapter.listSecrets() if err != nil { return corev1.SecretList{}, fmt.Errorf("unable to list secrets: %w", err) } @@ -64,7 +101,7 @@ func (adapter *KubeDockerAdapter) ListSecrets(selector labels.Selector) (corev1. } func (adapter *KubeDockerAdapter) GetSecretTable(selector labels.Selector) (*metav1.Table, error) { - secretList, err := adapter.listSecrets(selector) + secretList, err := adapter.listSecrets() if err != nil { return &metav1.Table{}, fmt.Errorf("unable to list secrets: %w", err) } @@ -72,6 +109,22 @@ func (adapter *KubeDockerAdapter) GetSecretTable(selector labels.Selector) (*met return k8s.GenerateTable(&secretList) } -func (adapter *KubeDockerAdapter) listSecrets(selector labels.Selector) (core.SecretList, error) { - return adapter.fileSystemStore.GetSecrets(selector) +func (adapter *KubeDockerAdapter) listSecrets() (core.SecretList, error) { + labelFilter := filters.NewArgs() + labelFilter.Add("label", fmt.Sprintf("%s=%s", k2dtypes.GenericLabelKey, "secret")) + + volume, err := adapter.cli.VolumeList(context.Background(), volume.ListOptions{ + Filters: labelFilter, + }) + + if err != nil { + return core.SecretList{}, fmt.Errorf("unable to list volumes: %w", err) + } + + mountPoints := []string{} + for _, v := range volume.Volumes { + mountPoints = append(mountPoints, v.Mountpoint) + } + + return adapter.fileSystemStore.GetSecrets(mountPoints) } diff --git a/internal/adapter/types/generic.go b/internal/adapter/types/generic.go new file mode 100644 index 0000000..751d868 --- /dev/null +++ b/internal/adapter/types/generic.go @@ -0,0 +1,6 @@ +package types + +const ( + // GenericLabelKey is the key used to store the secret type in the volume labels + GenericLabelKey = "k2d.io/type" +) diff --git a/internal/adapter/volume.go b/internal/adapter/volume.go new file mode 100644 index 0000000..a1dade8 --- /dev/null +++ b/internal/adapter/volume.go @@ -0,0 +1,65 @@ +package adapter + +import ( + "context" + "fmt" + + "github.com/docker/docker/api/types/volume" +) + +type VolumeOptions struct { + VolumeName string + Labels map[string]string +} + +func (adapter *KubeDockerAdapter) CreateVolume(ctx context.Context, options VolumeOptions) (volume.Volume, error) { + out, err := adapter.cli.VolumeCreate(ctx, volume.CreateOptions{ + Name: options.VolumeName, + Labels: options.Labels, + }) + + if err != nil { + return volume.Volume{}, fmt.Errorf("unable to create the %s docker volume: %w", options.VolumeName, err) + } + + return out, nil +} + +func (adapter *KubeDockerAdapter) ListVolume(ctx context.Context, options VolumeOptions) (volume.Volume, error) { + out, err := adapter.cli.VolumeCreate(ctx, volume.CreateOptions{ + Name: options.VolumeName, + Labels: options.Labels, + }) + + if err != nil { + return volume.Volume{}, fmt.Errorf("unable to list %s docker volumes: %w", options.VolumeName, err) + } + + return out, nil +} + +func (adapter *KubeDockerAdapter) GetVolume(ctx context.Context, options VolumeOptions) (volume.Volume, error) { + out, err := adapter.cli.VolumeCreate(ctx, volume.CreateOptions{ + Name: options.VolumeName, + Labels: options.Labels, + }) + + if err != nil { + return volume.Volume{}, fmt.Errorf("unable to get the %s docker volume: %w", options.VolumeName, err) + } + + return out, nil +} + +func (adapter *KubeDockerAdapter) DeleteVolume(ctx context.Context, options VolumeOptions) (volume.Volume, error) { + out, err := adapter.cli.VolumeCreate(ctx, volume.CreateOptions{ + Name: options.VolumeName, + Labels: options.Labels, + }) + + if err != nil { + return volume.Volume{}, fmt.Errorf("unable to delete the %s docker volume: %w", options.VolumeName, err) + } + + return out, nil +} diff --git a/internal/api/core/v1/secrets/put.go b/internal/api/core/v1/secrets/put.go index 0a45558..3319677 100644 --- a/internal/api/core/v1/secrets/put.go +++ b/internal/api/core/v1/secrets/put.go @@ -1,6 +1,7 @@ package secrets import ( + "context" "errors" "fmt" "net/http" @@ -39,7 +40,7 @@ func (svc SecretService) PutSecret(r *restful.Request, w *restful.Response) { for { _, err := svc.adapter.GetSecret(secretName) if err == nil { - err = svc.adapter.CreateSecret(secret) + err = svc.adapter.CreateSecret(context.TODO(), secret) if err != nil { utils.HttpError(r, w, http.StatusInternalServerError, fmt.Errorf("unable to update secret: %w", err)) return diff --git a/internal/config/config.go b/internal/config/config.go index d2133d7..08f4376 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -15,6 +15,11 @@ type Config struct { // the default value is set to /var/lib/k2d. DataPath string `env:"K2D_DATA_PATH,default=/var/lib/k2d"` + // VolumePath represents the path for Docker volume storage. + // If not provided through an environment variable named K2D_VOLUME_PATH, + // the default value is set to /var/lib/docker/volumes. + VolumePath string `env:"K2D_VOLUME_PATH,default=/var/lib/docker/volumes"` + // DockerClientTimeout represents the timeout duration for Docker client operations. // If not provided through an environment variable named K2D_DOCKER_CLIENT_TIMEOUT, // the default value is set to 10 minutes (10m). diff --git a/internal/controller/controller.go b/internal/controller/controller.go index 2400428..d855724 100644 --- a/internal/controller/controller.go +++ b/internal/controller/controller.go @@ -226,10 +226,10 @@ func (controller *OperationController) createService(op Operation) error { func (controller *OperationController) createConfigMap(op Operation) error { configMap := op.Operation.(*corev1.ConfigMap) - return controller.adapter.CreateConfigMap(configMap) + return controller.adapter.CreateConfigMap(context.TODO(), configMap) } func (controller *OperationController) createSecret(op Operation) error { secret := op.Operation.(*corev1.Secret) - return controller.adapter.CreateSecret(secret) + return controller.adapter.CreateSecret(context.TODO(), secret) }