...

Source file src/github.com/chaos-mesh/chaos-mesh/pkg/apiserver/schedule/schedule.go

Documentation: github.com/chaos-mesh/chaos-mesh/pkg/apiserver/schedule

     1  // Copyright 2021 Chaos Mesh Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // See the License for the specific language governing permissions and
    12  // limitations under the License.
    13  
    14  package schedule
    15  
    16  import (
    17  	"context"
    18  	"encoding/json"
    19  	"fmt"
    20  	"net/http"
    21  	"reflect"
    22  	"sort"
    23  	"strings"
    24  	"time"
    25  
    26  	"github.com/gin-gonic/gin"
    27  	"github.com/jinzhu/gorm"
    28  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    29  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    30  	"k8s.io/apimachinery/pkg/runtime"
    31  	"k8s.io/apimachinery/pkg/types"
    32  	"k8s.io/client-go/util/retry"
    33  	ctrl "sigs.k8s.io/controller-runtime"
    34  	"sigs.k8s.io/controller-runtime/pkg/client"
    35  	"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
    36  
    37  	"github.com/chaos-mesh/chaos-mesh/api/v1alpha1"
    38  	"github.com/chaos-mesh/chaos-mesh/pkg/apiserver/utils"
    39  	"github.com/chaos-mesh/chaos-mesh/pkg/clientpool"
    40  	dashboardconfig "github.com/chaos-mesh/chaos-mesh/pkg/config/dashboard"
    41  	"github.com/chaos-mesh/chaos-mesh/pkg/core"
    42  )
    43  
    44  var log = ctrl.Log.WithName("schedule api")
    45  
    46  // Service defines a handler service for experiments.
    47  type Service struct {
    48  	schedule core.ScheduleStore
    49  	event    core.EventStore
    50  	conf     *dashboardconfig.ChaosDashboardConfig
    51  	scheme   *runtime.Scheme
    52  }
    53  
    54  // NewService returns an experiment service instance.
    55  func NewService(
    56  	schedule core.ScheduleStore,
    57  	event core.EventStore,
    58  	conf *dashboardconfig.ChaosDashboardConfig,
    59  	scheme *runtime.Scheme,
    60  ) *Service {
    61  	return &Service{
    62  		schedule: schedule,
    63  		event:    event,
    64  		conf:     conf,
    65  		scheme:   scheme,
    66  	}
    67  }
    68  
    69  // Register mounts HTTP handler on the mux.
    70  func Register(r *gin.RouterGroup, s *Service) {
    71  	endpoint := r.Group("/schedules")
    72  
    73  	endpoint.GET("", s.listSchedules)
    74  	endpoint.GET("/:uid", s.getScheduleDetail)
    75  	endpoint.POST("/", s.createSchedule)
    76  	endpoint.PUT("/", s.updateSchedule)
    77  	endpoint.DELETE("/:uid", s.deleteSchedule)
    78  	endpoint.DELETE("/", s.batchDeleteSchedule)
    79  	endpoint.PUT("/pause/:uid", s.pauseSchedule)
    80  	endpoint.PUT("/start/:uid", s.startSchedule)
    81  }
    82  
    83  // Base represents the base info of an experiment.
    84  type Base struct {
    85  	Kind      string `json:"kind"`
    86  	Namespace string `json:"namespace"`
    87  	Name      string `json:"name"`
    88  }
    89  
    90  // Schedule defines the basic information of a Schedule object
    91  type Schedule struct {
    92  	Base
    93  	UID     string `json:"uid"`
    94  	Created string `json:"created_at"`
    95  	Status  string `json:"status"`
    96  }
    97  
    98  // Detail represents an experiment instance.
    99  type Detail struct {
   100  	Schedule
   101  	YAML           core.KubeObjectDesc `json:"kube_object"`
   102  	ExperimentUIDs []string            `json:"experiment_uids"`
   103  }
   104  
   105  type parseScheduleFunc func(*core.ScheduleInfo) v1alpha1.ScheduleItem
   106  
   107  // StatusResponse defines a common status struct.
   108  type StatusResponse struct {
   109  	Status string `json:"status"`
   110  }
   111  
   112  type pauseFlag bool
   113  
   114  const (
   115  	PauseSchedule pauseFlag = true
   116  	StartSchedule pauseFlag = false
   117  )
   118  
   119  // @Summary Create a new schedule experiment.
   120  // @Description Create a new schedule experiment.
   121  // @Tags schedules
   122  // @Produce json
   123  // @Param request body core.ScheduleInfo true "Request body"
   124  // @Success 200 {object} core.ScheduleInfo
   125  // @Failure 400 {object} utils.APIError
   126  // @Failure 500 {object} utils.APIError
   127  // @Router /schedules [post]
   128  func (s *Service) createSchedule(c *gin.Context) {
   129  	kubeCli, err := clientpool.ExtractTokenAndGetClient(c.Request.Header)
   130  	if err != nil {
   131  		_ = c.Error(utils.ErrInvalidRequest.WrapWithNoMessage(err))
   132  		return
   133  	}
   134  
   135  	exp := &core.ScheduleInfo{}
   136  	if err := c.ShouldBindJSON(exp); err != nil {
   137  		c.Status(http.StatusBadRequest)
   138  		_ = c.Error(utils.ErrInvalidRequest.WrapWithNoMessage(err))
   139  		return
   140  	}
   141  
   142  	sch := &v1alpha1.Schedule{
   143  		ObjectMeta: metav1.ObjectMeta{
   144  			Name:        exp.Name,
   145  			Namespace:   exp.Namespace,
   146  			Labels:      exp.Labels,
   147  			Annotations: exp.Annotations,
   148  		},
   149  		Spec: v1alpha1.ScheduleSpec{
   150  			Schedule:          exp.Schedule,
   151  			ConcurrencyPolicy: exp.ConcurrencyPolicy,
   152  			HistoryLimit:      exp.HistoryLimit,
   153  			Type:              v1alpha1.ScheduleTemplateType(exp.Target.Kind),
   154  		},
   155  	}
   156  	if exp.StartingDeadlineSeconds != nil {
   157  		sch.Spec.StartingDeadlineSeconds = exp.StartingDeadlineSeconds
   158  	}
   159  
   160  	parseFuncs := map[string]parseScheduleFunc{
   161  		v1alpha1.KindPodChaos:     parsePodChaos,
   162  		v1alpha1.KindNetworkChaos: parseNetworkChaos,
   163  		v1alpha1.KindIOChaos:      parseIOChaos,
   164  		v1alpha1.KindStressChaos:  parseStressChaos,
   165  		v1alpha1.KindTimeChaos:    parseTimeChaos,
   166  		v1alpha1.KindKernelChaos:  parseKernelChaos,
   167  		v1alpha1.KindDNSChaos:     parseDNSChaos,
   168  		v1alpha1.KindAWSChaos:     parseAWSChaos,
   169  		v1alpha1.KindGCPChaos:     parseGCPChaos,
   170  	}
   171  
   172  	f, ok := parseFuncs[exp.Target.Kind]
   173  	if !ok {
   174  		c.Status(http.StatusBadRequest)
   175  		_ = c.Error(utils.ErrInvalidRequest.New(exp.Target.Kind + " is not supported"))
   176  		return
   177  	}
   178  	embedChaos := f(exp)
   179  	sch.Spec.ScheduleItem = embedChaos
   180  
   181  	if err := kubeCli.Create(context.Background(), sch); err != nil {
   182  		c.Status(http.StatusInternalServerError)
   183  		_ = c.Error(utils.ErrInternalServer.WrapWithNoMessage(err))
   184  		return
   185  	}
   186  
   187  	c.JSON(http.StatusOK, exp)
   188  }
   189  
   190  func parsePodChaos(exp *core.ScheduleInfo) v1alpha1.ScheduleItem {
   191  	chaos := &v1alpha1.PodChaos{
   192  		ObjectMeta: metav1.ObjectMeta{
   193  			Name:        exp.Name,
   194  			Namespace:   exp.Namespace,
   195  			Labels:      exp.Labels,
   196  			Annotations: exp.Annotations,
   197  		},
   198  		Spec: v1alpha1.PodChaosSpec{
   199  			ContainerSelector: v1alpha1.ContainerSelector{
   200  				PodSelector: v1alpha1.PodSelector{
   201  					Selector: exp.Scope.ParseSelector(),
   202  					Mode:     v1alpha1.PodMode(exp.Scope.Mode),
   203  					Value:    exp.Scope.Value,
   204  				},
   205  				ContainerNames: exp.Target.PodChaos.ContainerNames,
   206  			},
   207  			Action:      v1alpha1.PodChaosAction(exp.Target.PodChaos.Action),
   208  			GracePeriod: exp.Target.PodChaos.GracePeriod,
   209  		},
   210  	}
   211  
   212  	if exp.Duration != "" {
   213  		chaos.Spec.Duration = &exp.Duration
   214  	}
   215  
   216  	return v1alpha1.ScheduleItem{
   217  		EmbedChaos: v1alpha1.EmbedChaos{PodChaos: &chaos.Spec},
   218  	}
   219  }
   220  
   221  func parseNetworkChaos(exp *core.ScheduleInfo) v1alpha1.ScheduleItem {
   222  	chaos := &v1alpha1.NetworkChaos{
   223  		ObjectMeta: metav1.ObjectMeta{
   224  			Name:        exp.Name,
   225  			Namespace:   exp.Namespace,
   226  			Labels:      exp.Labels,
   227  			Annotations: exp.Annotations,
   228  		},
   229  		Spec: v1alpha1.NetworkChaosSpec{
   230  			PodSelector: v1alpha1.PodSelector{
   231  				Selector: exp.Scope.ParseSelector(),
   232  				Mode:     v1alpha1.PodMode(exp.Scope.Mode),
   233  				Value:    exp.Scope.Value,
   234  			},
   235  			Action: v1alpha1.NetworkChaosAction(exp.Target.NetworkChaos.Action),
   236  			TcParameter: v1alpha1.TcParameter{
   237  				Delay:     exp.Target.NetworkChaos.Delay,
   238  				Loss:      exp.Target.NetworkChaos.Loss,
   239  				Duplicate: exp.Target.NetworkChaos.Duplicate,
   240  				Corrupt:   exp.Target.NetworkChaos.Corrupt,
   241  				Bandwidth: exp.Target.NetworkChaos.Bandwidth,
   242  			},
   243  			Direction:       v1alpha1.Direction(exp.Target.NetworkChaos.Direction),
   244  			ExternalTargets: exp.Target.NetworkChaos.ExternalTargets,
   245  		},
   246  	}
   247  
   248  	if exp.Target.NetworkChaos.TargetScope != nil {
   249  		chaos.Spec.Target = &v1alpha1.PodSelector{
   250  			Selector: exp.Target.NetworkChaos.TargetScope.ParseSelector(),
   251  			Mode:     v1alpha1.PodMode(exp.Target.NetworkChaos.TargetScope.Mode),
   252  			Value:    exp.Target.NetworkChaos.TargetScope.Value,
   253  		}
   254  	}
   255  
   256  	if exp.Duration != "" {
   257  		chaos.Spec.Duration = &exp.Duration
   258  	}
   259  
   260  	return v1alpha1.ScheduleItem{
   261  		EmbedChaos: v1alpha1.EmbedChaos{NetworkChaos: &chaos.Spec},
   262  	}
   263  }
   264  
   265  func parseIOChaos(exp *core.ScheduleInfo) v1alpha1.ScheduleItem {
   266  	chaos := &v1alpha1.IOChaos{
   267  		ObjectMeta: metav1.ObjectMeta{
   268  			Name:        exp.Name,
   269  			Namespace:   exp.Namespace,
   270  			Labels:      exp.Labels,
   271  			Annotations: exp.Annotations,
   272  		},
   273  		Spec: v1alpha1.IOChaosSpec{
   274  			ContainerSelector: v1alpha1.ContainerSelector{
   275  				PodSelector: v1alpha1.PodSelector{
   276  					Selector: exp.Scope.ParseSelector(),
   277  					Mode:     v1alpha1.PodMode(exp.Scope.Mode),
   278  					Value:    exp.Scope.Value,
   279  				},
   280  				ContainerNames: []string{exp.Target.IOChaos.ContainerName},
   281  			},
   282  			Action:     v1alpha1.IOChaosType(exp.Target.IOChaos.Action),
   283  			Delay:      exp.Target.IOChaos.Delay,
   284  			Errno:      exp.Target.IOChaos.Errno,
   285  			Attr:       exp.Target.IOChaos.Attr,
   286  			Mistake:    exp.Target.IOChaos.Mistake,
   287  			Path:       exp.Target.IOChaos.Path,
   288  			Methods:    exp.Target.IOChaos.Methods,
   289  			Percent:    exp.Target.IOChaos.Percent,
   290  			VolumePath: exp.Target.IOChaos.VolumePath,
   291  		},
   292  	}
   293  
   294  	if exp.Duration != "" {
   295  		chaos.Spec.Duration = &exp.Duration
   296  	}
   297  
   298  	return v1alpha1.ScheduleItem{
   299  		EmbedChaos: v1alpha1.EmbedChaos{IOChaos: &chaos.Spec},
   300  	}
   301  }
   302  
   303  func parseTimeChaos(exp *core.ScheduleInfo) v1alpha1.ScheduleItem {
   304  	chaos := &v1alpha1.TimeChaos{
   305  		ObjectMeta: metav1.ObjectMeta{
   306  			Name:        exp.Name,
   307  			Namespace:   exp.Namespace,
   308  			Labels:      exp.Labels,
   309  			Annotations: exp.Annotations,
   310  		},
   311  		Spec: v1alpha1.TimeChaosSpec{
   312  			ContainerSelector: v1alpha1.ContainerSelector{
   313  				PodSelector: v1alpha1.PodSelector{
   314  					Selector: exp.Scope.ParseSelector(),
   315  					Mode:     v1alpha1.PodMode(exp.Scope.Mode),
   316  					Value:    exp.Scope.Value,
   317  				},
   318  				ContainerNames: exp.Target.TimeChaos.ContainerNames,
   319  			},
   320  			TimeOffset: exp.Target.TimeChaos.TimeOffset,
   321  			ClockIds:   exp.Target.TimeChaos.ClockIDs,
   322  		},
   323  	}
   324  
   325  	if exp.Duration != "" {
   326  		chaos.Spec.Duration = &exp.Duration
   327  	}
   328  
   329  	return v1alpha1.ScheduleItem{
   330  		EmbedChaos: v1alpha1.EmbedChaos{TimeChaos: &chaos.Spec},
   331  	}
   332  }
   333  
   334  func parseKernelChaos(exp *core.ScheduleInfo) v1alpha1.ScheduleItem {
   335  	chaos := &v1alpha1.KernelChaos{
   336  		ObjectMeta: metav1.ObjectMeta{
   337  			Name:        exp.Name,
   338  			Namespace:   exp.Namespace,
   339  			Labels:      exp.Labels,
   340  			Annotations: exp.Annotations,
   341  		},
   342  		Spec: v1alpha1.KernelChaosSpec{
   343  			PodSelector: v1alpha1.PodSelector{
   344  				Selector: exp.Scope.ParseSelector(),
   345  				Mode:     v1alpha1.PodMode(exp.Scope.Mode),
   346  				Value:    exp.Scope.Value,
   347  			},
   348  			FailKernRequest: exp.Target.KernelChaos.FailKernRequest,
   349  		},
   350  	}
   351  
   352  	if exp.Duration != "" {
   353  		chaos.Spec.Duration = &exp.Duration
   354  	}
   355  
   356  	return v1alpha1.ScheduleItem{
   357  		EmbedChaos: v1alpha1.EmbedChaos{KernelChaos: &chaos.Spec},
   358  	}
   359  }
   360  
   361  func parseStressChaos(exp *core.ScheduleInfo) v1alpha1.ScheduleItem {
   362  	var stressors *v1alpha1.Stressors
   363  
   364  	// Error checking
   365  	if exp.Target.StressChaos.Stressors.CPUStressor.Workers <= 0 && exp.Target.StressChaos.Stressors.MemoryStressor.Workers > 0 {
   366  		stressors = &v1alpha1.Stressors{
   367  			MemoryStressor: exp.Target.StressChaos.Stressors.MemoryStressor,
   368  		}
   369  	} else if exp.Target.StressChaos.Stressors.MemoryStressor.Workers <= 0 && exp.Target.StressChaos.Stressors.CPUStressor.Workers > 0 {
   370  		stressors = &v1alpha1.Stressors{
   371  			CPUStressor: exp.Target.StressChaos.Stressors.CPUStressor,
   372  		}
   373  	} else {
   374  		stressors = exp.Target.StressChaos.Stressors
   375  	}
   376  
   377  	chaos := &v1alpha1.StressChaos{
   378  		ObjectMeta: metav1.ObjectMeta{
   379  			Name:        exp.Name,
   380  			Namespace:   exp.Namespace,
   381  			Labels:      exp.Labels,
   382  			Annotations: exp.Annotations,
   383  		},
   384  		Spec: v1alpha1.StressChaosSpec{
   385  			ContainerSelector: v1alpha1.ContainerSelector{
   386  				PodSelector: v1alpha1.PodSelector{
   387  					Selector: exp.Scope.ParseSelector(),
   388  					Mode:     v1alpha1.PodMode(exp.Scope.Mode),
   389  					Value:    exp.Scope.Value,
   390  				},
   391  				ContainerNames: exp.Target.StressChaos.ContainerNames,
   392  			},
   393  			Stressors:         stressors,
   394  			StressngStressors: exp.Target.StressChaos.StressngStressors,
   395  		},
   396  	}
   397  
   398  	if exp.Duration != "" {
   399  		chaos.Spec.Duration = &exp.Duration
   400  	}
   401  
   402  	return v1alpha1.ScheduleItem{
   403  		EmbedChaos: v1alpha1.EmbedChaos{StressChaos: &chaos.Spec},
   404  	}
   405  }
   406  
   407  func parseDNSChaos(exp *core.ScheduleInfo) v1alpha1.ScheduleItem {
   408  	chaos := &v1alpha1.DNSChaos{
   409  		ObjectMeta: metav1.ObjectMeta{
   410  			Name:        exp.Name,
   411  			Namespace:   exp.Namespace,
   412  			Labels:      exp.Labels,
   413  			Annotations: exp.Annotations,
   414  		},
   415  		Spec: v1alpha1.DNSChaosSpec{
   416  			Action: v1alpha1.DNSChaosAction(exp.Target.DNSChaos.Action),
   417  			ContainerSelector: v1alpha1.ContainerSelector{
   418  				PodSelector: v1alpha1.PodSelector{
   419  					Selector: exp.Scope.ParseSelector(),
   420  					Mode:     v1alpha1.PodMode(exp.Scope.Mode),
   421  					Value:    exp.Scope.Value,
   422  				},
   423  				ContainerNames: exp.Target.DNSChaos.ContainerNames,
   424  			},
   425  			DomainNamePatterns: exp.Target.DNSChaos.DomainNamePatterns,
   426  		},
   427  	}
   428  
   429  	if exp.Duration != "" {
   430  		chaos.Spec.Duration = &exp.Duration
   431  	}
   432  
   433  	return v1alpha1.ScheduleItem{
   434  		EmbedChaos: v1alpha1.EmbedChaos{DNSChaos: &chaos.Spec},
   435  	}
   436  }
   437  
   438  func parseAWSChaos(exp *core.ScheduleInfo) v1alpha1.ScheduleItem {
   439  	chaos := &v1alpha1.AWSChaos{
   440  		ObjectMeta: metav1.ObjectMeta{
   441  			Name:        exp.Name,
   442  			Namespace:   exp.Namespace,
   443  			Labels:      exp.Labels,
   444  			Annotations: exp.Annotations,
   445  		},
   446  		Spec: v1alpha1.AWSChaosSpec{
   447  			Action:     v1alpha1.AWSChaosAction(exp.Target.AWSChaos.Action),
   448  			SecretName: exp.Target.AWSChaos.SecretName,
   449  			AWSSelector: v1alpha1.AWSSelector{
   450  				AWSRegion:   exp.Target.AWSChaos.AWSRegion,
   451  				Ec2Instance: exp.Target.AWSChaos.Ec2Instance,
   452  				EbsVolume:   exp.Target.AWSChaos.EbsVolume,
   453  				DeviceName:  exp.Target.AWSChaos.DeviceName,
   454  			},
   455  		},
   456  	}
   457  
   458  	if exp.Duration != "" {
   459  		chaos.Spec.Duration = &exp.Duration
   460  	}
   461  
   462  	return v1alpha1.ScheduleItem{
   463  		EmbedChaos: v1alpha1.EmbedChaos{AWSChaos: &chaos.Spec},
   464  	}
   465  }
   466  
   467  func parseGCPChaos(exp *core.ScheduleInfo) v1alpha1.ScheduleItem {
   468  	chaos := &v1alpha1.GCPChaos{
   469  		ObjectMeta: metav1.ObjectMeta{
   470  			Name:        exp.Name,
   471  			Namespace:   exp.Namespace,
   472  			Labels:      exp.Labels,
   473  			Annotations: exp.Annotations,
   474  		},
   475  		Spec: v1alpha1.GCPChaosSpec{
   476  			Action:     v1alpha1.GCPChaosAction(exp.Target.GCPChaos.Action),
   477  			SecretName: exp.Target.GCPChaos.SecretName,
   478  			GCPSelector: v1alpha1.GCPSelector{
   479  				Project:     exp.Target.GCPChaos.Project,
   480  				Zone:        exp.Target.GCPChaos.Zone,
   481  				Instance:    exp.Target.GCPChaos.Instance,
   482  				DeviceNames: exp.Target.GCPChaos.DeviceNames,
   483  			},
   484  		},
   485  	}
   486  
   487  	if exp.Duration != "" {
   488  		chaos.Spec.Duration = &exp.Duration
   489  	}
   490  
   491  	return v1alpha1.ScheduleItem{
   492  		EmbedChaos: v1alpha1.EmbedChaos{GCPChaos: &chaos.Spec},
   493  	}
   494  }
   495  
   496  // @Summary Get chaos schedules from Kubernetes cluster.
   497  // @Description Get chaos schedules from Kubernetes cluster.
   498  // @Tags schedules
   499  // @Produce json
   500  // @Param namespace query string false "namespace"
   501  // @Param name query string false "name"
   502  // @Success 200 {array} Schedule
   503  // @Router /Schedules [get]
   504  // @Failure 500 {object} utils.APIError
   505  func (s *Service) listSchedules(c *gin.Context) {
   506  	kubeCli, err := clientpool.ExtractTokenAndGetClient(c.Request.Header)
   507  	if err != nil {
   508  		_ = c.Error(utils.ErrInvalidRequest.WrapWithNoMessage(err))
   509  		return
   510  	}
   511  
   512  	name := c.Query("name")
   513  	ns := c.Query("namespace")
   514  
   515  	if !s.conf.ClusterScoped {
   516  		log.Info("Overwrite namespace within namespace scoped mode", "origin", ns, "new", s.conf.TargetNamespace)
   517  		ns = s.conf.TargetNamespace
   518  	}
   519  
   520  	ScheduleList := v1alpha1.ScheduleList{}
   521  	sches := make([]*Schedule, 0)
   522  	if err := kubeCli.List(context.Background(), &ScheduleList, &client.ListOptions{Namespace: ns}); err != nil {
   523  		c.Status(http.StatusInternalServerError)
   524  		utils.SetErrorForGinCtx(c, err)
   525  		return
   526  	}
   527  	for _, schedule := range ScheduleList.Items {
   528  		if name != "" && schedule.Name != name {
   529  			continue
   530  		}
   531  		sches = append(sches, &Schedule{
   532  			Base: Base{
   533  				Kind:      string(schedule.Spec.Type),
   534  				Namespace: schedule.Namespace,
   535  				Name:      schedule.Name,
   536  			},
   537  			UID:     string(schedule.UID),
   538  			Created: schedule.CreationTimestamp.Format(time.RFC3339),
   539  			Status:  string(utils.GetScheduleState(schedule)),
   540  		})
   541  	}
   542  
   543  	sort.Slice(sches, func(i, j int) bool {
   544  		return sches[i].Created > sches[j].Created
   545  	})
   546  
   547  	c.JSON(http.StatusOK, sches)
   548  }
   549  
   550  // @Summary Get detailed information about the specified schedule experiment.
   551  // @Description Get detailed information about the specified schedule experiment.
   552  // @Tags experiments
   553  // @Produce json
   554  // @Param uid path string true "uid"
   555  // @Router /schedules/{uid} [GET]
   556  // @Success 200 {object} Detail
   557  // @Failure 400 {object} utils.APIError
   558  // @Failure 500 {object} utils.APIError
   559  func (s *Service) getScheduleDetail(c *gin.Context) {
   560  	var (
   561  		sch       *core.Schedule
   562  		schDetail Detail
   563  	)
   564  
   565  	kubeCli, err := clientpool.ExtractTokenAndGetClient(c.Request.Header)
   566  	if err != nil {
   567  		_ = c.Error(utils.ErrInvalidRequest.WrapWithNoMessage(err))
   568  		return
   569  	}
   570  
   571  	uid := c.Param("uid")
   572  	if sch, err = s.schedule.FindByUID(context.Background(), uid); err != nil {
   573  		if gorm.IsRecordNotFoundError(err) {
   574  			c.Status(http.StatusInternalServerError)
   575  			_ = c.Error(utils.ErrInvalidRequest.New("the schedule is not found"))
   576  		} else {
   577  			c.Status(http.StatusInternalServerError)
   578  			_ = c.Error(utils.ErrInternalServer.NewWithNoMessage())
   579  		}
   580  		return
   581  	}
   582  
   583  	ns := sch.Namespace
   584  	name := sch.Name
   585  
   586  	if !s.conf.ClusterScoped && ns != s.conf.TargetNamespace {
   587  		c.Status(http.StatusBadRequest)
   588  		_ = c.Error(utils.ErrInvalidRequest.New("the namespace is not supported in cluster scoped mode"))
   589  		return
   590  	}
   591  
   592  	schedule := &v1alpha1.Schedule{}
   593  
   594  	scheduleKey := types.NamespacedName{Namespace: ns, Name: name}
   595  	if err := kubeCli.Get(context.Background(), scheduleKey, schedule); err != nil {
   596  		c.Status(http.StatusInternalServerError)
   597  		_ = c.Error(utils.ErrInternalServer.WrapWithNoMessage(err))
   598  		return
   599  	}
   600  
   601  	gvk, err := apiutil.GVKForObject(schedule, s.scheme)
   602  	if err != nil {
   603  		c.Status(http.StatusInternalServerError)
   604  		_ = c.Error(utils.ErrInternalServer.WrapWithNoMessage(err))
   605  		return
   606  	}
   607  
   608  	UIDList := make([]string, 0)
   609  	kind, ok := v1alpha1.AllScheduleItemKinds()[string(schedule.Spec.Type)]
   610  	if !ok {
   611  		c.Status(http.StatusInternalServerError)
   612  		_ = c.Error(utils.ErrInvalidRequest.New("the kind is not supported"))
   613  		return
   614  	}
   615  	list := kind.ChaosList.DeepCopyObject()
   616  	selector, err := metav1.LabelSelectorAsSelector(&metav1.LabelSelector{
   617  		MatchLabels: map[string]string{"managed-by": schedule.Name},
   618  	})
   619  	if err != nil {
   620  		c.Status(http.StatusInternalServerError)
   621  		_ = c.Error(utils.ErrInternalServer.WrapWithNoMessage(err))
   622  		return
   623  	}
   624  
   625  	err = kubeCli.List(context.Background(), list, &client.ListOptions{
   626  		Namespace:     ns,
   627  		LabelSelector: selector,
   628  	})
   629  	if err != nil {
   630  		c.Status(http.StatusInternalServerError)
   631  		_ = c.Error(utils.ErrInternalServer.WrapWithNoMessage(err))
   632  		return
   633  	}
   634  	items := reflect.ValueOf(list).Elem().FieldByName("Items")
   635  	for i := 0; i < items.Len(); i++ {
   636  		if schedule.Spec.Type != v1alpha1.ScheduleTypeWorkflow {
   637  			item := items.Index(i).Addr().Interface().(v1alpha1.InnerObject)
   638  			UIDList = append(UIDList, item.GetChaos().UID)
   639  		} else {
   640  			workflow := items.Index(i).Addr().Interface().(*v1alpha1.Workflow)
   641  			UIDList = append(UIDList, string(workflow.UID))
   642  		}
   643  	}
   644  
   645  	schDetail = Detail{
   646  		Schedule: Schedule{
   647  			Base: Base{
   648  				Kind:      string(schedule.Spec.Type),
   649  				Namespace: schedule.Namespace,
   650  				Name:      schedule.Name,
   651  			},
   652  			UID:     string(schedule.UID),
   653  			Created: schedule.CreationTimestamp.Format(time.RFC3339),
   654  			Status:  string(utils.GetScheduleState(*schedule)),
   655  		},
   656  		YAML: core.KubeObjectDesc{
   657  			TypeMeta: metav1.TypeMeta{
   658  				Kind:       gvk.Kind,
   659  				APIVersion: gvk.GroupVersion().String(),
   660  			},
   661  			Meta: core.KubeObjectMeta{
   662  				Name:        schedule.Name,
   663  				Namespace:   schedule.Namespace,
   664  				Labels:      schedule.Labels,
   665  				Annotations: schedule.Annotations,
   666  			},
   667  			Spec: schedule.Spec,
   668  		},
   669  		ExperimentUIDs: UIDList,
   670  	}
   671  
   672  	c.JSON(http.StatusOK, schDetail)
   673  }
   674  
   675  // @Summary Delete the specified schedule experiment.
   676  // @Description Delete the specified schedule experiment.
   677  // @Tags schedules
   678  // @Produce json
   679  // @Param uid path string true "uid"
   680  // @Param force query string true "force" Enums(true, false)
   681  // @Success 200 {object} StatusResponse
   682  // @Failure 400 {object} utils.APIError
   683  // @Failure 404 {object} utils.APIError
   684  // @Failure 500 {object} utils.APIError
   685  // @Router /experiments/{uid} [delete]
   686  func (s *Service) deleteSchedule(c *gin.Context) {
   687  	var (
   688  		exp *core.Schedule
   689  	)
   690  
   691  	kubeCli, err := clientpool.ExtractTokenAndGetClient(c.Request.Header)
   692  	if err != nil {
   693  		_ = c.Error(utils.ErrInvalidRequest.WrapWithNoMessage(err))
   694  		return
   695  	}
   696  
   697  	uid := c.Param("uid")
   698  	if exp, err = s.schedule.FindByUID(context.Background(), uid); err != nil {
   699  		if gorm.IsRecordNotFoundError(err) {
   700  			c.Status(http.StatusInternalServerError)
   701  			_ = c.Error(utils.ErrInvalidRequest.New("the experiment is not found"))
   702  		} else {
   703  			c.Status(http.StatusInternalServerError)
   704  			_ = c.Error(utils.ErrInternalServer.NewWithNoMessage())
   705  		}
   706  		return
   707  	}
   708  
   709  	ns := exp.Namespace
   710  	name := exp.Name
   711  
   712  	ctx := context.TODO()
   713  	scheduleKey := types.NamespacedName{Namespace: ns, Name: name}
   714  	schedule := &v1alpha1.Schedule{}
   715  
   716  	if err := kubeCli.Get(ctx, scheduleKey, schedule); err != nil {
   717  		if apierrors.IsNotFound(err) {
   718  			c.Status(http.StatusNotFound)
   719  			_ = c.Error(utils.ErrNotFound.NewWithNoMessage())
   720  		} else {
   721  			c.Status(http.StatusInternalServerError)
   722  			_ = c.Error(utils.ErrInternalServer.WrapWithNoMessage(err))
   723  		}
   724  		return
   725  	}
   726  
   727  	if err := kubeCli.Delete(ctx, schedule, &client.DeleteOptions{}); err != nil {
   728  		if apierrors.IsNotFound(err) {
   729  			c.Status(http.StatusNotFound)
   730  			_ = c.Error(utils.ErrNotFound.NewWithNoMessage())
   731  		} else {
   732  			c.Status(http.StatusInternalServerError)
   733  			_ = c.Error(utils.ErrInternalServer.WrapWithNoMessage(err))
   734  		}
   735  		return
   736  	}
   737  
   738  	c.JSON(http.StatusOK, StatusResponse{Status: "success"})
   739  }
   740  
   741  // @Summary Update a schedule experiment.
   742  // @Description Update a schedule experiment.
   743  // @Tags schedules
   744  // @Produce json
   745  // @Param request body core.KubeObjectDesc true "Request body"
   746  // @Success 200 {object} core.KubeObjectDesc
   747  // @Failure 400 {object} utils.APIError
   748  // @Failure 500 {object} utils.APIError
   749  // @Router /schedules [put]
   750  func (s *Service) updateSchedule(c *gin.Context) {
   751  	kubeCli, err := clientpool.ExtractTokenAndGetClient(c.Request.Header)
   752  	if err != nil {
   753  		_ = c.Error(utils.ErrInvalidRequest.WrapWithNoMessage(err))
   754  		return
   755  	}
   756  
   757  	exp := &core.KubeObjectDesc{}
   758  	if err := c.ShouldBindJSON(exp); err != nil {
   759  		c.Status(http.StatusBadRequest)
   760  		_ = c.Error(utils.ErrInvalidRequest.WrapWithNoMessage(err))
   761  		return
   762  	}
   763  
   764  	err = retry.RetryOnConflict(retry.DefaultRetry, func() error {
   765  		return s.updateScheduleFun(exp, kubeCli)
   766  	})
   767  	if err != nil {
   768  		if apierrors.IsNotFound(err) {
   769  			c.Status(http.StatusNotFound)
   770  			_ = c.Error(utils.ErrNotFound.WrapWithNoMessage(err))
   771  		} else {
   772  			c.Status(http.StatusInternalServerError)
   773  			_ = c.Error(utils.ErrInternalServer.WrapWithNoMessage(err))
   774  		}
   775  		return
   776  	}
   777  	c.JSON(http.StatusOK, exp)
   778  }
   779  
   780  func (s *Service) updateScheduleFun(exp *core.KubeObjectDesc, kubeCli client.Client) error {
   781  	sch := &v1alpha1.Schedule{}
   782  	meta := &exp.Meta
   783  	key := types.NamespacedName{Namespace: meta.Namespace, Name: meta.Name}
   784  
   785  	if err := kubeCli.Get(context.Background(), key, sch); err != nil {
   786  		return err
   787  	}
   788  
   789  	sch.SetLabels(meta.Labels)
   790  	sch.SetAnnotations(meta.Annotations)
   791  
   792  	var spec v1alpha1.ScheduleSpec
   793  	bytes, err := json.Marshal(exp.Spec)
   794  	if err != nil {
   795  		return err
   796  	}
   797  	if err = json.Unmarshal(bytes, &spec); err != nil {
   798  		return err
   799  	}
   800  	sch.Spec = spec
   801  
   802  	return kubeCli.Update(context.Background(), sch)
   803  }
   804  
   805  // @Summary Delete the specified schedule experiment.
   806  // @Description Delete the specified schedule experiment.
   807  // @Tags schedules
   808  // @Produce json
   809  // @Param uids query string true "uids"
   810  // @Success 200 {object} StatusResponse
   811  // @Failure 400 {object} utils.APIError
   812  // @Failure 404 {object} utils.APIError
   813  // @Failure 500 {object} utils.APIError
   814  // @Router /schedules [delete]
   815  func (s *Service) batchDeleteSchedule(c *gin.Context) {
   816  	var (
   817  		exp      *core.Schedule
   818  		errFlag  bool
   819  		uidSlice []string
   820  	)
   821  
   822  	kubeCli, err := clientpool.ExtractTokenAndGetClient(c.Request.Header)
   823  	if err != nil {
   824  		_ = c.Error(utils.ErrInvalidRequest.WrapWithNoMessage(err))
   825  		return
   826  	}
   827  
   828  	uids := c.Query("uids")
   829  	if uids == "" {
   830  		c.Status(http.StatusBadRequest)
   831  		_ = c.Error(utils.ErrInternalServer.WrapWithNoMessage(fmt.Errorf("uids cannot be empty")))
   832  		return
   833  	}
   834  	uidSlice = strings.Split(uids, ",")
   835  	errFlag = false
   836  
   837  	for _, uid := range uidSlice {
   838  		if exp, err = s.schedule.FindByUID(context.Background(), uid); err != nil {
   839  			if gorm.IsRecordNotFoundError(err) {
   840  				_ = c.Error(utils.ErrInternalServer.WrapWithNoMessage(fmt.Errorf("delete experiment uid (%s) error, because the experiment is not found", uid)))
   841  			} else {
   842  				_ = c.Error(utils.ErrInternalServer.WrapWithNoMessage(fmt.Errorf("delete experiment uid (%s) error, because %s", uid, err.Error())))
   843  			}
   844  			errFlag = true
   845  			continue
   846  		}
   847  
   848  		ns := exp.Namespace
   849  		name := exp.Name
   850  
   851  		ctx := context.TODO()
   852  		scheduleKey := types.NamespacedName{Namespace: ns, Name: name}
   853  		schedule := &v1alpha1.Schedule{}
   854  
   855  		if err := kubeCli.Get(ctx, scheduleKey, schedule); err != nil {
   856  			if apierrors.IsNotFound(err) {
   857  				_ = c.Error(utils.ErrInternalServer.WrapWithNoMessage(fmt.Errorf("delete experiment uid (%s) error, because the chaos is not found", uid)))
   858  			} else {
   859  				_ = c.Error(utils.ErrInternalServer.WrapWithNoMessage(fmt.Errorf("delete experiment uid (%s) error, because %s", uid, err.Error())))
   860  			}
   861  			errFlag = true
   862  			continue
   863  		}
   864  
   865  		if err := kubeCli.Delete(ctx, schedule, &client.DeleteOptions{}); err != nil {
   866  			if apierrors.IsNotFound(err) {
   867  				_ = c.Error(utils.ErrInternalServer.WrapWithNoMessage(fmt.Errorf("delete experiment uid (%s) error, because the chaos is not found", uid)))
   868  			} else {
   869  				_ = c.Error(utils.ErrInternalServer.WrapWithNoMessage(fmt.Errorf("delete experiment uid (%s) error, because %s", uid, err.Error())))
   870  			}
   871  			errFlag = true
   872  			continue
   873  		}
   874  	}
   875  	if errFlag {
   876  		c.Status(http.StatusInternalServerError)
   877  	} else {
   878  		c.JSON(http.StatusOK, StatusResponse{Status: "success"})
   879  	}
   880  }
   881  
   882  // @Summary Pause a schedule object.
   883  // @Description Pause a schedule object.
   884  // @Tags schedules
   885  // @Produce json
   886  // @Param uid path string true "uid"
   887  // @Success 200 {object} StatusResponse
   888  // @Failure 400 {object} utils.APIError
   889  // @Failure 404 {object} utils.APIError
   890  // @Failure 500 {object} utils.APIError
   891  // @Router /schedules/pause/{uid} [put]
   892  func (s *Service) pauseSchedule(c *gin.Context) {
   893  	kubeCli, err := clientpool.ExtractTokenAndGetClient(c.Request.Header)
   894  	if err != nil {
   895  		_ = c.Error(utils.ErrInvalidRequest.WrapWithNoMessage(err))
   896  		return
   897  	}
   898  
   899  	uid := c.Param("uid")
   900  	err = s.pauseOrStartSchedule(uid, PauseSchedule, kubeCli)
   901  
   902  	if err != nil {
   903  		if gorm.IsRecordNotFoundError(err) || apierrors.IsNotFound(err) {
   904  			c.Status(http.StatusNotFound)
   905  			_ = c.Error(utils.ErrInvalidRequest.New("the schedule is not found"))
   906  		}
   907  		c.Status(http.StatusInternalServerError)
   908  		_ = c.Error(utils.ErrInternalServer.WrapWithNoMessage(err))
   909  		return
   910  	}
   911  
   912  	c.JSON(http.StatusOK, StatusResponse{Status: "success"})
   913  }
   914  
   915  // @Summary Start a schedule object.
   916  // @Description Start a schedule object.
   917  // @Tags schedules
   918  // @Produce json
   919  // @Param uid path string true "uid"
   920  // @Success 200 {object} StatusResponse
   921  // @Failure 400 {object} utils.APIError
   922  // @Failure 404 {object} utils.APIError
   923  // @Failure 500 {object} utils.APIError
   924  // @Router /schedules/start/{uid} [put]
   925  func (s *Service) startSchedule(c *gin.Context) {
   926  	kubeCli, err := clientpool.ExtractTokenAndGetClient(c.Request.Header)
   927  	if err != nil {
   928  		_ = c.Error(utils.ErrInvalidRequest.WrapWithNoMessage(err))
   929  		return
   930  	}
   931  
   932  	uid := c.Param("uid")
   933  	err = s.pauseOrStartSchedule(uid, StartSchedule, kubeCli)
   934  
   935  	if err != nil {
   936  		if gorm.IsRecordNotFoundError(err) || apierrors.IsNotFound(err) {
   937  			c.Status(http.StatusNotFound)
   938  			_ = c.Error(utils.ErrInvalidRequest.New("the schedule is not found"))
   939  		}
   940  		c.Status(http.StatusInternalServerError)
   941  		_ = c.Error(utils.ErrInternalServer.WrapWithNoMessage(err))
   942  		return
   943  	}
   944  
   945  	c.JSON(http.StatusOK, StatusResponse{Status: "success"})
   946  }
   947  
   948  func (s *Service) pauseOrStartSchedule(uid string, flag pauseFlag, kubeCli client.Client) error {
   949  	var (
   950  		err             error
   951  		schedule        *core.Schedule
   952  		pauseAnnotation string
   953  	)
   954  
   955  	if schedule, err = s.schedule.FindByUID(context.Background(), uid); err != nil {
   956  		return err
   957  	}
   958  
   959  	exp := &Base{
   960  		Name:      schedule.Name,
   961  		Namespace: schedule.Namespace,
   962  	}
   963  
   964  	if flag == PauseSchedule {
   965  		pauseAnnotation = "true"
   966  	} else {
   967  		pauseAnnotation = "false"
   968  	}
   969  
   970  	annotations := map[string]string{
   971  		v1alpha1.PauseAnnotationKey: pauseAnnotation,
   972  	}
   973  
   974  	return s.patchSchedule(exp, annotations, kubeCli)
   975  }
   976  
   977  func (s *Service) patchSchedule(exp *Base, annotations map[string]string, kubeCli client.Client) error {
   978  	sch := &v1alpha1.Schedule{}
   979  	key := types.NamespacedName{Namespace: exp.Namespace, Name: exp.Name}
   980  
   981  	if err := kubeCli.Get(context.Background(), key, sch); err != nil {
   982  		return err
   983  	}
   984  
   985  	var mergePatch []byte
   986  	mergePatch, _ = json.Marshal(map[string]interface{}{
   987  		"metadata": map[string]interface{}{
   988  			"annotations": annotations,
   989  		},
   990  	})
   991  
   992  	return kubeCli.Patch(context.Background(),
   993  		sch,
   994  		client.ConstantPatch(types.MergePatchType, mergePatch))
   995  }
   996