github.com/kubeshop/testkube@v1.17.23/pkg/cronjob/client.go (about)

     1  package cronjob
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"fmt"
     7  	"strings"
     8  
     9  	"go.uber.org/zap"
    10  	v1 "k8s.io/api/batch/v1"
    11  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    12  	"k8s.io/apimachinery/pkg/util/yaml"
    13  	batchv1 "k8s.io/client-go/applyconfigurations/batch/v1"
    14  	"k8s.io/client-go/kubernetes"
    15  	kyaml "sigs.k8s.io/kustomize/kyaml/yaml"
    16  	"sigs.k8s.io/kustomize/kyaml/yaml/merge2"
    17  
    18  	"github.com/kubeshop/testkube/pkg/k8sclient"
    19  	"github.com/kubeshop/testkube/pkg/log"
    20  	"github.com/kubeshop/testkube/pkg/utils"
    21  )
    22  
    23  // Client data struct for managing running cron jobs
    24  type Client struct {
    25  	ClientSet       *kubernetes.Clientset
    26  	Log             *zap.SugaredLogger
    27  	serviceName     string
    28  	servicePort     int
    29  	cronJobTemplate string
    30  	Namespace       string
    31  }
    32  
    33  type CronJobOptions struct {
    34  	Schedule                  string
    35  	Resource                  string
    36  	Data                      string
    37  	Labels                    map[string]string
    38  	CronJobTemplate           string
    39  	CronJobTemplateExtensions string
    40  }
    41  
    42  type templateParameters struct {
    43  	Id                        string
    44  	Name                      string
    45  	Namespace                 string
    46  	ServiceName               string
    47  	ServicePort               int
    48  	Schedule                  string
    49  	Resource                  string
    50  	CronJobTemplate           string
    51  	CronJobTemplateExtensions string
    52  	Data                      string
    53  	Labels                    map[string]string
    54  }
    55  
    56  // NewClient is a method to create new cron job client
    57  func NewClient(serviceName string, servicePort int, cronJobTemplate string, namespace string) (*Client, error) {
    58  	clientSet, err := k8sclient.ConnectToK8s()
    59  	if err != nil {
    60  		return nil, err
    61  	}
    62  
    63  	return &Client{
    64  		ClientSet:       clientSet,
    65  		Log:             log.DefaultLogger,
    66  		serviceName:     serviceName,
    67  		servicePort:     servicePort,
    68  		cronJobTemplate: cronJobTemplate,
    69  		Namespace:       namespace,
    70  	}, nil
    71  }
    72  
    73  // Get is a method to retrieve an existing cron job
    74  func (c *Client) Get(name string) (*v1.CronJob, error) {
    75  	cronJobClient := c.ClientSet.BatchV1().CronJobs(c.Namespace)
    76  	ctx := context.Background()
    77  
    78  	cronJob, err := cronJobClient.Get(ctx, name, metav1.GetOptions{})
    79  	if err != nil {
    80  		return nil, err
    81  	}
    82  
    83  	return cronJob, nil
    84  }
    85  
    86  // Apply is a method to create or update a cron job
    87  func (c *Client) Apply(id, name string, options CronJobOptions) error {
    88  	template := c.cronJobTemplate
    89  	if options.CronJobTemplate != "" {
    90  		template = options.CronJobTemplate
    91  	}
    92  
    93  	cronJobClient := c.ClientSet.BatchV1().CronJobs(c.Namespace)
    94  	ctx := context.Background()
    95  
    96  	parameters := templateParameters{
    97  		Id:                        id,
    98  		Name:                      name,
    99  		Namespace:                 c.Namespace,
   100  		ServiceName:               c.serviceName,
   101  		ServicePort:               c.servicePort,
   102  		Schedule:                  options.Schedule,
   103  		Resource:                  options.Resource,
   104  		CronJobTemplate:           template,
   105  		CronJobTemplateExtensions: options.CronJobTemplateExtensions,
   106  		Data:                      options.Data,
   107  		Labels:                    options.Labels,
   108  	}
   109  
   110  	cronJobSpec, err := NewApplySpec(c.Log, parameters)
   111  	if err != nil {
   112  		return err
   113  	}
   114  
   115  	if _, err := cronJobClient.Apply(ctx, cronJobSpec, metav1.ApplyOptions{
   116  		FieldManager: "application/apply-patch"}); err != nil {
   117  		return err
   118  	}
   119  
   120  	return nil
   121  }
   122  
   123  // UpdateLabels is a method to update an existing cron job labels
   124  func (c *Client) UpdateLabels(cronJobSpec *v1.CronJob, oldLabels, newLabels map[string]string) error {
   125  	cronJobClient := c.ClientSet.BatchV1().CronJobs(c.Namespace)
   126  	ctx := context.Background()
   127  
   128  	for key := range oldLabels {
   129  		delete(cronJobSpec.Labels, key)
   130  	}
   131  
   132  	for key, value := range newLabels {
   133  		cronJobSpec.Labels[key] = value
   134  	}
   135  
   136  	if _, err := cronJobClient.Update(ctx, cronJobSpec, metav1.UpdateOptions{}); err != nil {
   137  		return err
   138  	}
   139  
   140  	return nil
   141  }
   142  
   143  // Delete is a method to delete an existing cron job
   144  func (c *Client) Delete(name string) error {
   145  	cronJobClient := c.ClientSet.BatchV1().CronJobs(c.Namespace)
   146  	ctx := context.Background()
   147  
   148  	if err := cronJobClient.Delete(ctx, name, metav1.DeleteOptions{}); err != nil {
   149  		return err
   150  	}
   151  
   152  	return nil
   153  }
   154  
   155  // DeleteAll is a method to delete all existing cron jobs
   156  func (c *Client) DeleteAll(resource, selector string) error {
   157  	cronJobClient := c.ClientSet.BatchV1().CronJobs(c.Namespace)
   158  	ctx := context.Background()
   159  
   160  	filter := fmt.Sprintf("testkube=%s", resource)
   161  	if selector != "" {
   162  		filter += "," + selector
   163  	}
   164  
   165  	if err := cronJobClient.DeleteCollection(ctx, metav1.DeleteOptions{},
   166  		metav1.ListOptions{LabelSelector: filter}); err != nil {
   167  		return err
   168  	}
   169  
   170  	return nil
   171  }
   172  
   173  // NewApplySpec is a method to return cron job apply spec
   174  func NewApplySpec(log *zap.SugaredLogger, parameters templateParameters) (*batchv1.CronJobApplyConfiguration, error) {
   175  	tmpl, err := utils.NewTemplate("cronJob").Parse(parameters.CronJobTemplate)
   176  	if err != nil {
   177  		return nil, fmt.Errorf("creating cron job spec from options.CronJobTemplate error: %w", err)
   178  	}
   179  
   180  	parameters.Data = strings.ReplaceAll(parameters.Data, "'", "''''")
   181  	var buffer bytes.Buffer
   182  	if err = tmpl.ExecuteTemplate(&buffer, "cronJob", parameters); err != nil {
   183  		return nil, fmt.Errorf("executing cron job spec template: %w", err)
   184  	}
   185  
   186  	var cronJob batchv1.CronJobApplyConfiguration
   187  	cronJobSpec := buffer.String()
   188  	if parameters.CronJobTemplateExtensions != "" {
   189  		tmplExt, err := utils.NewTemplate("cronJobExt").Parse(parameters.CronJobTemplateExtensions)
   190  		if err != nil {
   191  			return nil, fmt.Errorf("creating cron job extensions spec from default template error: %w", err)
   192  		}
   193  
   194  		var bufferExt bytes.Buffer
   195  		if err = tmplExt.ExecuteTemplate(&bufferExt, "cronJobExt", parameters); err != nil {
   196  			return nil, fmt.Errorf("executing cron job extensions spec default template: %w", err)
   197  		}
   198  
   199  		if cronJobSpec, err = merge2.MergeStrings(bufferExt.String(), cronJobSpec, false, kyaml.MergeOptions{}); err != nil {
   200  			return nil, fmt.Errorf("merging cron job spec templates: %w", err)
   201  		}
   202  	}
   203  
   204  	log.Debug("Cron job specification", cronJobSpec)
   205  	decoder := yaml.NewYAMLOrJSONDecoder(bytes.NewBufferString(cronJobSpec), len(cronJobSpec))
   206  	if err := decoder.Decode(&cronJob); err != nil {
   207  		return nil, fmt.Errorf("decoding cron job spec error: %w", err)
   208  	}
   209  
   210  	for key, value := range parameters.Labels {
   211  		cronJob.Labels[key] = value
   212  	}
   213  
   214  	return &cronJob, nil
   215  }
   216  
   217  // GetMetadataName returns cron job metadata name
   218  func GetMetadataName(name, resource string) string {
   219  	return fmt.Sprintf("%s-%s", name, resource)
   220  }