github.com/openebs/node-disk-manager@v1.9.1-0.20230225014141-4531f06ffa1e/pkg/cleaner/jobcontroller.go (about)

     1  /*
     2  Copyright 2019 OpenEBS 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  This pkg is inspired from the deleter pkg in local-static-provisioner
    17  in kubernetes-sigs
    18  	https://github.com/kubernetes-sigs/sig-storage-local-static-provisioner/tree/master/pkg/deleter
    19  */
    20  
    21  package cleaner
    22  
    23  import (
    24  	"context"
    25  	"fmt"
    26  
    27  	"github.com/openebs/node-disk-manager/api/v1alpha1"
    28  	"github.com/openebs/node-disk-manager/blockdevice"
    29  	"github.com/openebs/node-disk-manager/cmd/ndm_daemonset/controller"
    30  	"github.com/openebs/node-disk-manager/pkg/env"
    31  	batchv1 "k8s.io/api/batch/v1"
    32  	v1 "k8s.io/api/core/v1"
    33  	"k8s.io/apimachinery/pkg/api/errors"
    34  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    35  	"sigs.k8s.io/controller-runtime/pkg/client"
    36  )
    37  
    38  const (
    39  	// JobContainerName is the name of the cleanup job container
    40  	JobContainerName = "cleaner"
    41  	// JobNamePrefix is the prefix for the cleanup job name
    42  	JobNamePrefix = "cleanup-"
    43  	// BDLabel is the label set on the job for identification of the BD
    44  	BDLabel = "blockdevice"
    45  )
    46  
    47  // JobController defines the interface for the job controller.
    48  type JobController interface {
    49  	IsCleaningJobRunning(bdName string) bool
    50  	CancelJob(bdName string) error
    51  	RemoveJob(bdName string) (CleanupState, error)
    52  }
    53  
    54  var _ JobController = &jobController{}
    55  
    56  type jobController struct {
    57  	client    client.Client
    58  	namespace string
    59  }
    60  
    61  // NewCleanupJob creates a new cleanup job in the  namespace. It returns a Job object which can be used to
    62  // start the job
    63  func NewCleanupJob(bd *v1alpha1.BlockDevice, volMode VolumeMode, tolerations []v1.Toleration, namespace string) (*batchv1.Job, error) {
    64  	nodeName := bd.Labels[controller.KubernetesHostNameLabel]
    65  
    66  	priv := true
    67  	jobContainer := v1.Container{
    68  		Name:  JobContainerName,
    69  		Image: getCleanUpImage(),
    70  		SecurityContext: &v1.SecurityContext{
    71  			Privileged: &priv,
    72  		},
    73  	}
    74  
    75  	podSpec := v1.PodSpec{}
    76  	mountName := "vol-mount"
    77  
    78  	if volMode == VolumeModeBlock {
    79  		jobContainer.Command = []string{"/bin/sh", "-c"}
    80  
    81  		// fdisk is used to get all the partitions of the device.
    82  		// Example
    83  		// $ fdisk -o Device -l /dev/sda
    84  		// 	Disk /dev/sda: 465.8 GiB, 500107862016 bytes, 976773168 sectors
    85  		// 	Units: sectors of 1 * 512 = 512 bytes
    86  		// 	Sector size (logical/physical): 512 bytes / 4096 bytes
    87  		// 	I/O size (minimum/optimal): 4096 bytes / 4096 bytes
    88  		// 	Disklabel type: dos
    89  		// 	Disk identifier: 0x065e2357
    90  		//
    91  		// 	Device
    92  		// 	/dev/sda1
    93  		// 	/dev/sda2
    94  		// 	/dev/sda5
    95  		// 	/dev/sda6
    96  		// 	/dev/sda7
    97  		//
    98  		// From the above output the partitions are filtered using grep,
    99  		//
   100  		// first all the partitions are cleared off any filesystem signatures, then the actual partition table
   101  		// header is removed. partprobe is called so as to re-read partition table, and update system with
   102  		// the changes.  Partprobe will be called only if the device is a block file; else if its sparse file, wipefs
   103  		// will be done.
   104  		// wipefs erases the filesystem signature from the block
   105  		// -a    wipe all magic strings
   106  		// -f    force erasure
   107  
   108  		args := fmt.Sprintf(""+
   109  			"(pvs -o pv_name,vg_name|grep %[1]s|awk '{print $2}'"+
   110  			"| xargs -I {} sh -c 'dmsetup info -c -o name --noheadings|grep ^{}- "+
   111  			"| xargs -t -I {} dmsetup remove {} ') && "+
   112  			"(fdisk -o Device -l %[1]s "+
   113  			"| grep \"^%[1]s\" "+
   114  			"| xargs -I '{}' wipefs -fa '{}') "+
   115  			"&& wipefs -fa %[1]s ",
   116  			bd.Spec.Path)
   117  
   118  		// partprobe need to be executed only if the device is of type disk.
   119  		if bd.Spec.Details.DeviceType == blockdevice.BlockDeviceTypeDisk {
   120  			args += fmt.Sprintf("&& partprobe %s ", bd.Spec.Path)
   121  		}
   122  
   123  		jobContainer.Args = []string{args}
   124  
   125  		var volume v1.Volume
   126  		var volumeMount v1.VolumeMount
   127  
   128  		// in case of sparse disk, need to mount the sparse file directory
   129  		// and clear the sparse file
   130  		if bd.Spec.Details.DeviceType == blockdevice.SparseBlockDeviceType {
   131  			volume, volumeMount = getVolumeMounts(bd.Spec.Path, bd.Spec.Path, mountName)
   132  		} else {
   133  			// for a non sparse disk, mount the /dev directory so that wipefs inside the container
   134  			// gets reflected outside the container also. When this is done a correspnding change
   135  			// event is also generated from udev
   136  			volume, volumeMount = getVolumeMounts("/dev", "/dev", mountName)
   137  		}
   138  
   139  		jobContainer.VolumeMounts = []v1.VolumeMount{volumeMount}
   140  		podSpec.Volumes = []v1.Volume{volume}
   141  
   142  	} else if volMode == VolumeModeFileSystem {
   143  		jobContainer.Command = []string{"/bin/sh", "-c"}
   144  		jobContainer.Args = []string{"find /tmp -mindepth 1 -maxdepth 1 -print0 | xargs -0 rm -rf"}
   145  		volume, volumeMount := getVolumeMounts(bd.Spec.FileSystem.Mountpoint, "/tmp", mountName)
   146  
   147  		jobContainer.VolumeMounts = []v1.VolumeMount{volumeMount}
   148  		podSpec.Volumes = []v1.Volume{volume}
   149  	}
   150  
   151  	podSpec.Tolerations = tolerations
   152  	podSpec.ServiceAccountName = getServiceAccount()
   153  	podSpec.Containers = []v1.Container{jobContainer}
   154  	podSpec.NodeSelector = map[string]string{controller.KubernetesHostNameLabel: nodeName}
   155  	podSpec.ImagePullSecrets = env.GetOpenEBSImagePullSecrets()
   156  	podTemplate := v1.Pod{}
   157  	podTemplate.Spec = podSpec
   158  
   159  	labels := map[string]string{
   160  		controller.KubernetesHostNameLabel: nodeName,
   161  		BDLabel:                            bd.Name,
   162  	}
   163  
   164  	podTemplate.ObjectMeta = metav1.ObjectMeta{
   165  		Name:      generateCleaningJobName(bd.Name),
   166  		Namespace: namespace,
   167  		Labels:    labels,
   168  	}
   169  
   170  	job := &batchv1.Job{}
   171  	job.ObjectMeta = podTemplate.ObjectMeta
   172  	job.Spec.Template.Spec = podTemplate.Spec
   173  	job.Spec.Template.Spec.RestartPolicy = v1.RestartPolicyOnFailure
   174  
   175  	return job, nil
   176  }
   177  
   178  // NewJobController returns a job controller struct which can be used to get the status
   179  // of the running job
   180  func NewJobController(client client.Client, namespace string) *jobController {
   181  	return &jobController{
   182  		client:    client,
   183  		namespace: namespace,
   184  	}
   185  }
   186  
   187  func (c *jobController) IsCleaningJobRunning(bdName string) bool {
   188  	jobName := generateCleaningJobName(bdName)
   189  	objKey := client.ObjectKey{
   190  		Namespace: c.namespace,
   191  		Name:      jobName,
   192  	}
   193  	job := &batchv1.Job{}
   194  
   195  	err := c.client.Get(context.TODO(), objKey, job)
   196  	if errors.IsNotFound(err) {
   197  		return false
   198  	}
   199  
   200  	if err != nil {
   201  		// failed to check whether it is running, assuming job is still running
   202  		return true
   203  	}
   204  
   205  	return job.Status.Succeeded <= 0
   206  }
   207  
   208  func (c *jobController) RemoveJob(bdName string) (CleanupState, error) {
   209  	jobName := generateCleaningJobName(bdName)
   210  	objKey := client.ObjectKey{
   211  		Namespace: c.namespace,
   212  		Name:      jobName,
   213  	}
   214  	job := &batchv1.Job{}
   215  
   216  	err := c.client.Get(context.TODO(), objKey, job)
   217  	if err != nil {
   218  		if errors.IsNotFound(err) {
   219  			return CleanupStateNotFound, nil
   220  		}
   221  		return CleanupStateUnknown, err
   222  	}
   223  	if job.Status.Succeeded == 0 {
   224  		return CleanupStateRunning, nil
   225  	}
   226  
   227  	// cancel the job
   228  	err = c.CancelJob(bdName)
   229  	if err != nil {
   230  		return CleanupStateUnknown, err
   231  	}
   232  
   233  	return CleanupStateSucceeded, nil
   234  }
   235  
   236  // CancelJob deletes a job, if it is present. if the job is not present, it will return an error.
   237  func (c *jobController) CancelJob(bdName string) error {
   238  	jobName := generateCleaningJobName(bdName)
   239  	objKey := client.ObjectKey{
   240  		Namespace: c.namespace,
   241  		Name:      jobName,
   242  	}
   243  	job := &batchv1.Job{}
   244  	err := c.client.Get(context.TODO(), objKey, job)
   245  
   246  	err = c.client.Delete(context.TODO(), job, client.PropagationPolicy(metav1.DeletePropagationForeground))
   247  	return err
   248  }
   249  
   250  func generateCleaningJobName(bdName string) string {
   251  	return JobNamePrefix + bdName
   252  }
   253  
   254  // GetNodeName gets the Node name from BlockDevice
   255  func GetNodeName(bd *v1alpha1.BlockDevice) string {
   256  	return bd.Spec.NodeAttributes.NodeName
   257  }
   258  
   259  // getVolumeMounts returns the volume and volume mount for the given hostpath and
   260  // mountpath
   261  func getVolumeMounts(hostPath, mountPath, mountName string) (v1.Volume, v1.VolumeMount) {
   262  	volumes := v1.Volume{
   263  		Name: mountName,
   264  		VolumeSource: v1.VolumeSource{
   265  			HostPath: &v1.HostPathVolumeSource{
   266  				Path: hostPath,
   267  			},
   268  		},
   269  	}
   270  
   271  	volumeMount := v1.VolumeMount{
   272  		Name:      mountName,
   273  		MountPath: mountPath,
   274  	}
   275  
   276  	return volumes, volumeMount
   277  }