diff --git a/configs/settings.yml b/configs/settings.yml index f63074e8..767ce967 100644 --- a/configs/settings.yml +++ b/configs/settings.yml @@ -26,6 +26,7 @@ base: &base cert_manager_cluster_issuer: ${CERT_MANAGER_CLUSTER_ISSUER:-} sandbox_enabled: ${SANDBOX_ENABLED} ingress_default_port: 443 + pvc_storage_class_name: uffizzi-standard service_checks: ip_ping_timeout: 1800s availability_timeout: 1800s diff --git a/internal/clients/kuber/deployment_utils.go b/internal/clients/kuber/deployment_utils.go index d0dc53f8..8ddebd21 100644 --- a/internal/clients/kuber/deployment_utils.go +++ b/internal/clients/kuber/deployment_utils.go @@ -53,6 +53,63 @@ func prepareContainerSecrets(container domainTypes.Container, secret *corev1.Sec func prepareDeploymentVolumes(containerList domainTypes.ContainerList) []corev1.Volume { volumes := []corev1.Volume{} + configFileVolumes := prepareDeploymentConfigFileVolumes(containerList) + volumes = append(volumes, configFileVolumes...) + pvcVolumes := prepareDeploymentPvcVolumes(containerList) + volumes = append(volumes, pvcVolumes...) + + return volumes +} + +func prepareContainerVolumeMounts(container domainTypes.Container) []corev1.VolumeMount { + volumeMounts := []corev1.VolumeMount{} + configVolumeMounts := prepareContainerConfigFileVolumeMounts(container) + volumeMounts = append(volumeMounts, configVolumeMounts...) + + namedVolumeMounts := prepareContainerNamedVolumeMounts(container) + volumeMounts = append(volumeMounts, namedVolumeMounts...) + + anonymousVolumeMounts := prepareContainerAnonymousVolumeMounts(container) + volumeMounts = append(volumeMounts, anonymousVolumeMounts...) + + return volumeMounts +} + +func prepareConfigFileMountPath(containerConfigFile *domainTypes.ContainerConfigFile) (string, string) { + mountPath := containerConfigFile.MountPath + + if len(mountPath) == 0 { + mountPath = "/" + } + + if mountPath[0] != '/' { + mountPath = fmt.Sprintf("/%v", containerConfigFile.MountPath) + } + + if mountPath == "/" { + mountPath = fmt.Sprintf("/%v", containerConfigFile.ConfigFile.Filename) + } + + mountPathParts := strings.Split(mountPath, "/") + mountFileName := mountPathParts[len(mountPathParts)-1] + + return mountPath, mountFileName +} + +func prepareCredentialsDeployment(credentials []domainTypes.Credential) []corev1.LocalObjectReference { + references := []corev1.LocalObjectReference{} + + for _, credential := range credentials { + credentialName := global.Settings.ResourceName.Credential(credential.ID) + + references = append(references, corev1.LocalObjectReference{Name: credentialName}) + } + + return references +} + +func prepareDeploymentConfigFileVolumes(containerList domainTypes.ContainerList) []corev1.Volume { + volumes := []corev1.Volume{} for _, container := range containerList.Items { for _, containerConfigFile := range container.ContainerConfigFiles { @@ -93,7 +150,28 @@ func prepareDeploymentVolumes(containerList domainTypes.ContainerList) []corev1. return volumes } -func prepareContainerVolumeMounts(container domainTypes.Container) []corev1.VolumeMount { +func prepareDeploymentPvcVolumes(containerList domainTypes.ContainerList) []corev1.Volume { + volumes := []corev1.Volume{} + pvcVolumes := containerList.GetUniqNamedVolumes() + pvcVolumes = append(pvcVolumes, containerList.GetUniqAnonymousVolumes()...) + + for _, pvcVolume := range pvcVolumes { + volume := corev1.Volume{ + Name: global.Settings.ResourceName.VolumeName(pvcVolume.UniqName), + VolumeSource: corev1.VolumeSource{ + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ + ClaimName: global.Settings.ResourceName.PvcName(pvcVolume.UniqName), + }, + }, + } + + volumes = append(volumes, volume) + } + + return volumes +} + +func prepareContainerConfigFileVolumeMounts(container domainTypes.Container) []corev1.VolumeMount { volumeMounts := []corev1.VolumeMount{} for _, containerConfigFile := range container.ContainerConfigFiles { @@ -116,37 +194,46 @@ func prepareContainerVolumeMounts(container domainTypes.Container) []corev1.Volu return volumeMounts } -func prepareConfigFileMountPath(containerConfigFile *domainTypes.ContainerConfigFile) (string, string) { - mountPath := containerConfigFile.MountPath +func prepareContainerNamedVolumeMounts(container domainTypes.Container) []corev1.VolumeMount { + volumeMounts := []corev1.VolumeMount{} - if len(mountPath) == 0 { - mountPath = "/" - } + for _, containerVolume := range container.ContainerVolumes { + if !containerVolume.IsNamedType() { + continue + } - if mountPath[0] != '/' { - mountPath = fmt.Sprintf("/%v", containerConfigFile.MountPath) - } + name := containerVolume.BuildUniqName(&container) + volumeMount := corev1.VolumeMount{ + Name: global.Settings.ResourceName.VolumeName(name), + MountPath: containerVolume.Target, + ReadOnly: containerVolume.ReadOnly, + } - if mountPath == "/" { - mountPath = fmt.Sprintf("/%v", containerConfigFile.ConfigFile.Filename) + volumeMounts = append(volumeMounts, volumeMount) } - mountPathParts := strings.Split(mountPath, "/") - mountFileName := mountPathParts[len(mountPathParts)-1] - - return mountPath, mountFileName + return volumeMounts } -func prepareCredentialsDeployment(credentials []domainTypes.Credential) []corev1.LocalObjectReference { - references := []corev1.LocalObjectReference{} +func prepareContainerAnonymousVolumeMounts(container domainTypes.Container) []corev1.VolumeMount { + volumeMounts := []corev1.VolumeMount{} - for _, credential := range credentials { - credentialName := global.Settings.ResourceName.Credential(credential.ID) + for _, containerVolume := range container.ContainerVolumes { + if !containerVolume.IsAnonymousType() { + continue + } - references = append(references, corev1.LocalObjectReference{Name: credentialName}) + name := containerVolume.BuildUniqName(&container) + volumeMount := corev1.VolumeMount{ + Name: global.Settings.ResourceName.VolumeName(name), + MountPath: containerVolume.Source, + ReadOnly: containerVolume.ReadOnly, + } + + volumeMounts = append(volumeMounts, volumeMount) } - return references + return volumeMounts } func prepareContainerHealthcheck(container domainTypes.Container) *corev1.Probe { diff --git a/internal/clients/kuber/persistent_volume_claim.go b/internal/clients/kuber/persistent_volume_claim.go new file mode 100644 index 00000000..bd313e1c --- /dev/null +++ b/internal/clients/kuber/persistent_volume_claim.go @@ -0,0 +1,97 @@ +package kuber + +import ( + "fmt" + + "gitlab.com/dualbootpartners/idyl/uffizzi_controller/internal/global" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func (client Client) FindOrInitializePersistentVolumeClaim( + namespace, name string) (*corev1.PersistentVolumeClaim, error) { + persistentVolumeClaim, err := client.GetPersistentVolumeClaim(namespace, name) + if err != nil && !errors.IsNotFound(err) { + return persistentVolumeClaim, err + } + + if persistentVolumeClaim != nil && len(persistentVolumeClaim.UID) > 0 { + return persistentVolumeClaim, nil + } + + var storageClassName string = global.Settings.PvcStorageClassName + + persistentVolumeClaimDraft := &corev1.PersistentVolumeClaim{ + Spec: corev1.PersistentVolumeClaimSpec{ + StorageClassName: &storageClassName, + AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce}, + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceStorage: resource.MustParse("1Gi"), + }, + }, + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Labels: map[string]string{ + "app.kubernetes.io/managed-by": global.Settings.ManagedApplication, + }, + }, + } + + return persistentVolumeClaimDraft, nil +} + +func (client *Client) GetPersistentVolumeClaim(namespace, name string) (*corev1.PersistentVolumeClaim, error) { + persistentVolumeClaimClient := client.clientset.CoreV1().PersistentVolumeClaims(namespace) + + persistentVolumeClaim, err := persistentVolumeClaimClient.Get(client.context, name, metav1.GetOptions{}) + if err != nil { + return nil, err + } + + return persistentVolumeClaim, nil +} + +func (client *Client) GetPersistentVolumeClaims(namespace string) ([]corev1.PersistentVolumeClaim, error) { + persistentVolumeClaimClient := client.clientset.CoreV1().PersistentVolumeClaims(namespace) + + persistentVolumeClaimList, err := persistentVolumeClaimClient.List(client.context, metav1.ListOptions{ + LabelSelector: fmt.Sprintf("app.kubernetes.io/managed-by=%v", global.Settings.ManagedApplication), + }) + if err != nil { + return nil, err + } + + return persistentVolumeClaimList.Items, nil +} + +func (client *Client) CreatePersistentVolumeClaim( + namespace string, + persistentVolumeClaimDraft *corev1.PersistentVolumeClaim, +) (*corev1.PersistentVolumeClaim, error) { + persistentVolumeClaimClient := client.clientset.CoreV1().PersistentVolumeClaims(namespace) + + persistentVolumeClaim, err := persistentVolumeClaimClient.Create( + client.context, persistentVolumeClaimDraft, metav1.CreateOptions{}) + + if err != nil { + return nil, err + } + + return persistentVolumeClaim, nil +} + +func (client *Client) DeletePersistentVolumeClaim(namespace, name string) error { + persistentVolumeClaimClient := client.clientset.CoreV1().PersistentVolumeClaims(namespace) + + err := persistentVolumeClaimClient.Delete(client.context, name, metav1.DeleteOptions{}) + if err != nil { + return nil + } + + return nil +} diff --git a/internal/config/settings/settings.go b/internal/config/settings/settings.go index f38264c0..ce1961de 100644 --- a/internal/config/settings/settings.go +++ b/internal/config/settings/settings.go @@ -36,6 +36,7 @@ type Settings struct { CertManagerClusterIssuer string `yaml:"cert_manager_cluster_issuer"` EphemeralStorageCoefficient float64 `yaml:"ephemeral_storage_coefficient"` IngressDefaultPort int `yaml:"ingress_default_port"` + PvcStorageClassName string `yaml:"pvc_storage_class_name"` } type ServiceChecksSettings struct { diff --git a/internal/domain_logic/containers.go b/internal/domain_logic/containers.go index 577103e3..e174904f 100644 --- a/internal/domain_logic/containers.go +++ b/internal/domain_logic/containers.go @@ -1,9 +1,11 @@ package domain import ( + "log" "time" "gitlab.com/dualbootpartners/idyl/uffizzi_controller/internal/global" + "gitlab.com/dualbootpartners/idyl/uffizzi_controller/internal/pkg/string_utils" domainTypes "gitlab.com/dualbootpartners/idyl/uffizzi_controller/internal/types/domain" ) @@ -58,3 +60,75 @@ func (l *Logic) ApplyContainerSecrets(namespace string, containerList domainType return nil } + +func (l *Logic) ApplyContainersNamedVolumes(namespace string, containerList domainTypes.ContainerList) error { + for _, volume := range containerList.GetUniqNamedVolumes() { + pvcName := global.Settings.ResourceName.PvcName(volume.UniqName) + pvc, err := l.KuberClient.FindOrInitializePersistentVolumeClaim(namespace, pvcName) + + if err != nil { + return err + } + + if len(pvc.UID) == 0 { + _, err = l.KuberClient.CreatePersistentVolumeClaim(namespace, pvc) + } + + if err != nil { + return err + } + } + + return nil +} + +func (l *Logic) ApplyContainersAnonymousVolumes(namespace string, containerList domainTypes.ContainerList) error { + for _, volume := range containerList.GetUniqAnonymousVolumes() { + pvcName := global.Settings.ResourceName.PvcName(volume.UniqName) + pvc, err := l.KuberClient.FindOrInitializePersistentVolumeClaim(namespace, pvcName) + + if err != nil { + return err + } + + if len(pvc.UID) == 0 { + _, err = l.KuberClient.CreatePersistentVolumeClaim(namespace, pvc) + } + + if err != nil { + return err + } + } + + return nil +} + +func (l *Logic) RemoveUnusedContainersVolumes(namespace string, containerList domainTypes.ContainerList) error { + uniqVolumes := containerList.GetUniqNamedVolumes() + uniqVolumes = append(uniqVolumes, containerList.GetUniqAnonymousVolumes()...) + newPersistentVolumeClaimNames := []string{} + + for _, volume := range uniqVolumes { + newPersistentVolumeClaimNames = append(newPersistentVolumeClaimNames, global.Settings.ResourceName.PvcName(volume.UniqName)) + } + + existsPersistentVolumeClaims, err := l.KuberClient.GetPersistentVolumeClaims(namespace) + if err != nil { + return err + } + + for _, existsPersistentVolumeClaim := range existsPersistentVolumeClaims { + if string_utils.Contains(newPersistentVolumeClaimNames, existsPersistentVolumeClaim.Name) { + continue + } + + err := l.KuberClient.DeletePersistentVolumeClaim(namespace, existsPersistentVolumeClaim.Name) + if err != nil { + return nil + } + + log.Printf("%v/pvc %v was deleted\n", namespace, existsPersistentVolumeClaim.Name) + } + + return nil +} diff --git a/internal/domain_logic/deployment.go b/internal/domain_logic/deployment.go index 9d459ffd..466f80f5 100644 --- a/internal/domain_logic/deployment.go +++ b/internal/domain_logic/deployment.go @@ -117,11 +117,26 @@ func (l *Logic) ApplyContainers( return err } + err = l.RemoveUnusedContainersVolumes(namespaceName, containerList) + if err != nil { + return err + } + err = l.ApplyContainerSecrets(namespaceName, containerList) if err != nil { return err } + err = l.ApplyContainersNamedVolumes(namespaceName, containerList) + if err != nil { + return err + } + + err = l.ApplyContainersAnonymousVolumes(namespaceName, containerList) + if err != nil { + return err + } + if containerList.IsEmpty() { return l.CleaningNamespaceForEmptyContainers(namespace) } diff --git a/internal/pkg/resource_name_utils/resource_name_utils.go b/internal/pkg/resource_name_utils/resource_name_utils.go index d82cb681..cd263428 100644 --- a/internal/pkg/resource_name_utils/resource_name_utils.go +++ b/internal/pkg/resource_name_utils/resource_name_utils.go @@ -1,6 +1,9 @@ package resource_name_utils -import "fmt" +import ( + "fmt" + "regexp" +) type ResouceNameUtils struct{} @@ -27,3 +30,21 @@ func (resouceNameUtils *ResouceNameUtils) Deployment(namespace string) string { func (resouceNameUtils *ResouceNameUtils) Policy(namespace string) string { return fmt.Sprintf("app-%v", namespace) } + +func (resouceNameUtils *ResouceNameUtils) PvcName(name string) string { + rfcName := toRfc(name) + + return fmt.Sprintf("pvc-%v", rfcName) +} + +func (resouceNameUtils *ResouceNameUtils) VolumeName(name string) string { + rfcName := toRfc(name) + + return fmt.Sprintf("volume-%v", rfcName) +} + +func toRfc(str string) string { + regexp := regexp.MustCompile(`(\/|~|\.|_)`) + + return regexp.ReplaceAllString(str, "-") +} diff --git a/internal/types/domain/container.go b/internal/types/domain/container.go index 75aafd95..fc6ecd8b 100644 --- a/internal/types/domain/container.go +++ b/internal/types/domain/container.go @@ -26,6 +26,8 @@ type Container struct { MemoryRequest uint `json:"memory_request"` ContainerConfigFiles []*ContainerConfigFile `json:"container_config_files"` Healthcheck *Healthcheck `json:"healthcheck"` + ContainerVolumes []*ContainerVolume `json:"volumes"` + ServiceName string `json:"service_name"` } func (c Container) IsPublic() bool { diff --git a/internal/types/domain/container_list.go b/internal/types/domain/container_list.go index 52fec1bb..3a734794 100644 --- a/internal/types/domain/container_list.go +++ b/internal/types/domain/container_list.go @@ -6,6 +6,12 @@ type ContainerList struct { Items []Container `json:"items"` } +type DeploymentVolume struct { + Volume *ContainerVolume + Container *Container + UniqName string +} + func (list ContainerList) IsEmpty() bool { return list.Count() == 0 } @@ -61,3 +67,75 @@ func (list ContainerList) GetUserContainerList() ContainerList { func (list *ContainerList) AddContainer(container Container) { list.Items = append(list.Items, container) } + +func (list ContainerList) GetUniqNamedVolumes() []DeploymentVolume { + volumes := []DeploymentVolume{} + + for i, container := range list.Items { + for _, containerVolume := range container.ContainerVolumes { + if containerVolume.Type != ContainerVolumeTypeNamed { + continue + } + + isVolumeExists := false + uniqName := containerVolume.BuildUniqName(&list.Items[i]) + + for _, existsVolume := range volumes { + if existsVolume.UniqName == uniqName { + isVolumeExists = true + break + } + } + + if isVolumeExists { + continue + } + + volume := DeploymentVolume{ + Volume: containerVolume, + Container: &list.Items[i], + UniqName: uniqName, + } + + volumes = append(volumes, volume) + } + } + + return volumes +} + +func (list ContainerList) GetUniqAnonymousVolumes() []DeploymentVolume { + volumes := []DeploymentVolume{} + + for i, container := range list.Items { + for _, containerVolume := range container.ContainerVolumes { + if containerVolume.Type != ContainerVolumeTypeAnonymous { + continue + } + + isVolumeExists := false + uniqName := containerVolume.BuildUniqName(&list.Items[i]) + + for _, existsVolume := range volumes { + if existsVolume.UniqName == uniqName { + isVolumeExists = true + break + } + } + + if isVolumeExists { + continue + } + + volume := DeploymentVolume{ + Volume: containerVolume, + Container: &list.Items[i], + UniqName: uniqName, + } + + volumes = append(volumes, volume) + } + } + + return volumes +} diff --git a/internal/types/domain/container_volume.go b/internal/types/domain/container_volume.go new file mode 100644 index 00000000..1fedce41 --- /dev/null +++ b/internal/types/domain/container_volume.go @@ -0,0 +1,41 @@ +package types + +import "fmt" + +type ContainerVolume struct { + Source string `json:"source"` + Target string `json:"target"` + Type ContainerVolumeType `json:"type"` + ReadOnly bool `json:"read_only"` +} + +type ContainerVolumeType string + +const ( + ContainerVolumeTypeNamed ContainerVolumeType = "named" + ContainerVolumeTypeAnonymous ContainerVolumeType = "anonymous" + ContainerVolumeTypeHost ContainerVolumeType = "host" +) + +func (volume ContainerVolume) BuildUniqName(container *Container) string { + switch volume.Type { + case ContainerVolumeTypeAnonymous: + return fmt.Sprintf("%s-%s", container.ServiceName, volume.Source) + case ContainerVolumeTypeNamed: + return volume.Source + default: + return "" + } +} + +func (volume ContainerVolume) IsHostType() bool { + return volume.Type == ContainerVolumeTypeHost +} + +func (volume ContainerVolume) IsNamedType() bool { + return volume.Type == ContainerVolumeTypeNamed +} + +func (volume ContainerVolume) IsAnonymousType() bool { + return volume.Type == ContainerVolumeTypeAnonymous +}