...

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

Documentation: github.com/chaos-mesh/chaos-mesh/pkg/dashboard/apiserver/common

     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  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  //
    15  
    16  package common
    17  
    18  import (
    19  	"context"
    20  	"crypto/rand"
    21  	"fmt"
    22  	"math/big"
    23  	"net/http"
    24  	"sort"
    25  	"strings"
    26  
    27  	"github.com/gin-gonic/gin"
    28  	"github.com/go-logr/logr"
    29  	"github.com/pkg/errors"
    30  	v1 "k8s.io/api/core/v1"
    31  	"sigs.k8s.io/controller-runtime/pkg/client"
    32  
    33  	"github.com/chaos-mesh/chaos-mesh/api/v1alpha1"
    34  	"github.com/chaos-mesh/chaos-mesh/pkg/clientpool"
    35  	"github.com/chaos-mesh/chaos-mesh/pkg/config"
    36  	apiservertypes "github.com/chaos-mesh/chaos-mesh/pkg/dashboard/apiserver/types"
    37  	u "github.com/chaos-mesh/chaos-mesh/pkg/dashboard/apiserver/utils"
    38  	"github.com/chaos-mesh/chaos-mesh/pkg/selector/generic/namespace"
    39  	"github.com/chaos-mesh/chaos-mesh/pkg/selector/physicalmachine"
    40  	"github.com/chaos-mesh/chaos-mesh/pkg/selector/pod"
    41  )
    42  
    43  const (
    44  	roleManager = "manager"
    45  	roleViewer  = "viewer"
    46  
    47  	serviceAccountTemplate = `kind: ServiceAccount
    48  apiVersion: v1
    49  metadata:
    50    namespace: %s
    51    name: %s
    52  `
    53  	roleTemplate = `kind: Role
    54  apiVersion: rbac.authorization.k8s.io/v1
    55  metadata:
    56    namespace: %s
    57    name: %s
    58  rules:
    59  - apiGroups: [""]
    60    resources: ["pods", "namespaces"]
    61    verbs: ["get", "watch", "list"]
    62  - apiGroups: ["chaos-mesh.org"]
    63    resources: [ "*" ]
    64    verbs: [%s]
    65  `
    66  	clusterRoleTemplate = `kind: ClusterRole
    67  apiVersion: rbac.authorization.k8s.io/v1
    68  metadata:
    69    name: %s
    70  rules:
    71  - apiGroups: [""]
    72    resources: ["pods", "namespaces"]
    73    verbs: ["get", "watch", "list"]
    74  - apiGroups: ["chaos-mesh.org"]
    75    resources: [ "*" ]
    76    verbs: [%s]
    77  `
    78  	roleBindingTemplate = `apiVersion: rbac.authorization.k8s.io/v1
    79  kind: RoleBinding
    80  metadata:
    81    name: %s
    82    namespace: %s
    83  subjects:
    84  - kind: ServiceAccount
    85    name: %s
    86    namespace: %s
    87  roleRef:
    88    kind: Role
    89    name: %s
    90    apiGroup: rbac.authorization.k8s.io
    91  `
    92  	clusterRoleBindingTemplate = `apiVersion: rbac.authorization.k8s.io/v1
    93  kind: ClusterRoleBinding
    94  metadata:
    95    name: %s
    96  subjects:
    97  - kind: ServiceAccount
    98    name: %s
    99    namespace: %s
   100  roleRef:
   101    kind: ClusterRole
   102    name: %s
   103    apiGroup: rbac.authorization.k8s.io
   104  `
   105  )
   106  
   107  // Service defines a handler service for cluster common objects.
   108  type Service struct {
   109  	// this kubeCli use the local token, used for list namespace of the K8s cluster
   110  	kubeCli client.Client
   111  	conf    *config.ChaosDashboardConfig
   112  	logger  logr.Logger
   113  }
   114  
   115  // NewService returns an experiment service instance.
   116  func NewService(
   117  	conf *config.ChaosDashboardConfig,
   118  	kubeCli client.Client,
   119  	logger logr.Logger,
   120  ) *Service {
   121  	return &Service{
   122  		conf:    conf,
   123  		kubeCli: kubeCli,
   124  		logger:  logger.WithName("common-api"),
   125  	}
   126  }
   127  
   128  // Register mounts our HTTP handler on the mux.
   129  func Register(r *gin.RouterGroup, s *Service) {
   130  	endpoint := r.Group("/common")
   131  
   132  	endpoint.POST("/pods", s.listPods)
   133  	endpoint.GET("/namespaces", s.listNamespaces)
   134  	endpoint.GET("/chaos-available-namespaces", s.getChaosAvailableNamespaces)
   135  	endpoint.GET("/kinds", s.getKinds)
   136  	endpoint.GET("/labels", s.getLabels)
   137  	endpoint.GET("/annotations", s.getAnnotations)
   138  	endpoint.GET("/config", s.getConfig)
   139  	endpoint.GET("/rbac-config", s.getRbacConfig)
   140  	endpoint.POST("/physicalmachines", s.listPhysicalMachines)
   141  	endpoint.GET("/physicalmachine-labels", s.getPhysicalMachineLabels)
   142  	endpoint.GET("/physicalmachine-annotations", s.getPhysicalMachineAnnotations)
   143  }
   144  
   145  // @Summary Get pods from Kubernetes cluster.
   146  // @Description Get pods from Kubernetes cluster.
   147  // @Tags common
   148  // @Produce json
   149  // @Param request body v1alpha1.PodSelectorSpec true "Request body"
   150  // @Success 200 {array} apiservertypes.Pod
   151  // @Failure 500 {object} u.APIError
   152  // @Router /common/pods [post]
   153  func (s *Service) listPods(c *gin.Context) {
   154  	kubeCli, err := clientpool.ExtractTokenAndGetClient(c.Request.Header)
   155  	if err != nil {
   156  		_ = c.Error(u.ErrBadRequest.WrapWithNoMessage(err))
   157  		return
   158  	}
   159  
   160  	selector := v1alpha1.PodSelectorSpec{}
   161  	if err := c.ShouldBindJSON(&selector); err != nil {
   162  		c.Status(http.StatusBadRequest)
   163  		_ = c.Error(u.ErrBadRequest.WrapWithNoMessage(err))
   164  		return
   165  	}
   166  	ctx := context.TODO()
   167  	filteredPods, err := pod.SelectPods(ctx, kubeCli, nil, selector, s.conf.ClusterScoped, s.conf.TargetNamespace, s.conf.EnableFilterNamespace)
   168  	if err != nil {
   169  		c.Status(http.StatusInternalServerError)
   170  		_ = c.Error(u.ErrInternalServer.WrapWithNoMessage(err))
   171  		return
   172  	}
   173  
   174  	pods := make([]apiservertypes.Pod, 0, len(filteredPods))
   175  	for _, pod := range filteredPods {
   176  		pods = append(pods, apiservertypes.Pod{
   177  			Name:      pod.Name,
   178  			IP:        pod.Status.PodIP,
   179  			Namespace: pod.Namespace,
   180  			State:     string(pod.Status.Phase),
   181  		})
   182  	}
   183  
   184  	c.JSON(http.StatusOK, pods)
   185  }
   186  
   187  // @Summary Get all namespaces from Kubernetes cluster.
   188  // @Description Get all from Kubernetes cluster.
   189  // @Deprecated This API only works within cluster scoped mode. Please use /common/chaos-available-namespaces instead.
   190  // @Tags common
   191  // @Produce json
   192  // @Success 200 {array} string
   193  // @Router /common/namespaces [get]
   194  // @Failure 500 {object} u.APIError
   195  func (s *Service) listNamespaces(c *gin.Context) {
   196  	var namespaces sort.StringSlice
   197  
   198  	var nsList v1.NamespaceList
   199  	if err := s.kubeCli.List(context.Background(), &nsList); err != nil {
   200  		c.Status(http.StatusInternalServerError)
   201  		_ = c.Error(u.ErrInternalServer.WrapWithNoMessage(err))
   202  		return
   203  	}
   204  	namespaces = make(sort.StringSlice, 0, len(nsList.Items))
   205  	for _, ns := range nsList.Items {
   206  		namespaces = append(namespaces, ns.Name)
   207  	}
   208  
   209  	sort.Sort(namespaces)
   210  	c.JSON(http.StatusOK, namespaces)
   211  }
   212  
   213  // @Summary Get all namespaces which could inject chaos(explosion scope) from Kubernetes cluster.
   214  // @Description Get all namespaces which could inject chaos(explosion scope) from Kubernetes cluster.
   215  // @Tags common
   216  // @Produce json
   217  // @Success 200 {array} string
   218  // @Router /common/chaos-available-namespaces [get]
   219  // @Failure 500 {object} u.APIError
   220  func (s *Service) getChaosAvailableNamespaces(c *gin.Context) {
   221  	var namespaces sort.StringSlice
   222  
   223  	if s.conf.ClusterScoped {
   224  		var nsList v1.NamespaceList
   225  		if err := s.kubeCli.List(context.Background(), &nsList); err != nil {
   226  			c.Status(http.StatusInternalServerError)
   227  			_ = c.Error(u.ErrInternalServer.WrapWithNoMessage(err))
   228  			return
   229  		}
   230  		namespaces = make(sort.StringSlice, 0, len(nsList.Items))
   231  		for _, ns := range nsList.Items {
   232  			if s.conf.EnableFilterNamespace && !namespace.CheckNamespace(context.TODO(), s.kubeCli, ns.Name, s.logger) {
   233  				continue
   234  			}
   235  			namespaces = append(namespaces, ns.Name)
   236  		}
   237  	} else {
   238  		namespaces = append(namespaces, s.conf.TargetNamespace)
   239  	}
   240  
   241  	sort.Sort(namespaces)
   242  	c.JSON(http.StatusOK, namespaces)
   243  }
   244  
   245  // @Summary Get all chaos kinds from Kubernetes cluster.
   246  // @Description Get all chaos kinds from Kubernetes cluster.
   247  // @Tags common
   248  // @Produce json
   249  // @Success 200 {array} string
   250  // @Router /common/kinds [get]
   251  // @Failure 500 {object} u.APIError
   252  func (s *Service) getKinds(c *gin.Context) {
   253  	var kinds []string
   254  
   255  	allKinds := v1alpha1.AllKinds()
   256  	for name := range allKinds {
   257  		kinds = append(kinds, name)
   258  	}
   259  
   260  	sort.Strings(kinds)
   261  	c.JSON(http.StatusOK, kinds)
   262  }
   263  
   264  // @Summary Get the labels of the pods in the specified namespace from Kubernetes cluster.
   265  // @Description Get the labels of the pods in the specified namespace from Kubernetes cluster.
   266  // @Tags common
   267  // @Produce json
   268  // @Param podNamespaceList query string true "The pod's namespace list, split by ,"
   269  // @Success 200 {object} u.MapStringSliceResponse
   270  // @Router /common/labels [get]
   271  // @Failure 500 {object} u.APIError
   272  func (s *Service) getLabels(c *gin.Context) {
   273  	kubeCli, err := clientpool.ExtractTokenAndGetClient(c.Request.Header)
   274  	if err != nil {
   275  		_ = c.Error(u.ErrBadRequest.WrapWithNoMessage(err))
   276  		return
   277  	}
   278  
   279  	podNamespaceList := c.Query("podNamespaceList")
   280  
   281  	if len(podNamespaceList) == 0 {
   282  		c.Status(http.StatusInternalServerError)
   283  		_ = c.Error(u.ErrInternalServer.WrapWithNoMessage(errors.New("podNamespaceList is required")))
   284  		return
   285  	}
   286  
   287  	selector := v1alpha1.PodSelectorSpec{}
   288  	nsList := strings.Split(podNamespaceList, ",")
   289  	selector.Namespaces = nsList
   290  
   291  	ctx := context.TODO()
   292  	filteredPods, err := pod.SelectPods(ctx, kubeCli, nil, selector, s.conf.ClusterScoped, s.conf.TargetNamespace, s.conf.EnableFilterNamespace)
   293  	if err != nil {
   294  		c.Status(http.StatusInternalServerError)
   295  		_ = c.Error(u.ErrInternalServer.WrapWithNoMessage(err))
   296  		return
   297  	}
   298  
   299  	labels := make(u.MapStringSliceResponse)
   300  	for _, pod := range filteredPods {
   301  		for k, v := range pod.Labels {
   302  			if _, ok := labels[k]; ok {
   303  				if !inSlice(v, labels[k]) {
   304  					labels[k] = append(labels[k], v)
   305  				}
   306  			} else {
   307  				labels[k] = []string{v}
   308  			}
   309  		}
   310  	}
   311  
   312  	c.JSON(http.StatusOK, labels)
   313  }
   314  
   315  // @Summary Get the annotations of the pods in the specified namespace from Kubernetes cluster.
   316  // @Description Get the annotations of the pods in the specified namespace from Kubernetes cluster.
   317  // @Tags common
   318  // @Produce json
   319  // @Param podNamespaceList query string true "The pod's namespace list, split by ,"
   320  // @Success 200 {object} u.MapStringSliceResponse
   321  // @Router /common/annotations [get]
   322  // @Failure 500 {object} u.APIError
   323  func (s *Service) getAnnotations(c *gin.Context) {
   324  	kubeCli, err := clientpool.ExtractTokenAndGetClient(c.Request.Header)
   325  	if err != nil {
   326  		_ = c.Error(u.ErrBadRequest.WrapWithNoMessage(err))
   327  		return
   328  	}
   329  
   330  	podNamespaceList := c.Query("podNamespaceList")
   331  
   332  	if len(podNamespaceList) == 0 {
   333  		c.Status(http.StatusInternalServerError)
   334  		_ = c.Error(u.ErrInternalServer.WrapWithNoMessage(errors.New("podNamespaceList is required")))
   335  		return
   336  	}
   337  
   338  	selector := v1alpha1.PodSelectorSpec{}
   339  	nsList := strings.Split(podNamespaceList, ",")
   340  	selector.Namespaces = nsList
   341  
   342  	ctx := context.TODO()
   343  	filteredPods, err := pod.SelectPods(ctx, kubeCli, nil, selector, s.conf.ClusterScoped, s.conf.TargetNamespace, s.conf.EnableFilterNamespace)
   344  	if err != nil {
   345  		c.Status(http.StatusInternalServerError)
   346  		_ = c.Error(u.ErrInternalServer.WrapWithNoMessage(err))
   347  		return
   348  	}
   349  
   350  	annotations := make(u.MapStringSliceResponse)
   351  	for _, pod := range filteredPods {
   352  		for k, v := range pod.Annotations {
   353  			if _, ok := annotations[k]; ok {
   354  				if !inSlice(v, annotations[k]) {
   355  					annotations[k] = append(annotations[k], v)
   356  				}
   357  			} else {
   358  				annotations[k] = []string{v}
   359  			}
   360  		}
   361  	}
   362  
   363  	c.JSON(http.StatusOK, annotations)
   364  }
   365  
   366  // @Summary Get the config of Dashboard.
   367  // @Description Get the config of Dashboard.
   368  // @Tags common
   369  // @Produce json
   370  // @Success 200 {object} config.ChaosDashboardConfig
   371  // @Router /common/config [get]
   372  // @Failure 500 {object} u.APIError
   373  func (s *Service) getConfig(c *gin.Context) {
   374  	c.JSON(http.StatusOK, s.conf)
   375  }
   376  
   377  // @Summary Get the rbac config according to the user's choice.
   378  // @Description Get the rbac config according to the user's choice.
   379  // @Tags common
   380  // @Produce json
   381  // @Param namespace query string false "The namespace of RBAC"
   382  // @Param role query string false "The role of RBAC"
   383  // @Success 200 {object} map[string]string
   384  // @Router /common/rbac-config [get]
   385  // @Failure 500 {object} u.APIError
   386  func (s *Service) getRbacConfig(c *gin.Context) {
   387  	namespace := c.Query("namespace")
   388  	roleType := c.Query("role")
   389  
   390  	var serviceAccount, role, roleBinding, verbs string
   391  	randomStr := randomStringWithCharset(5, charset)
   392  
   393  	scope := namespace
   394  	if len(namespace) == 0 {
   395  		namespace = "default"
   396  		scope = "cluster"
   397  	}
   398  	if roleType == roleManager {
   399  		verbs = `"get", "list", "watch", "create", "delete", "patch", "update"`
   400  	} else if roleType == roleViewer {
   401  		verbs = `"get", "list", "watch"`
   402  	} else {
   403  		c.Status(http.StatusBadRequest)
   404  		_ = c.Error(u.ErrBadRequest.WrapWithNoMessage(errors.New("roleType is neither manager nor viewer")))
   405  		return
   406  	}
   407  
   408  	serviceAccountName := fmt.Sprintf("account-%s-%s-%s", scope, roleType, randomStr)
   409  	roleName := fmt.Sprintf("role-%s-%s-%s", scope, roleType, randomStr)
   410  	roleBindingName := fmt.Sprintf("bind-%s-%s-%s", scope, roleType, randomStr)
   411  
   412  	serviceAccount = fmt.Sprintf(serviceAccountTemplate, namespace, serviceAccountName)
   413  	if scope == "cluster" {
   414  		role = fmt.Sprintf(clusterRoleTemplate, roleName, verbs)
   415  		roleBinding = fmt.Sprintf(clusterRoleBindingTemplate, roleBindingName, serviceAccountName, namespace, roleName)
   416  	} else {
   417  		role = fmt.Sprintf(roleTemplate, namespace, roleName, verbs)
   418  		roleBinding = fmt.Sprintf(roleBindingTemplate, roleBindingName, namespace, serviceAccountName, namespace, roleName)
   419  	}
   420  
   421  	rbacMap := make(map[string]string)
   422  	rbacMap[serviceAccountName] = serviceAccount + "\n---\n" + role + "\n---\n" + roleBinding
   423  
   424  	c.JSON(http.StatusOK, rbacMap)
   425  }
   426  
   427  // @Summary Get physicalMachines from Kubernetes cluster.
   428  // @Description Get physicalMachines from Kubernetes cluster.
   429  // @Tags common
   430  // @Produce json
   431  // @Param request body v1alpha1.PhysicalMachineSelectorSpec true "Request body"
   432  // @Success 200 {array} apiservertypes.PhysicalMachine
   433  // @Failure 500 {object} u.APIError
   434  // @Router /common/physicalmachines [post]
   435  func (s *Service) listPhysicalMachines(c *gin.Context) {
   436  	kubeCli, err := clientpool.ExtractTokenAndGetClient(c.Request.Header)
   437  	if err != nil {
   438  		u.SetAPIError(c, u.ErrBadRequest.WrapWithNoMessage(err))
   439  		return
   440  	}
   441  
   442  	selector := v1alpha1.PhysicalMachineSelectorSpec{}
   443  	if err := c.ShouldBindJSON(&selector); err != nil {
   444  		u.SetAPIError(c, u.ErrBadRequest.WrapWithNoMessage(err))
   445  		return
   446  	}
   447  	ctx := context.TODO()
   448  	filtered, err := physicalmachine.SelectPhysicalMachines(ctx, kubeCli, nil, selector, s.conf.ClusterScoped, s.conf.TargetNamespace, s.conf.EnableFilterNamespace, s.logger)
   449  	if err != nil {
   450  		u.SetAPIError(c, u.ErrInternalServer.WrapWithNoMessage(err))
   451  		return
   452  	}
   453  
   454  	physicalMachines := make([]apiservertypes.PhysicalMachine, 0, len(filtered))
   455  	for _, pm := range filtered {
   456  		physicalMachines = append(physicalMachines, apiservertypes.PhysicalMachine{
   457  			Name:      pm.Name,
   458  			Namespace: pm.Namespace,
   459  			Address:   pm.Spec.Address,
   460  		})
   461  	}
   462  
   463  	c.JSON(http.StatusOK, physicalMachines)
   464  }
   465  
   466  // @Summary Get the labels of the physicalMachines in the specified namespace from Kubernetes cluster.
   467  // @Description Get the labels of the physicalMachines in the specified namespace from Kubernetes cluster.
   468  // @Tags common
   469  // @Produce json
   470  // @Param physicalMachineNamespaceList query string true "The physicalMachine's namespace list, split by ,"
   471  // @Success 200 {object} u.MapStringSliceResponse
   472  // @Router /common/physicalmachine-labels [get]
   473  // @Failure 500 {object} u.APIError
   474  func (s *Service) getPhysicalMachineLabels(c *gin.Context) {
   475  
   476  	kubeCli, err := clientpool.ExtractTokenAndGetClient(c.Request.Header)
   477  	if err != nil {
   478  		u.SetAPIError(c, u.ErrBadRequest.WrapWithNoMessage(err))
   479  		return
   480  	}
   481  
   482  	physicalMachineNamespaceList := c.Query("physicalMachineNamespaceList")
   483  
   484  	if len(physicalMachineNamespaceList) == 0 {
   485  		u.SetAPIError(c, u.ErrInternalServer.WrapWithNoMessage(errors.New("physicalMachineNamespaceList is required")))
   486  		return
   487  	}
   488  
   489  	selector := v1alpha1.PhysicalMachineSelectorSpec{}
   490  	nsList := strings.Split(physicalMachineNamespaceList, ",")
   491  	selector.Namespaces = nsList
   492  
   493  	ctx := context.TODO()
   494  	filtered, err := physicalmachine.SelectPhysicalMachines(ctx, kubeCli, nil, selector, s.conf.ClusterScoped, s.conf.TargetNamespace, s.conf.EnableFilterNamespace, s.logger)
   495  	if err != nil {
   496  		u.SetAPIError(c, u.ErrInternalServer.WrapWithNoMessage(err))
   497  		return
   498  	}
   499  
   500  	labels := make(u.MapStringSliceResponse)
   501  	for _, obj := range filtered {
   502  		for k, v := range obj.Labels {
   503  			if _, ok := labels[k]; ok {
   504  				if !inSlice(v, labels[k]) {
   505  					labels[k] = append(labels[k], v)
   506  				}
   507  			} else {
   508  				labels[k] = []string{v}
   509  			}
   510  		}
   511  	}
   512  
   513  	c.JSON(http.StatusOK, labels)
   514  }
   515  
   516  // @Summary Get the annotations of the physicalMachines in the specified namespace from Kubernetes cluster.
   517  // @Description Get the annotations of the physicalMachines in the specified namespace from Kubernetes cluster.
   518  // @Tags common
   519  // @Produce json
   520  // @Param physicalMachineNamespaceList query string true "The physicalMachine's namespace list, split by ,"
   521  // @Success 200 {object} u.MapStringSliceResponse
   522  // @Router /common/physicalmachine-annotations [get]
   523  // @Failure 500 {object} u.APIError
   524  func (s *Service) getPhysicalMachineAnnotations(c *gin.Context) {
   525  
   526  	kubeCli, err := clientpool.ExtractTokenAndGetClient(c.Request.Header)
   527  	if err != nil {
   528  		u.SetAPIError(c, u.ErrBadRequest.WrapWithNoMessage(err))
   529  		return
   530  	}
   531  
   532  	physicalMachineNamespaceList := c.Query("physicalMachineNamespaceList")
   533  
   534  	if len(physicalMachineNamespaceList) == 0 {
   535  		u.SetAPIError(c, u.ErrInternalServer.WrapWithNoMessage(errors.New("physicalMachineNamespaceList is required")))
   536  		return
   537  	}
   538  	selector := v1alpha1.PhysicalMachineSelectorSpec{}
   539  	nsList := strings.Split(physicalMachineNamespaceList, ",")
   540  	selector.Namespaces = nsList
   541  
   542  	ctx := context.TODO()
   543  	filtered, err := physicalmachine.SelectPhysicalMachines(ctx, kubeCli, nil, selector, s.conf.ClusterScoped, s.conf.TargetNamespace, s.conf.EnableFilterNamespace, s.logger)
   544  	if err != nil {
   545  		u.SetAPIError(c, u.ErrInternalServer.WrapWithNoMessage(err))
   546  		return
   547  	}
   548  
   549  	annotations := make(u.MapStringSliceResponse)
   550  	for _, obj := range filtered {
   551  		for k, v := range obj.Annotations {
   552  			if _, ok := annotations[k]; ok {
   553  				if !inSlice(v, annotations[k]) {
   554  					annotations[k] = append(annotations[k], v)
   555  				}
   556  			} else {
   557  				annotations[k] = []string{v}
   558  			}
   559  		}
   560  	}
   561  
   562  	c.JSON(http.StatusOK, annotations)
   563  }
   564  
   565  // inSlice checks given string in string slice or not.
   566  func inSlice(v string, sl []string) bool {
   567  	for _, vv := range sl {
   568  		if vv == v {
   569  			return true
   570  		}
   571  	}
   572  	return false
   573  }
   574  
   575  const charset = "abcdefghijklmnopqrstuvwxyz"
   576  
   577  func randomStringWithCharset(length int, charset string) string {
   578  	b := make([]byte, length)
   579  	for i := range b {
   580  		num, _ := rand.Int(rand.Reader, big.NewInt(int64(len(charset))))
   581  		b[i] = charset[num.Int64()]
   582  	}
   583  	return string(b)
   584  }
   585