diff --git a/.gitignore b/.gitignore index 7615dda71..cb3b19b82 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ build coverage.txt +coverage.html +pkg/mocks +**/gomock* diff --git a/.travis.yml b/.travis.yml index 6fe26f3f0..6593126bb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,6 +16,9 @@ install: - rsync -az ${TRAVIS_BUILD_DIR}/ $GOPATH/src/github.com/autonomy/conform/ - export TRAVIS_BUILD_DIR=$GOPATH/src/github.com/autonomy/conform - cd $GOPATH/src/github.com/autonomy/conform + - cat /etc/apt/sources.list + - ls -l /etc/apt/sources.list.d + - sudo rm -rf /etc/apt/sources.list.d - sudo apt-get -y remove docker docker-engine - sudo apt-get -y update - sudo apt-get -y install apt-transport-https ca-certificates curl software-properties-common diff --git a/Dockerfile b/Dockerfile index 3f25e7da2..f401d70af 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,8 +7,10 @@ RUN GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o /argus -ldflags "-X \"gith FROM golang:1.14 as test WORKDIR $GOPATH/src/github.com/logicmonitor/k8s-argus RUN go get -u github.com/alecthomas/gometalinter +RUN go get github.com/golang/mock/mockgen RUN gometalinter --install COPY --from=build $GOPATH/src/github.com/logicmonitor/k8s-argus ./ +RUN go generate ./... RUN chmod +x ./scripts/test.sh; sync; ./scripts/test.sh RUN cp coverage.txt /coverage.txt diff --git a/Makefile b/Makefile index 38042fb33..2127a9dfc 100644 --- a/Makefile +++ b/Makefile @@ -1,14 +1,31 @@ NAMESPACE := logicmonitor REPOSITORY := argus -VERSION := 1.0.0 +VERSION ?= $(shell git describe --tags --always --dirty) -all: - docker build --build-arg VERSION=$(VERSION) -t $(NAMESPACE)/$(REPOSITORY):v2latest . - docker tag $(NAMESPACE)/$(REPOSITORY):v2latest $(NAMESPACE)/$(REPOSITORY):$(VERSION) +default: build + +lint: +ifeq ($(shell uname -s), Darwin) + find pkg/ -type f | grep go | egrep -v "mocks|gomock" | xargs gofmt -l -d -s -w + find pkg/ -type f | grep go | egrep -v "mocks|gomock" | xargs goimports -l -d -w +endif + +build: lint + + docker build --build-arg VERSION=$(VERSION) -t $(NAMESPACE)/$(REPOSITORY):$(VERSION) . + +dev: lint + docker build --build-arg VERSION=$(VERSION) -t $(NAMESPACE)/$(REPOSITORY):$(VERSION) -f Dockerfile.dev . + +mockgen: + go generate ./... +test: mockgen lint + go test ./... -v -coverprofile=coverage.txt -race + go tool cover -html=coverage.txt -o coverage.html + +devsetup: + go get github.com/golang/mock/mockgen -dev: - docker build --build-arg VERSION=$(VERSION) -t $(NAMESPACE)/$(REPOSITORY):v2latest -f Dockerfile.dev . - docker tag $(NAMESPACE)/$(REPOSITORY):v2latest $(NAMESPACE)/$(REPOSITORY):$(VERSION) .PHONY: docs docs: diff --git a/go.mod b/go.mod index 34c119c2a..506368234 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/coreos/etcd v3.3.13+incompatible github.com/go-openapi/runtime v0.19.11 github.com/go-openapi/strfmt v0.19.4 + github.com/golang/mock v1.4.4 github.com/golang/protobuf v1.4.2 // indirect github.com/google/uuid v1.1.1 github.com/googleapis/gnostic v0.4.0 // indirect diff --git a/go.sum b/go.sum index 018c5c38e..fd5720acc 100644 --- a/go.sum +++ b/go.sum @@ -140,7 +140,10 @@ github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4er github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef h1:veQD95Isof8w9/WXiA+pa3tz3fJXkt5B7QaRBrM62gk= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0 h1:28o5sBqPkBsMGnC6b4MvE2TzSr5/AT4c/1fLqVGIwlk= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -220,14 +223,8 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/logicmonitor/k8s-collectorset-controller v2.0.0+incompatible h1:0zX0S8yYyxImnLCb22PZ9p21FGBlyAjH59KawhzrNTU= github.com/logicmonitor/k8s-collectorset-controller v2.0.0+incompatible/go.mod h1:Lttrp2s4kBgrI+Bo3mzlURp/BGs7Op3ncCL1mIGegEU= -github.com/logicmonitor/lm-sdk-go v2.0.0-argus1+incompatible h1:/aHD5h0X5PxKVB7NBurv/UUyxR42fFIsJar/5t8ac0s= -github.com/logicmonitor/lm-sdk-go v2.0.0-argus1+incompatible/go.mod h1:gpLvWtDkb4lq+ezlvG79A953MZHoq/3XLTff4AIUF+M= -github.com/logicmonitor/lm-sdk-go v2.0.0-argus2+incompatible h1:BPoEW3DbfjtiBRgpygjOo1S0vj7+nWV7i+wprziuCxQ= -github.com/logicmonitor/lm-sdk-go v2.0.0-argus2+incompatible/go.mod h1:gpLvWtDkb4lq+ezlvG79A953MZHoq/3XLTff4AIUF+M= github.com/logicmonitor/lm-sdk-go v2.0.0-argus3+incompatible h1:Z5LJO+g30WyL4rG8bOsu6X3dwUpO0pHWtP6ZoCJFQh0= github.com/logicmonitor/lm-sdk-go v2.0.0-argus3+incompatible/go.mod h1:gpLvWtDkb4lq+ezlvG79A953MZHoq/3XLTff4AIUF+M= -github.com/logicmonitor/lm-sdk-go v2.0.0+incompatible h1:g/8iONuJKtUWGQ3MVdSRXhE5ovtxF2voi4OCZFwEDDM= -github.com/logicmonitor/lm-sdk-go v2.0.0+incompatible/go.mod h1:gpLvWtDkb4lq+ezlvG79A953MZHoq/3XLTff4AIUF+M= github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= @@ -319,8 +316,6 @@ github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhV github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5 h1:LnC5Kc/wtumK+WB441p7ynQJzVuNRJiqddSIE3IlSEQ= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= -github.com/vkumbhar94/lm-sdk-go v2.0.1+incompatible h1:t1UbZZ1ju6wv6NRVf6lx9wu4hHEe/mneZxqYGwIq/O8= -github.com/vkumbhar94/lm-sdk-go v2.0.1+incompatible/go.mod h1:svWOa+F9+4aChOCrhd5m2pZt7V1zx2VUkmCkkdjw7Lg= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= @@ -409,8 +404,10 @@ golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190617190820-da514acc4774 h1:CQVOmarCBFzTx0kbOU0ru54Cvot8SdSrNYjZPhQl+gk= golang.org/x/tools v0.0.0-20190617190820-da514acc4774/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/pkg/argus.go b/pkg/argus.go index 4413f8ccf..c475ffc53 100644 --- a/pkg/argus.go +++ b/pkg/argus.go @@ -3,7 +3,6 @@ package argus import ( "net/http" "net/url" - "time" httptransport "github.com/go-openapi/runtime/client" "github.com/go-openapi/strfmt" @@ -52,6 +51,10 @@ func newLMClient(argusConfig *config.Config) (*client.LMSdkGo, error) { config.SetAccountDomain(&domain) //config.UserAgent = constants.UserAgentBase + constants.Version if argusConfig.ProxyURL == "" { + //return client.New(config), nil + if argusConfig.IgnoreSSL { + return newLMClientWithoutSSL(config) + } return client.New(config), nil } return newLMClientWithProxy(config, argusConfig) @@ -83,6 +86,22 @@ func newLMClientWithProxy(config *client.Config, argusConfig *config.Config) (*c return client, nil } +func newLMClientWithoutSSL(config *client.Config) (*client.LMSdkGo, error) { + + var opts = httptransport.TLSClientOptions{InsecureSkipVerify: true} + var httpClient, err = httptransport.TLSClient(opts) + + if err != nil { + return nil, err + } + transport := httptransport.NewWithClient(config.TransportCfg.Host, config.TransportCfg.BasePath, config.TransportCfg.Schemes, httpClient) + authInfo := client.LMv1Auth(*config.AccessID, *config.AccessKey) + cli := new(client.LMSdkGo) + cli.Transport = transport + cli.LM = lm.New(transport, strfmt.Default, authInfo) + return cli, nil +} + func newK8sClient() (*kubernetes.Clientset, error) { config, err := rest.InClusterConfig() if err != nil { @@ -110,7 +129,7 @@ func NewArgus(base *types.Base) (*Argus, error) { Base: base, } - dcache := devicecache.NewDeviceCache(base, 5) + dcache := devicecache.NewDeviceCache(base, base.Config.GetCacheSyncInterval()) dcache.Run() deviceManager := &device.Manager{ @@ -237,7 +256,9 @@ func NewArgus(base *types.Base) (*Argus, error) { lctx := lmlog.NewLMContextWith(log.WithFields(log.Fields{"name": "init-sync"})) initSyncer.InitSync(lctx, true) - initSyncer.RunPeriodicSync(10) + + // periodically delete the non-exist resource devices through logicmonitor API based on specified time interval. + initSyncer.RunPeriodicSync(base.Config.GetPeriodicDeleteInterval()) if base.Config.EtcdDiscoveryToken != "" { etcdController := etcd.Controller{ @@ -280,6 +301,7 @@ func NewBase(config *config.Config) (*types.Base, error) { // Watch watches the API for events. func (a *Argus) Watch() { + syncInterval := a.Base.Config.GetPeriodicSyncInterval() log.Debugf("Starting watchers") for _, w := range a.Watchers { if !w.Enabled() { @@ -290,7 +312,7 @@ func (a *Argus) Watch() { _, controller := cache.NewInformer( watchlist, w.ObjType(), - time.Minute*10, + syncInterval, cache.ResourceEventHandlerFuncs{ AddFunc: w.AddFunc(), DeleteFunc: w.DeleteFunc(), @@ -300,18 +322,6 @@ func (a *Argus) Watch() { log.Debugf("Starting watcher of %v", w.Resource()) stop := make(chan struct{}) go controller.Run(stop) - // c := w.GetConfig() - // if c == nil { - // continue - // } - // wc := worker.NewWorker(c) - // b, err := a.Facade.RegisterWorker(w.Resource(), wc) - // if err != nil { - // log.Errorf("Failed to register worker for resource for: %s", w.Resource()) - // } - // if b { - // wc.StartWorker() - // } } } diff --git a/pkg/config/config.go b/pkg/config/config.go index ffe108f4a..bfa672e1f 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -2,25 +2,31 @@ package config import ( "io/ioutil" + "time" "github.com/kelseyhightower/envconfig" "github.com/logicmonitor/k8s-argus/pkg/constants" - "gopkg.in/yaml.v2" + log "github.com/sirupsen/logrus" + yaml "gopkg.in/yaml.v2" ) // Config represents the application's configuration file. +// nolint: maligned type Config struct { *Secrets - Address string `yaml:"address"` - ClusterCategory string `yaml:"cluster_category"` - ClusterName string `yaml:"cluster_name"` - Debug bool `yaml:"debug"` - DeleteDevices bool `yaml:"delete_devices"` - DisableAlerting bool `yaml:"disable_alerting"` - FullDisplayNameIncludeNamespace bool `yaml:"displayName_include_namespace"` - FullDisplayNameIncludeClusterName bool `yaml:"displayName_include_clustername"` - ClusterGroupID int32 `yaml:"cluster_group_id"` - ProxyURL string `yaml:"proxy_url"` + DeviceGroupProperties DeviceGroupProperties `yaml:"device_group_props"` + Intervals Intervals `yaml:"app_intervals"` + Address string `yaml:"address"` + ClusterCategory string `yaml:"cluster_category"` + ClusterName string `yaml:"cluster_name"` + Debug bool `yaml:"debug"` + DeleteDevices bool `yaml:"delete_devices"` + DisableAlerting bool `yaml:"disable_alerting"` + FullDisplayNameIncludeNamespace bool `yaml:"displayName_include_namespace"` + FullDisplayNameIncludeClusterName bool `yaml:"displayName_include_clustername"` + ClusterGroupID int32 `yaml:"cluster_group_id"` + ProxyURL string `yaml:"proxy_url"` + IgnoreSSL bool `yaml:"ignore_ssl"` } // Secrets represents the application's sensitive configuration file. @@ -33,6 +39,27 @@ type Secrets struct { ProxyPass string `envconfig:"PROXY_PASS"` } +// DeviceGroupProperties represents the properties applied on device groups +type DeviceGroupProperties struct { + Cluster []map[string]interface{} `yaml:"cluster"` + Pods []map[string]interface{} `yaml:"pods"` + Services []map[string]interface{} `yaml:"services"` + Deployments []map[string]interface{} `yaml:"deployments"` + Nodes []map[string]interface{} `yaml:"nodes"` + ETCD []map[string]interface{} `yaml:"etcd"` + HPA []map[string]interface{} `yaml:"hpas"` +} + +// Intervals represents default and min values for periodic sync, periodic delete and device cache sycn intervals +type Intervals struct { + PeriodicSyncInterval time.Duration `yaml:"periodic_sync_interval"` + PeriodicDeleteInterval time.Duration `yaml:"periodic_delete_interval"` + CacheSyncInterval time.Duration `yaml:"cache_sync_interval"` + PeriodicSyncMinInterval time.Duration `yaml:"periodic_sync_min_interval"` + PeriodicDeleteMinInterval time.Duration `yaml:"periodic_delete_min_interval"` + CacheSyncMinInterval time.Duration `yaml:"cache_sync_min_interval"` +} + // GetConfig returns the application configuration specified by the config file. func GetConfig() (*Config, error) { configBytes, err := ioutil.ReadFile(constants.ConfigPath) @@ -53,3 +80,39 @@ func GetConfig() (*Config, error) { return c, nil } + +// GetCacheSyncInterval gets cache resync interval +func (config Config) GetCacheSyncInterval() time.Duration { + cacheInterval := validateAndGetIntervalValue("cache_sync_interval", config.Intervals.CacheSyncInterval, config.Intervals.CacheSyncMinInterval, constants.DefaultCacheResyncInterval) + log.Debugf("cache_sync_interval - %v ", cacheInterval) + return cacheInterval +} + +// GetPeriodicSyncInterval gets periodic sync interval +func (config Config) GetPeriodicSyncInterval() time.Duration { + periodicSyncInterval := validateAndGetIntervalValue("periodic_sync_interval", config.Intervals.PeriodicSyncInterval, config.Intervals.PeriodicSyncMinInterval, constants.DefaultPeriodicSyncInterval) + log.Debugf("periodic_sync_interval - %v ", periodicSyncInterval) + return periodicSyncInterval +} + +// GetPeriodicDeleteInterval gets periodic delete interval +func (config Config) GetPeriodicDeleteInterval() time.Duration { + periodicDeleteInterval := validateAndGetIntervalValue("periodic_delete_interval", config.Intervals.PeriodicDeleteInterval, config.Intervals.PeriodicDeleteMinInterval, constants.DefaultPeriodicDeleteInterval) + log.Debugf("periodic_delete_interval - %v ", periodicDeleteInterval) + return periodicDeleteInterval +} + +// ValidateAndGetIntervalValue parses given interval into duration format. Returns default value if any errors. +func validateAndGetIntervalValue(intervalName string, syncInterval, minInterval, defaultValue time.Duration) time.Duration { + if syncInterval < minInterval { + log.Warnf("Please provide valid value for %s. Since invalid value is configured, forcefully setting it to default %v. ", intervalName, defaultValue) + syncInterval = defaultValue + } + + if syncInterval == 0 || minInterval == 0 { + log.Warnf("Looks like helm chart is of previous version than the current Argus expects. Please upgrade helm chart. Setting %s to its default : %v", intervalName, defaultValue) + syncInterval = defaultValue + } + + return syncInterval +} diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go index b92daa1bd..82d7ba6bf 100644 --- a/pkg/constants/constants.go +++ b/pkg/constants/constants.go @@ -1,5 +1,7 @@ package constants +import "time" + var ( // Version is the Argus version and is set at build time. Version string @@ -15,6 +17,12 @@ const ( RootDeviceGroupID = 1 // CustomPropertiesFieldName is the field name for a device's custom properties. CustomPropertiesFieldName = "customProperties" + // NameFieldName is the field name for a device's name. + NameFieldName = "name" + // DisplayNameFieldName is the field name for a device's display name. + DisplayNameFieldName = "displayName" + // MaxResourceLength is the max length of the resource name + MaxResourceLength = 244 ) const ( @@ -108,6 +116,12 @@ const ( K8sResourceNamePropertyKey = "auto.resourcename" // K8sResourceCreatedOnPropertyKey is the key of the custom property used to record resource create timestamp K8sResourceCreatedOnPropertyKey = "kubernetes.resourceCreatedOn" + // K8sResourceDeletedOnPropertyKey is the key of the custom property used to record resource deleted timestamp + K8sResourceDeletedOnPropertyKey = "kubernetes.resourceDeletedOn" + // K8sResourceDeleteAfterDurationPropertyKey is the key of the custom property used to delete resources from the portal after specified time + K8sResourceDeleteAfterDurationPropertyKey = "kubernetes.resourcedeleteafterduration" + // K8sResourceDeleteAfterDurationPropertyValue is the default value of the custom property used to delete resources from the portal after specified time + K8sResourceDeleteAfterDurationPropertyValue = "P1DT0H0M0S" // K8sDeviceType is the type value of the k8s device K8sDeviceType = 8 // K8sSystemCategoriesPropertyKey is the key of the unique custom property kubernetes system categories @@ -148,6 +162,8 @@ const ( ) const ( + // ArgusAppVersion is the key for Argus app version + ArgusAppVersion = "argus.app-version" // HelmChart is the key for Argus & Collectoeset-controller label HelmChart = "helm-chart" // HelmRevision is the key for Argus & Collectoeset-controller label @@ -172,3 +188,14 @@ const ( // IsPingDevice is the key used in watcher context to pass metadata IsPingDevice = "ispingdevice" ) + +const ( + // DefaultPeriodicSyncInterval Default interval for Periodic Discovery. + DefaultPeriodicSyncInterval = time.Minute * 30 + + // DefaultPeriodicDeleteInterval Default interval for Periodic delete. + DefaultPeriodicDeleteInterval = time.Minute * 30 + + // DefaultCacheResyncInterval Default interval for cache resync. + DefaultCacheResyncInterval = time.Minute * 5 +) diff --git a/pkg/cronjob/cronjob.go b/pkg/cronjob/cronjob.go index ea2448380..ea266753c 100644 --- a/pkg/cronjob/cronjob.go +++ b/pkg/cronjob/cronjob.go @@ -3,7 +3,7 @@ package cronjob import ( "github.com/logicmonitor/k8s-argus/pkg/lmctx" lmlog "github.com/logicmonitor/k8s-argus/pkg/log" - "github.com/robfig/cron/v3" + cron "github.com/robfig/cron/v3" ) var ( diff --git a/pkg/cronjob/devicegroupcron.go b/pkg/cronjob/devicegroupcron.go index fad3e3626..b75f3bbb3 100644 --- a/pkg/cronjob/devicegroupcron.go +++ b/pkg/cronjob/devicegroupcron.go @@ -61,6 +61,8 @@ func getExistingDeviceGroupPropertiesMap(lctx *lmctx.LMContext, groupID int32, c func getK8sAndHelmProperties(lctx *lmctx.LMContext, kubeClient kubernetes.Interface) map[string]string { customProperties := make(map[string]string) + // add Argus app version + customProperties[constants.ArgusAppVersion] = constants.Version customProperties = getKubernetesVersion(lctx, customProperties, kubeClient) customProperties = getHelmChartDetailsFromConfigMap(lctx, customProperties, kubeClient) return customProperties diff --git a/pkg/device/builder/builder.go b/pkg/device/builder/builder.go index 57fdc20b9..abba4cf7a 100644 --- a/pkg/device/builder/builder.go +++ b/pkg/device/builder/builder.go @@ -88,6 +88,27 @@ func (b *Builder) Custom(name, value string) types.DeviceOption { return setProperty(name, value) } +// DeletedOn impletements types.DeviceBuilder +func (b *Builder) DeletedOn(name string, value string) types.DeviceOption { + return func(device *models.Device) { + if device == nil { + return + } + if device.CustomProperties == nil { + device.CustomProperties = []*models.NameAndValue{} + } + for _, prop := range device.CustomProperties { + if *prop.Name == name { + return + } + } + device.CustomProperties = append(device.CustomProperties, &models.NameAndValue{ + Name: &name, + Value: &value, + }) + } +} + func setProperty(name, value string) types.DeviceOption { return func(device *models.Device) { if device == nil { diff --git a/pkg/device/device.go b/pkg/device/device.go index 871d1479f..4b162c275 100644 --- a/pkg/device/device.go +++ b/pkg/device/device.go @@ -2,7 +2,9 @@ package device import ( "fmt" + "strconv" "strings" + "time" "github.com/logicmonitor/k8s-argus/pkg/config" "github.com/logicmonitor/k8s-argus/pkg/constants" @@ -12,6 +14,7 @@ import ( "github.com/logicmonitor/k8s-argus/pkg/lmctx" lmlog "github.com/logicmonitor/k8s-argus/pkg/log" util "github.com/logicmonitor/k8s-argus/pkg/utilities" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" //"github.com/logicmonitor/k8s-argus/pkg/lmexec" "github.com/logicmonitor/k8s-argus/pkg/types" @@ -98,7 +101,7 @@ func (m *Manager) checkAndUpdateExistingDevice(lctx *lmctx.LMContext, resource s return existingDevice, nil } } - return nil, nil + return device, nil } // renameAndAddDevice rename display name and then add the device @@ -127,6 +130,7 @@ func (m *Manager) RenameAndUpdateDevice(lctx *lmctx.LMContext, resource string, entityProperty := models.EntityProperty{Name: constants.K8sSystemCategoriesPropertyKey, Value: updatedCategories, Type: "system"} err1 := m.updateDevicePropertyByName(lctx, updatedDevice.ID, &entityProperty, resource) if err1 != nil { + log.Errorf("Failed to remove device %s from conflicts group. %v", *updatedDevice.DisplayName, err1) return err1 } } @@ -141,15 +145,16 @@ func (m *Manager) RenameAndUpdateDevice(lctx *lmctx.LMContext, resource string, } newDevice, err := m.UpdateAndReplace(lctx, resource, device, options...) if err != nil { + log.Errorf("Failed to update the device %s : %v", *device.DisplayName, err) return err } err2 := m.moveDeviceToConflictGroup(lctx, newDevice, resource) if err2 != nil { - log.Errorf("%v", err2) + log.Errorf("Failed to move device %s to conflicts group: %v", *newDevice.DisplayName, err2) return err2 } - m.DC.Set(*device.DisplayName) + m.DC.Set(util.GetFullDisplayName(newDevice, resource, m.Config().ClusterName)) return nil } log.Errorf("%v", err) @@ -405,7 +410,7 @@ func (m *Manager) UpdateAndReplace(lctx *lmctx.LMContext, resource string, d *mo func (m *Manager) UpdateAndReplaceByDisplayName(lctx *lmctx.LMContext, resource, name, fullName string, filter types.UpdateFilter, labels map[string]string, options ...types.DeviceOption) (*models.Device, error) { log := lmlog.Logger(lctx) if !m.DC.Exists(fullName) { - log.Infof("Missing device %v; adding it now", name) + log.Infof("Missing device %v; (full name = %v) adding it now", name, fullName) return m.Add(lctx, resource, labels, options...) } if filter != nil && !filter() { @@ -436,19 +441,15 @@ func (m *Manager) UpdateAndReplaceByDisplayName(lctx *lmctx.LMContext, resource, return device, nil } -// TODO: this method needs to be removed in DEV-50496 - // UpdateAndReplaceField implements types.DeviceManager. -func (m *Manager) UpdateAndReplaceField(lctx *lmctx.LMContext, resource string, d *models.Device, field string, options ...types.DeviceOption) (*models.Device, error) { - log := lmlog.Logger(lctx) - device := buildDevice(lctx, m.Config(), d, options...) - log.Debugf("%#v", device) - +func (m *Manager) UpdateAndReplaceField(lctx *lmctx.LMContext, resource string, device *models.Device, fields string) (*models.Device, error) { params := lm.NewPatchDeviceParams() - params.SetID(d.ID) + params.SetID(device.ID) + params.SetPatchFields(&fields) params.SetBody(device) opType := "replace" params.SetOpType(&opType) + cmd := &types.HTTPCommand{ Command: &types.Command{ ExecFun: m.PatchDevice(params), @@ -460,14 +461,12 @@ func (m *Manager) UpdateAndReplaceField(lctx *lmctx.LMContext, resource string, ParseErrResp: m.PatchDeviceErrResp, }, } + restResponse, err := m.LMFacade.SendReceive(lctx, resource, cmd) - //restResponse, err := m.LMClient.LM.PatchDevice(params) if err != nil { return nil, err } resp := restResponse.(*lm.PatchDeviceOK) - log.Debugf("%#v", resp) - return resp.Payload, nil } @@ -501,31 +500,58 @@ func (m *Manager) updateDevicePropertyByName(lctx *lmctx.LMContext, deviceID int return nil } -// TODO: this method needs to be removed in DEV-50496 - -// UpdateAndReplaceFieldByDisplayName implements types.DeviceManager. -func (m *Manager) UpdateAndReplaceFieldByDisplayName(lctx *lmctx.LMContext, resource, name, fullName, field string, options ...types.DeviceOption) (*models.Device, error) { +// MoveToDeletedGroup implements types.DeviceManager. +func (m *Manager) MoveToDeletedGroup(lctx *lmctx.LMContext, resource, name, fullName string, deletionTimestamp *v1.Time, options ...types.DeviceOption) (*models.Device, error) { log := lmlog.Logger(lctx) - existingDevice, err := m.getExisitingDeviceByGivenProperties(lctx, name, fullName, resource) - if err != nil { return nil, err } - if existingDevice == nil { log.Warnf("Could not find device %q", name) return nil, nil } - options = append(options, m.DisplayName(*existingDevice.DisplayName)) - // Update the device. - device, err := m.UpdateAndReplaceField(lctx, resource, existingDevice, field, options...) + device := m.buildDeviceBeforeDeletion(deletionTimestamp, existingDevice, options...) + fields := constants.CustomPropertiesFieldName + "," + constants.NameFieldName + "," + constants.DisplayNameFieldName + + updatedDevice, err := m.UpdateAndReplaceField(lctx, resource, device, fields) if err != nil { return nil, err } + return updatedDevice, nil +} - return device, nil +func (m *Manager) buildDeviceBeforeDeletion(deletionTimestamp *v1.Time, existingDevice *models.Device, options ...types.DeviceOption) *models.Device { + // add resource deletion timestamp + timestamp := strconv.FormatInt(time.Now().Unix(), 10) + if deletionTimestamp != nil { + timestamp = strconv.FormatInt(deletionTimestamp.Unix(), 10) + } + options = append(options, m.DeletedOn(constants.K8sResourceDeletedOnPropertyKey, timestamp)) + + // rename device to resolve conflicts for new devices + shortUUID := strconv.FormatUint(uint64(util.GetShortUUID()), 10) + deviceName := util.TrimName(*existingDevice.Name) + options = append(options, m.Name(deviceName+"-"+shortUUID)) + deviceDisplayName := util.TrimName(util.GetPropertyValue(existingDevice, constants.K8sDeviceNamePropertyKey)) + options = append(options, m.DisplayName(deviceDisplayName+"-"+shortUUID)) + + // build device with specific fields that needs to be updated + // ID & PreferredCollectorID are required, if not passed then considered as 0 & API throws an error + device := &models.Device{ + ID: existingDevice.ID, + Name: existingDevice.Name, + DisplayName: existingDevice.DisplayName, + CustomProperties: existingDevice.CustomProperties, + PreferredCollectorID: existingDevice.PreferredCollectorID, + } + + for _, option := range options { + option(device) + } + + return device } // DeleteByID implements types.DeviceManager. diff --git a/pkg/devicecache/devicecache.go b/pkg/devicecache/devicecache.go index cd63c9692..d3bf0e5aa 100644 --- a/pkg/devicecache/devicecache.go +++ b/pkg/devicecache/devicecache.go @@ -17,12 +17,12 @@ import ( type DeviceCache struct { store map[string]interface{} rwm sync.RWMutex - resyncPeriod int + resyncPeriod time.Duration base *types.Base } // NewDeviceCache create new DeviceCache object -func NewDeviceCache(b *types.Base, rp int) *DeviceCache { +func NewDeviceCache(b *types.Base, rp time.Duration) *DeviceCache { dc := &DeviceCache{ store: make(map[string]interface{}), base: b, @@ -35,7 +35,7 @@ func NewDeviceCache(b *types.Base, rp int) *DeviceCache { func (dc *DeviceCache) Run() { go func(dcache *DeviceCache) { for { - time.Sleep(time.Duration(dcache.resyncPeriod) * time.Minute) + time.Sleep(dcache.resyncPeriod) log.Debugf("Device cache fetching devices") devices := dcache.getAllDevices(dcache.base) if devices == nil { diff --git a/pkg/devicegroup/devicegroup.go b/pkg/devicegroup/devicegroup.go index fdc9d36c3..495e5716e 100644 --- a/pkg/devicegroup/devicegroup.go +++ b/pkg/devicegroup/devicegroup.go @@ -29,6 +29,7 @@ type Options struct { DisableAlerting bool DeleteDevices bool FullDisplayNameIncludeClusterName bool + CustomProperties PropertyBuilder } // AppliesToBuilder is an interface for building an appliesTo string. @@ -90,8 +91,61 @@ func (a *appliesToBuilder) String() string { return a.value } +// PropertyBuilder is an interface for building properties +type PropertyBuilder interface { + Add(string, string, bool) PropertyBuilder + AddProperties([]map[string]interface{}) PropertyBuilder + Build([]*models.NameAndValue) []*models.NameAndValue +} + +type propertyBuilder struct { + properties []map[string]interface{} +} + +// NewPropertyBuilder is the builder for properties +func NewPropertyBuilder() PropertyBuilder { + return &propertyBuilder{} +} + +func (p *propertyBuilder) Add(key string, value string, override bool) PropertyBuilder { + m := make(map[string]interface{}) + m["name"] = key + m["value"] = value + m["override"] = override + p.properties = append(p.properties, m) + return p +} + +func (p *propertyBuilder) AddProperties(properties []map[string]interface{}) PropertyBuilder { + p.properties = append(p.properties, properties...) + return p +} + +func (p *propertyBuilder) Build(existingProps []*models.NameAndValue) []*models.NameAndValue { + props := []*models.NameAndValue{} + exProps := make(map[string]string) + for _, elm := range existingProps { + exProps[*elm.Name] = *elm.Value + } + for _, prop := range p.properties { + key := prop["name"].(string) + value := prop["value"].(string) + override := true + if v := prop["override"]; v != nil { + override = v.(bool) + } + if val, ok := exProps[key]; !override && ok { + value = val + } + props = append(props, &models.NameAndValue{Name: &key, Value: &value}) + } + return props +} + // Create creates a device group. func Create(opts *Options) (int32, error) { + lctx := lmlog.NewLMContextWith(log.WithFields(log.Fields{"res": "create-device-group", "device_group_name": opts.Name})) + log := lmlog.Logger(lctx) clusterDeviceGroup, err := Find(opts.ParentID, opts.Name, opts.Client) if err != nil { return 0, err @@ -99,12 +153,14 @@ func Create(opts *Options) (int32, error) { if clusterDeviceGroup == nil { log.Infof("Could not find device group %q", opts.Name) - cdg, err := create(opts.Name, opts.AppliesTo.String(), opts.DisableAlerting, opts.ParentID, opts.Client) + cdg, err := create(opts.Name, opts.AppliesTo.String(), opts.DisableAlerting, opts.ParentID, opts.CustomProperties.Build(nil), opts.Client) if err != nil { return 0, err } clusterDeviceGroup = cdg + } else { + replaceCustomProperty(lctx, clusterDeviceGroup, opts) } if !opts.DeleteDevices && opts.AppliesToDeletedGroup != nil { @@ -113,7 +169,7 @@ func Create(opts *Options) (int32, error) { return 0, err } if deletedDeviceGroup == nil { - _, err := create(constants.DeletedDeviceGroup, opts.AppliesToDeletedGroup.String(), true, clusterDeviceGroup.ID, opts.Client) + _, err := create(constants.DeletedDeviceGroup, opts.AppliesToDeletedGroup.String(), true, clusterDeviceGroup.ID, NewPropertyBuilder().Build(nil), opts.Client) if err != nil { return 0, err } @@ -136,7 +192,7 @@ func createConflictDynamicGroup(opts *Options, clusterGrpID int32) error { return err } if conflictingDeviceGroup == nil { - _, err := create(constants.ConflictDeviceGroup, opts.AppliesToConflict.String(), true, clusterGrpID, opts.Client) + _, err := create(constants.ConflictDeviceGroup, opts.AppliesToConflict.String(), true, clusterGrpID, nil, opts.Client) if err != nil { return err } @@ -148,7 +204,7 @@ func createConflictDynamicGroup(opts *Options, clusterGrpID int32) error { // Find searches for a device group identified by the parent ID and name. func Find(parentID int32, name string, client *client.LMSdkGo) (*models.DeviceGroup, error) { params := lm.NewGetDeviceGroupListParams() - fields := "name,id,parentId,subGroups" + fields := "name,id,parentId,subGroups,customProperties" params.SetFields(&fields) filter := fmt.Sprintf("name:\"%s\"", name) params.SetFilter(&filter) @@ -157,7 +213,7 @@ func Find(parentID int32, name string, client *client.LMSdkGo) (*models.DeviceGr return nil, fmt.Errorf("failed to get device group list when searching for %q: %v", name, err) } - log.Debugf("%#v", restResponse) + log.Debugf("Find devicegroup response: %#v", restResponse) var deviceGroup *models.DeviceGroup for _, d := range restResponse.Payload.Items { @@ -216,7 +272,7 @@ func ExistsByID(groupID int32, client *client.LMSdkGo) bool { return false } - log.Debugf("%#v", restResponse) + log.Debugf("Exists device group by id response:%#v", restResponse) if restResponse.Payload != nil && restResponse.Payload.ID == groupID { return true @@ -258,14 +314,15 @@ func DeleteGroup(deviceGroup *models.DeviceGroup, client *client.LMSdkGo) error return err } -func create(name, appliesTo string, disableAlerting bool, parentID int32, client *client.LMSdkGo) (*models.DeviceGroup, error) { +func create(name, appliesTo string, disableAlerting bool, parentID int32, customProperties []*models.NameAndValue, client *client.LMSdkGo) (*models.DeviceGroup, error) { params := lm.NewAddDeviceGroupParams() params.SetBody(&models.DeviceGroup{ - Name: &name, - Description: "A dynamic device group for Kubernetes.", - ParentID: parentID, - AppliesTo: appliesTo, - DisableAlerting: disableAlerting, + Name: &name, + Description: "A dynamic device group for Kubernetes.", + ParentID: parentID, + AppliesTo: appliesTo, + DisableAlerting: disableAlerting, + CustomProperties: customProperties, }) restResponse, err := client.LM.AddDeviceGroup(params) @@ -322,3 +379,23 @@ func AddDeviceGroupProperty(lctx *lmctx.LMContext, groupID int32, entityProperty log.Debugf("Successfully added device group property '%v'", entityProperty.Name) return true } + +// replaceCustomProperty adds/replaces the custom properties for device group +func replaceCustomProperty(lctx *lmctx.LMContext, clusterDeviceGroup *models.DeviceGroup, opts *Options) { + existingGroupProperties := make(map[string]string) + customProperties := opts.CustomProperties.Build(clusterDeviceGroup.CustomProperties) + for _, v := range clusterDeviceGroup.CustomProperties { + existingGroupProperties[*v.Name] = *v.Value + } + for _, v := range customProperties { + value, exists := existingGroupProperties[*v.Name] + entityProperty := models.EntityProperty{Name: *v.Name, Value: *v.Value, Type: constants.DeviceGroupCustomType} + if !exists { + // Add property if property does not exists + AddDeviceGroupProperty(lctx, clusterDeviceGroup.ID, &entityProperty, opts.Client) + } else if value == "" || value != *v.Value { + // Update value if it is empty or not equal to value in configmap + UpdateDeviceGroupPropertyByName(lctx, clusterDeviceGroup.ID, &entityProperty, opts.Client) + } + } +} diff --git a/pkg/filters/filters.go b/pkg/filters/filters.go index 5bc354896..ae5d34830 100644 --- a/pkg/filters/filters.go +++ b/pkg/filters/filters.go @@ -8,7 +8,7 @@ import ( "github.com/Knetic/govaluate" "github.com/logicmonitor/k8s-argus/pkg/constants" log "github.com/sirupsen/logrus" - "gopkg.in/yaml.v2" + yaml "gopkg.in/yaml.v2" ) var ( diff --git a/pkg/mocks/doc.go b/pkg/mocks/doc.go new file mode 100644 index 000000000..9380e3726 --- /dev/null +++ b/pkg/mocks/doc.go @@ -0,0 +1,5 @@ +package mocks + +// marker to download package in vendor otherwise go generate mocks for unit testing fails with no package in vendor +// as go modules copies only those packages those are used in code, else it cannot +import _ "github.com/golang/mock/mockgen/model" diff --git a/pkg/rl/ratelimitmgr.go b/pkg/rl/ratelimitmgr.go index 1f9ebf66a..b9a871f1b 100644 --- a/pkg/rl/ratelimitmgr.go +++ b/pkg/rl/ratelimitmgr.go @@ -10,7 +10,7 @@ import ( "github.com/logicmonitor/k8s-argus/pkg/constants" "github.com/logicmonitor/k8s-argus/pkg/types" log "github.com/sirupsen/logrus" - "gopkg.in/yaml.v2" + yaml "gopkg.in/yaml.v2" ) var ( diff --git a/pkg/sync/initsyncer.go b/pkg/sync/initsyncer.go index 7e5d0c914..50f6e6c68 100644 --- a/pkg/sync/initsyncer.go +++ b/pkg/sync/initsyncer.go @@ -200,6 +200,12 @@ func (i *InitSyncer) syncDevices(lctx *lmctx.LMContext, resourceType string, res continue } + // ignore the device if it is moved in _deleted group + if util.GetPropertyValue(device, constants.K8sResourceDeletedOnPropertyKey) != "" { + log.Debugf("Ignore the device (%v) for synchronization as it is moved in _deleted group", *device.DisplayName) + continue + } + // the displayName may be renamed, we should use the complete displayName for comparison. fullDisplayName := util.GetFullDisplayName(device, resourceType, autoClusterName) _, exist := resourcesMap[fullDisplayName] @@ -209,7 +215,7 @@ func (i *InitSyncer) syncDevices(lctx *lmctx.LMContext, resourceType string, res if err != nil { log.Warnf("Failed to delete the device: %v", *device.DisplayName) } - return + continue } // Rename devices as per config parameters only on Argus restart. if isRestart { @@ -236,11 +242,11 @@ func (i *InitSyncer) renameDeviceToDesiredName(lctx *lmctx.LMContext, device *mo } // RunPeriodicSync runs synchronization periodically. -func (i *InitSyncer) RunPeriodicSync(syncTime int) { +func (i *InitSyncer) RunPeriodicSync(syncTime time.Duration) { lctx := lmlog.NewLMContextWith(logrus.WithFields(logrus.Fields{"name": "periodic-sync"})) go func() { for { - time.Sleep(time.Duration(syncTime) * time.Minute) + time.Sleep(syncTime) i.InitSync(lctx, false) } }() diff --git a/pkg/tree/tree.go b/pkg/tree/tree.go index 28173633a..7b833521f 100644 --- a/pkg/tree/tree.go +++ b/pkg/tree/tree.go @@ -17,12 +17,13 @@ func (d *DeviceTree) buildOptsSlice() []*devicegroup.Options { // The device group at index 0 will be the root device group for all subsequent device groups. return []*devicegroup.Options{ { - Name: constants.ClusterDeviceGroupPrefix + d.Config.ClusterName, - ParentID: d.Config.ClusterGroupID, - DisableAlerting: d.Config.DisableAlerting, - AppliesTo: devicegroup.NewAppliesToBuilder().HasCategory(constants.ClusterCategory).And().Auto("clustername").Equals(d.Config.ClusterName), - Client: d.LMClient, - DeleteDevices: d.Config.DeleteDevices, + Name: constants.ClusterDeviceGroupPrefix + d.Config.ClusterName, + ParentID: d.Config.ClusterGroupID, + DisableAlerting: d.Config.DisableAlerting, + AppliesTo: devicegroup.NewAppliesToBuilder().HasCategory(constants.ClusterCategory).And().Auto("clustername").Equals(d.Config.ClusterName), + Client: d.LMClient, + DeleteDevices: d.Config.DeleteDevices, + CustomProperties: devicegroup.NewPropertyBuilder().AddProperties(d.Config.DeviceGroupProperties.Cluster), }, { Name: constants.EtcdDeviceGroupName, @@ -31,6 +32,7 @@ func (d *DeviceTree) buildOptsSlice() []*devicegroup.Options { Client: d.LMClient, DeleteDevices: d.Config.DeleteDevices, AppliesToDeletedGroup: devicegroup.NewAppliesToBuilder().HasCategory(constants.EtcdDeletedCategory).And().Auto("clustername").Equals(d.Config.ClusterName), + CustomProperties: devicegroup.NewPropertyBuilder().AddProperties(d.Config.DeviceGroupProperties.ETCD), }, { @@ -41,6 +43,7 @@ func (d *DeviceTree) buildOptsSlice() []*devicegroup.Options { DeleteDevices: d.Config.DeleteDevices, AppliesToConflict: devicegroup.NewAppliesToBuilder().HasCategory(constants.NodeConflictCategory).And().Auto("clustername").Equals(d.Config.ClusterName), FullDisplayNameIncludeClusterName: d.Config.FullDisplayNameIncludeClusterName, + CustomProperties: devicegroup.NewPropertyBuilder().AddProperties(d.Config.DeviceGroupProperties.Nodes), }, { Name: constants.AllNodeDeviceGroupName, @@ -50,6 +53,7 @@ func (d *DeviceTree) buildOptsSlice() []*devicegroup.Options { DeleteDevices: d.Config.DeleteDevices, AppliesToDeletedGroup: devicegroup.NewAppliesToBuilder().HasCategory(constants.NodeDeletedCategory).And().Auto("clustername").Equals(d.Config.ClusterName), FullDisplayNameIncludeClusterName: d.Config.FullDisplayNameIncludeClusterName, + CustomProperties: devicegroup.NewPropertyBuilder(), }, { @@ -61,6 +65,7 @@ func (d *DeviceTree) buildOptsSlice() []*devicegroup.Options { AppliesToDeletedGroup: devicegroup.NewAppliesToBuilder().HasCategory(constants.ServiceDeletedCategory).And().Auto("clustername").Equals(d.Config.ClusterName), AppliesToConflict: devicegroup.NewAppliesToBuilder().HasCategory(constants.ServiceConflictCategory).And().Auto("clustername").Equals(d.Config.ClusterName), FullDisplayNameIncludeClusterName: d.Config.FullDisplayNameIncludeClusterName, + CustomProperties: devicegroup.NewPropertyBuilder().AddProperties(d.Config.DeviceGroupProperties.Services), }, { Name: constants.PodDeviceGroupName, @@ -71,6 +76,7 @@ func (d *DeviceTree) buildOptsSlice() []*devicegroup.Options { AppliesToDeletedGroup: devicegroup.NewAppliesToBuilder().HasCategory(constants.PodDeletedCategory).And().Auto("clustername").Equals(d.Config.ClusterName), AppliesToConflict: devicegroup.NewAppliesToBuilder().HasCategory(constants.PodConflictCategory).And().Auto("clustername").Equals(d.Config.ClusterName), FullDisplayNameIncludeClusterName: d.Config.FullDisplayNameIncludeClusterName, + CustomProperties: devicegroup.NewPropertyBuilder().AddProperties(d.Config.DeviceGroupProperties.Pods), }, { Name: constants.DeploymentDeviceGroupName, @@ -81,6 +87,7 @@ func (d *DeviceTree) buildOptsSlice() []*devicegroup.Options { AppliesToDeletedGroup: devicegroup.NewAppliesToBuilder().HasCategory(constants.DeploymentDeletedCategory).And().Auto("clustername").Equals(d.Config.ClusterName), AppliesToConflict: devicegroup.NewAppliesToBuilder().HasCategory(constants.DeploymentConflictCategory).And().Auto("clustername").Equals(d.Config.ClusterName), FullDisplayNameIncludeClusterName: d.Config.FullDisplayNameIncludeClusterName, + CustomProperties: devicegroup.NewPropertyBuilder().AddProperties(d.Config.DeviceGroupProperties.Deployments), }, { Name: constants.HorizontalPodAutoscalerDeviceGroupName, @@ -91,6 +98,7 @@ func (d *DeviceTree) buildOptsSlice() []*devicegroup.Options { AppliesToDeletedGroup: devicegroup.NewAppliesToBuilder().HasCategory(constants.HorizontalPodAutoscalerDeletedCategory).And().Auto("clustername").Equals(d.Config.ClusterName), AppliesToConflict: devicegroup.NewAppliesToBuilder().HasCategory(constants.HorizontalPodAutoscalerConflictCategory).And().Auto("clustername").Equals(d.Config.ClusterName), FullDisplayNameIncludeClusterName: d.Config.FullDisplayNameIncludeClusterName, + CustomProperties: devicegroup.NewPropertyBuilder().AddProperties(d.Config.DeviceGroupProperties.HPA), }, } } diff --git a/pkg/types/types.go b/pkg/types/types.go index 39360532b..667b06cc4 100644 --- a/pkg/types/types.go +++ b/pkg/types/types.go @@ -1,11 +1,14 @@ package types +//go:generate mockgen -destination=../mocks/mock_types.go -package=mocks github.com/logicmonitor/k8s-argus/pkg/types LMFacade,Watcher,DeviceManager,DeviceMapper,DeviceBuilder + import ( "github.com/logicmonitor/k8s-argus/pkg/config" "github.com/logicmonitor/k8s-argus/pkg/lmctx" "github.com/logicmonitor/lm-sdk-go/client" "github.com/logicmonitor/lm-sdk-go/client/lm" "github.com/logicmonitor/lm-sdk-go/models" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/kubernetes" ) @@ -42,6 +45,7 @@ func (wc *WConfig) GetChannel(command ICommand) chan ICommand { // Watcher is the LogicMonitor Watcher interface. type Watcher interface { APIVersion() string + Namespaced() bool Enabled() bool Resource() string ObjType() runtime.Object @@ -76,10 +80,9 @@ type DeviceMapper interface { UpdateAndReplaceByDisplayName(*lmctx.LMContext, string, string, string, UpdateFilter, map[string]string, ...DeviceOption) (*models.Device, error) // UpdateAndReplaceField updates a device using the 'replace' OpType for a // specific field of a device. - UpdateAndReplaceField(*lmctx.LMContext, string, *models.Device, string, ...DeviceOption) (*models.Device, error) - // UpdateAndReplaceFieldByDisplayName updates a device using the 'replace' OpType for a - // specific field of a device. - UpdateAndReplaceFieldByDisplayName(*lmctx.LMContext, string, string, string, string, ...DeviceOption) (*models.Device, error) + UpdateAndReplaceField(*lmctx.LMContext, string, *models.Device, string) (*models.Device, error) + // MoveToDeletedGroup moves a device to _deleted group and replace fields + MoveToDeletedGroup(*lmctx.LMContext, string, string, string, *v1.Time, ...DeviceOption) (*models.Device, error) // DeleteByID deletes a device by device ID. DeleteByID(*lmctx.LMContext, string, int32) error // DeleteByDisplayName deletes a device by device display name. @@ -109,6 +112,8 @@ type DeviceBuilder interface { System(string, string) DeviceOption // System adds a custom property to the device. Custom(string, string) DeviceOption + // DeletedOn adds kubernetes.resourceDeletedOn property to the device. + DeletedOn(string, string) DeviceOption } // UpdateFilter is a boolean function to run predicate and return boolean value diff --git a/pkg/utilities/deviceprop.go b/pkg/utilities/deviceprop.go index 3eec5ea7b..ba8ec746a 100644 --- a/pkg/utilities/deviceprop.go +++ b/pkg/utilities/deviceprop.go @@ -101,3 +101,28 @@ func GetConflictCategoryByResourceType(resource string) string { } return "" } + +// TrimName it will trim the name to 244 char if greater than 244 +func TrimName(name string) string { + if len(name) > constants.MaxResourceLength { + name = name[:constants.MaxResourceLength] + } + return name +} + +// GetNameWithResourceTypeAndNamespace return name with resource_type and namespace +func GetNameWithResourceTypeAndNamespace(name, resource, namespace string) string { + switch strings.ToLower(resource) { + case constants.Pods: + return fmt.Sprintf("%s-%s-%s", name, "pod", namespace) + case constants.Deployments: + return fmt.Sprintf("%s-%s-%s", name, "deploy", namespace) + case constants.Services: + return fmt.Sprintf("%s-%s-%s", name, "svc", namespace) + case constants.Nodes: + return fmt.Sprintf("%s-%s-%s", name, "node", namespace) + case constants.HorizontalPodAutoScalers: + return fmt.Sprintf("%s-%s-%s", name, "hpa", namespace) + } + return name +} diff --git a/pkg/utilities/k8s_resource.go b/pkg/utilities/k8s_resource.go new file mode 100644 index 000000000..7ee6fe040 --- /dev/null +++ b/pkg/utilities/k8s_resource.go @@ -0,0 +1,31 @@ +package utilities + +import ( + "fmt" + + "github.com/logicmonitor/k8s-argus/pkg/constants" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// selfLink utility to create self links +// Haven't export as there is no usecase/caller with these params +func selfLink(namespaced bool, apiVersion string, kind string, namespace string, name string) string { + var selfLinkAPIPrefix string + if apiVersion == constants.K8sAPIVersionV1 { + selfLinkAPIPrefix = "/api" + } else { + selfLinkAPIPrefix = "/apis" + } + if namespaced { + if namespace == "" { + return "" + } + return fmt.Sprintf(selfLinkAPIPrefix+"/%s/namespaces/%s/%s/%s", apiVersion, namespace, kind, name) + } + return fmt.Sprintf(selfLinkAPIPrefix+"/%s/%s/%s", apiVersion, kind, name) +} + +// SelfLink utility to create self links +func SelfLink(namespaced bool, apiVersion string, kind string, objectMeta metav1.ObjectMeta) string { + return selfLink(namespaced, apiVersion, kind, objectMeta.Namespace, objectMeta.Name) +} diff --git a/pkg/utilities/k8s_resource_test.go b/pkg/utilities/k8s_resource_test.go new file mode 100644 index 000000000..998747138 --- /dev/null +++ b/pkg/utilities/k8s_resource_test.go @@ -0,0 +1,46 @@ +package utilities + +import ( + "testing" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// TestSelfLinkCoreGlobalResource tests core api resource which is not namespaced +func TestSelfLinkCoreGlobalResource(t *testing.T) { + expectedResult := "/api/v1/nodes/node1" + objectMeta := metav1.ObjectMeta{ + Name: "node1", + Namespace: "", + } + result := SelfLink(false, "v1", "nodes", objectMeta) + if result != expectedResult { + t.Errorf("Result selflink: %s does not match with expected %s", result, expectedResult) + } +} + +// TestSelfLinkCoreNamespacedResource tests core api resource which is namespaced +func TestSelfLinkCoreNamespacedResource(t *testing.T) { + expectedResult := "/api/v1/namespaces/ns1/pods/pod1" + objectMeta := metav1.ObjectMeta{ + Name: "pod1", + Namespace: "ns1", + } + result := SelfLink(true, "v1", "pods", objectMeta) + if result != expectedResult { + t.Errorf("Result selflink: %s does not match with expected %s", result, expectedResult) + } +} + +// TestSelfLinkCoreGlobalResource tests core api resource which is not namespaced +func TestSelfLinkAPIGroupNamespacedResource(t *testing.T) { + expectedResult := "/apis/apps/v1/namespaces/ns1/deployments/deploy1" + objectMeta := metav1.ObjectMeta{ + Name: "deploy1", + Namespace: "ns1", + } + result := SelfLink(true, "apps/v1", "deployments", objectMeta) + if result != expectedResult { + t.Errorf("Result selflink: %s does not match with expected %s", result, expectedResult) + } +} diff --git a/pkg/watch/deployment/deployment.go b/pkg/watch/deployment/deployment.go index 003209051..1e7c86032 100644 --- a/pkg/watch/deployment/deployment.go +++ b/pkg/watch/deployment/deployment.go @@ -35,6 +35,11 @@ func (w *Watcher) APIVersion() string { return constants.K8sAPIVersionAppsV1 } +// Namespaced returns true if resource is namespaced +func (w *Watcher) Namespaced() bool { + return true +} + // Enabled is a function that check the resource can watch. func (w *Watcher) Enabled() bool { return permission.HasDeploymentPermissions() @@ -130,9 +135,8 @@ func (w *Watcher) update(lctx *lmctx.LMContext, old, new *appsv1.Deployment) { // nolint: dupl func (w *Watcher) move(lctx *lmctx.LMContext, deployment *appsv1.Deployment) { log := lmlog.Logger(lctx) - if _, err := w.UpdateAndReplaceFieldByDisplayName(lctx, w.Resource(), w.getDesiredDisplayName(deployment), - fmtDeploymentDisplayName(deployment, w.Config().ClusterName), constants.CustomPropertiesFieldName, - w.args(deployment, constants.DeploymentDeletedCategory)...); err != nil { + if _, err := w.MoveToDeletedGroup(lctx, w.Resource(), w.getDesiredDisplayName(deployment), + fmtDeploymentDisplayName(deployment, w.Config().ClusterName), deployment.DeletionTimestamp, w.args(deployment, constants.DeploymentDeletedCategory)...); err != nil { log.Errorf("Failed to move deployment %q: %v", w.getDesiredDisplayName(deployment), err) return } @@ -147,7 +151,7 @@ func (w *Watcher) args(deployment *appsv1.Deployment, category string) []types.D w.SystemCategories(category), w.Auto("name", deployment.Name), w.Auto("namespace", deployment.Namespace), - w.Auto("selflink", deployment.SelfLink), + w.Auto("selflink", util.SelfLink(w.Namespaced(), w.APIVersion(), w.Resource(), deployment.ObjectMeta)), w.Auto("uid", string(deployment.UID)), w.Custom(constants.K8sResourceCreatedOnPropertyKey, strconv.FormatInt(deployment.CreationTimestamp.Unix(), 10)), w.Custom(constants.K8sResourceNamePropertyKey, w.getDesiredDisplayName(deployment)), diff --git a/pkg/watch/hpa/hpa.go b/pkg/watch/hpa/hpa.go index e3f12d445..89ca47943 100644 --- a/pkg/watch/hpa/hpa.go +++ b/pkg/watch/hpa/hpa.go @@ -40,6 +40,11 @@ func (w *Watcher) Enabled() bool { return permission.HasHorizontalPodAutoscalerPermissions() } +// Namespaced returns true if resource is namespaced +func (w *Watcher) Namespaced() bool { + return true +} + // Resource is a function that implements the Watcher interface. func (w *Watcher) Resource() string { return resource @@ -133,9 +138,8 @@ func (w *Watcher) update(lctx *lmctx.LMContext, old, new *autoscalingv1.Horizont func (w *Watcher) move(lctx *lmctx.LMContext, horizontalPodAutoscaler *autoscalingv1.HorizontalPodAutoscaler) { log := lmlog.Logger(lctx) - if _, err := w.UpdateAndReplaceFieldByDisplayName(lctx, w.Resource(), w.getDesiredDisplayName(horizontalPodAutoscaler), - fmtHorizontalPodAutoscalerDisplayName(horizontalPodAutoscaler, w.Config().ClusterName), constants.CustomPropertiesFieldName, - w.args(horizontalPodAutoscaler, constants.HorizontalPodAutoscalerDeletedCategory)...); err != nil { + if _, err := w.MoveToDeletedGroup(lctx, w.Resource(), w.getDesiredDisplayName(horizontalPodAutoscaler), + fmtHorizontalPodAutoscalerDisplayName(horizontalPodAutoscaler, w.Config().ClusterName), horizontalPodAutoscaler.DeletionTimestamp, w.args(horizontalPodAutoscaler, constants.HorizontalPodAutoscalerDeletedCategory)...); err != nil { log.Errorf("Failed to move horizontalPodAutoscaler %q: %v", w.getDesiredDisplayName(horizontalPodAutoscaler), err) return } @@ -144,13 +148,13 @@ func (w *Watcher) move(lctx *lmctx.LMContext, horizontalPodAutoscaler *autoscali func (w *Watcher) args(horizontalPodAutoscaler *autoscalingv1.HorizontalPodAutoscaler, category string) []types.DeviceOption { return []types.DeviceOption{ - w.Name(w.getDesiredDisplayName(horizontalPodAutoscaler)), + w.Name(util.GetNameWithResourceTypeAndNamespace(horizontalPodAutoscaler.Name, w.Resource(), horizontalPodAutoscaler.Namespace)), w.ResourceLabels(horizontalPodAutoscaler.Labels), w.DisplayName(w.getDesiredDisplayName(horizontalPodAutoscaler)), w.SystemCategories(category), w.Auto("name", horizontalPodAutoscaler.Name), w.Auto("namespace", horizontalPodAutoscaler.Namespace), - w.Auto("selflink", horizontalPodAutoscaler.SelfLink), + w.Auto("selflink", util.SelfLink(w.Namespaced(), w.APIVersion(), w.Resource(), horizontalPodAutoscaler.ObjectMeta)), w.Auto("uid", string(horizontalPodAutoscaler.UID)), w.Custom(constants.K8sResourceCreatedOnPropertyKey, strconv.FormatInt(horizontalPodAutoscaler.CreationTimestamp.Unix(), 10)), w.Custom(constants.K8sResourceNamePropertyKey, w.getDesiredDisplayName(horizontalPodAutoscaler)), diff --git a/pkg/watch/namespace/namespace.go b/pkg/watch/namespace/namespace.go index b58083472..6d6b612df 100644 --- a/pkg/watch/namespace/namespace.go +++ b/pkg/watch/namespace/namespace.go @@ -35,6 +35,11 @@ func (w *Watcher) Enabled() bool { return true } +// Namespaced returns true if resource is namespaced +func (w *Watcher) Namespaced() bool { + return true +} + // Resource is a function that implements the Watcher interface. func (w *Watcher) Resource() string { return resource @@ -67,11 +72,12 @@ func (w *Watcher) AddFunc() func(obj interface{}) { } opts := &devicegroup.Options{ - AppliesTo: appliesTo, - Client: w.LMClient, - DisableAlerting: w.Config.DisableAlerting, - Name: namespace.Name, - ParentID: parentID, + AppliesTo: appliesTo, + Client: w.LMClient, + DisableAlerting: w.Config.DisableAlerting, + Name: namespace.Name, + ParentID: parentID, + CustomProperties: devicegroup.NewPropertyBuilder(), } log.Debugf("%v", opts) diff --git a/pkg/watch/node/node.go b/pkg/watch/node/node.go index a9e0fd3b7..36a8249a7 100644 --- a/pkg/watch/node/node.go +++ b/pkg/watch/node/node.go @@ -49,6 +49,11 @@ func (w *Watcher) Resource() string { return resource } +// Namespaced returns true if resource is namespaced +func (w *Watcher) Namespaced() bool { + return false +} + // ObjType is a function that implements the Watcher interface. func (w *Watcher) ObjType() runtime.Object { return &v1.Node{} @@ -169,9 +174,8 @@ func (w *Watcher) update(lctx *lmctx.LMContext, old, new *v1.Node) { // nolint: dupl func (w *Watcher) move(lctx *lmctx.LMContext, node *v1.Node) { log := lmlog.Logger(lctx) - if _, err := w.UpdateAndReplaceFieldByDisplayName(lctx, w.Resource(), w.getDesiredDisplayName(node), - fmtNodeDisplayName(node, w.Config().ClusterName), constants.CustomPropertiesFieldName, - w.args(node, constants.NodeDeletedCategory)...); err != nil { + if _, err := w.MoveToDeletedGroup(lctx, w.Resource(), w.getDesiredDisplayName(node), + fmtNodeDisplayName(node, w.Config().ClusterName), node.DeletionTimestamp, w.args(node, constants.NodeDeletedCategory)...); err != nil { log.Errorf("Failed to move node %q: %v", w.getDesiredDisplayName(node), err) return } @@ -185,7 +189,7 @@ func (w *Watcher) args(node *v1.Node, category string) []types.DeviceOption { w.DisplayName(w.getDesiredDisplayName(node)), w.SystemCategories(category), w.Auto("name", node.Name), - w.Auto("selflink", node.SelfLink), + w.Auto("selflink", util.SelfLink(w.Namespaced(), w.APIVersion(), w.Resource(), node.ObjectMeta)), w.Auto("uid", string(node.UID)), w.Custom(constants.K8sResourceCreatedOnPropertyKey, strconv.FormatInt(node.CreationTimestamp.Unix(), 10)), w.Custom(constants.K8sResourceNamePropertyKey, node.Name), @@ -237,6 +241,7 @@ func (w *Watcher) createRoleDeviceGroup(lctx *lmctx.LMContext, labels map[string Client: w.LMClient, DeleteDevices: w.Config().DeleteDevices, AppliesToDeletedGroup: devicegroup.NewAppliesToBuilder().Exists(constants.LabelCustomPropertyPrefix + label).And().HasCategory(constants.NodeDeletedCategory).And().Auto("clustername").Equals(w.Config().ClusterName), + CustomProperties: devicegroup.NewPropertyBuilder(), } log.Debugf("%v", opts) diff --git a/pkg/watch/pod/pod.go b/pkg/watch/pod/pod.go index 9291f94ef..416afacdf 100644 --- a/pkg/watch/pod/pod.go +++ b/pkg/watch/pod/pod.go @@ -43,6 +43,11 @@ func (w *Watcher) Resource() string { return resource } +// Namespaced returns true if resource is namespaced +func (w *Watcher) Namespaced() bool { + return true +} + // ObjType is a function that implements the Watcher interface. func (w *Watcher) ObjType() runtime.Object { return &v1.Pod{} @@ -168,9 +173,8 @@ func (w *Watcher) update(lctx *lmctx.LMContext, old, new *v1.Pod) { // nolint: dupl func (w *Watcher) move(lctx *lmctx.LMContext, pod *v1.Pod) { log := lmlog.Logger(lctx) - if _, err := w.UpdateAndReplaceFieldByDisplayName(lctx, w.Resource(), w.getDesiredDisplayName(pod), - fmtPodDisplayName(pod, w.Config().ClusterName), constants.CustomPropertiesFieldName, - w.args(pod, constants.PodDeletedCategory)...); err != nil { + if _, err := w.MoveToDeletedGroup(lctx, w.Resource(), w.getDesiredDisplayName(pod), + fmtPodDisplayName(pod, w.Config().ClusterName), pod.DeletionTimestamp, w.args(pod, constants.PodDeletedCategory)...); err != nil { log.Errorf("Failed to move pod %q: %v", w.getDesiredDisplayName(pod), err) return } @@ -186,7 +190,7 @@ func (w *Watcher) args(pod *v1.Pod, category string) []types.DeviceOption { w.Auto("name", pod.Name), w.Auto("namespace", pod.Namespace), w.Auto("nodename", pod.Spec.NodeName), - w.Auto("selflink", pod.SelfLink), + w.Auto("selflink", util.SelfLink(w.Namespaced(), w.APIVersion(), w.Resource(), pod.ObjectMeta)), w.Auto("uid", string(pod.UID)), w.System("ips", pod.Status.PodIP), w.Custom(constants.K8sResourceCreatedOnPropertyKey, strconv.FormatInt(pod.CreationTimestamp.Unix(), 10)), diff --git a/pkg/watch/pod/pod_mock_test.go b/pkg/watch/pod/pod_mock_test.go new file mode 100644 index 000000000..a29f7afd7 --- /dev/null +++ b/pkg/watch/pod/pod_mock_test.go @@ -0,0 +1,96 @@ +package pod + +import ( + "testing" + + "github.com/golang/mock/gomock" + "github.com/logicmonitor/k8s-argus/pkg/constants" + "github.com/logicmonitor/k8s-argus/pkg/lmctx" + "github.com/logicmonitor/k8s-argus/pkg/mocks" + "github.com/logicmonitor/k8s-argus/pkg/types" + "github.com/logicmonitor/lm-sdk-go/models" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestAddFunc(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + podObject := &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "dummy-pod-name", + Namespace: "dummy-namespace", + Labels: map[string]string{ + "abc": "xyz", + }, + }, + Spec: v1.PodSpec{ + HostNetwork: true, + }, + Status: v1.PodStatus{ + Phase: v1.PodRunning, + PodIP: "127.0.0.1", + }, + } + + mDevMgr := mocks.NewMockDeviceManager(ctrl) + pWatcher := Watcher{ + mDevMgr, + nil, + } + dummyDeviceOption := func(d *models.Device) {} + mDevMgr.EXPECT().Name(gomock.Eq("dummy-pod-name")).Return(dummyDeviceOption) + mDevMgr.EXPECT().ResourceLabels(gomock.Eq(podObject.Labels)).Return(dummyDeviceOption) + mDevMgr.EXPECT().DisplayName(gomock.Eq("dummy-desired-disp-name")).Return(dummyDeviceOption) + mDevMgr.EXPECT().SystemCategories("KubernetesPod").Return(dummyDeviceOption) + mDevMgr.EXPECT().Auto(gomock.Any(), gomock.Any()).Return(dummyDeviceOption).Times(5) + mDevMgr.EXPECT().System(gomock.Any(), gomock.Any()).Return(dummyDeviceOption).Times(1) + mDevMgr.EXPECT().Custom(gomock.Any(), gomock.Any()).Return(dummyDeviceOption).Times(3) + mDevMgr.EXPECT().GetDesiredDisplayName(gomock.Eq("dummy-pod-name"), gomock.Eq("dummy-namespace"), gomock.Eq("pods")).Return("dummy-desired-disp-name").Times(4) + + mDevMgr.EXPECT().Add(gomock.Any(), gomock.Eq("pods"), gomock.Eq(podObject.Labels), gomock.Any()).DoAndReturn( + func(a *lmctx.LMContext, b string, c map[string]string, d ...types.DeviceOption) (interface{}, string, map[string]string, []types.DeviceOption) { + t.Logf("Add called with Arguments: %v %v %v %v", a, b, c, d) + if a.Extract(constants.IsPingDevice).(bool) == false { + ctrl.T.Errorf("Context param %v is false, Expected is true") + } + return a, b, c, d + }, + ) + + f := pWatcher.AddFunc() + f(podObject) + +} +func TestAddFuncPodWithoutIP(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + podObject := &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "dummy-pod-name", + Namespace: "dummy-namespace", + Labels: map[string]string{ + "abc": "xyz", + }, + }, + Spec: v1.PodSpec{ + HostNetwork: true, + }, + Status: v1.PodStatus{ + Phase: v1.PodPending, + }, + } + + mDevMgr := mocks.NewMockDeviceManager(ctrl) + pWatcher := Watcher{ + mDevMgr, + nil, + } + mDevMgr.EXPECT().GetDesiredDisplayName(gomock.Eq("dummy-pod-name"), gomock.Eq("dummy-namespace"), gomock.Eq("pods")).Return("dummy-desired-disp-name") + + f := pWatcher.AddFunc() + f(podObject) + +} diff --git a/pkg/watch/service/service.go b/pkg/watch/service/service.go index 869cef078..49ec8bd05 100644 --- a/pkg/watch/service/service.go +++ b/pkg/watch/service/service.go @@ -38,6 +38,11 @@ func (w *Watcher) Enabled() bool { return true } +// Namespaced returns true if resource is namespaced +func (w *Watcher) Namespaced() bool { + return true +} + // Resource is a function that implements the Watcher interface. func (w *Watcher) Resource() string { return resource @@ -97,6 +102,7 @@ func (w *Watcher) DeleteFunc() func(obj interface{}) { lctx := lmlog.NewLMContextWith(logrus.WithFields(logrus.Fields{"device_id": resource + "-" + service.Name})) log := lmlog.Logger(lctx) // Delete the service. + // nolint: dupl if w.Config().DeleteDevices { if err := w.DeleteByDisplayName(lctx, w.Resource(), w.getDesiredDisplayName(service), fmtServiceDisplayName(service, w.Config().ClusterName)); err != nil { @@ -152,9 +158,8 @@ func (w *Watcher) update(lctx *lmctx.LMContext, old, new *v1.Service) { // nolint: dupl func (w *Watcher) move(lctx *lmctx.LMContext, service *v1.Service) { log := lmlog.Logger(lctx) - if _, err := w.UpdateAndReplaceFieldByDisplayName(lctx, w.Resource(), w.getDesiredDisplayName(service), - fmtServiceDisplayName(service, w.Config().ClusterName), constants.CustomPropertiesFieldName, - w.args(service, constants.ServiceDeletedCategory)...); err != nil { + if _, err := w.MoveToDeletedGroup(lctx, w.Resource(), w.getDesiredDisplayName(service), + fmtServiceDisplayName(service, w.Config().ClusterName), service.DeletionTimestamp, w.args(service, constants.ServiceDeletedCategory)...); err != nil { log.Errorf("Failed to move service %q: %v", w.getDesiredDisplayName(service), err) return } @@ -162,14 +167,19 @@ func (w *Watcher) move(lctx *lmctx.LMContext, service *v1.Service) { } func (w *Watcher) args(service *v1.Service, category string) []types.DeviceOption { + clusterIP := service.Spec.ClusterIP + // headless services set clusterip to None: https://kubernetes.io/docs/concepts/services-networking/service/#headless-services + if service.Spec.ClusterIP == "None" { + clusterIP = fmt.Sprintf("%s-svc-%s", service.Name, service.Namespace) + } return []types.DeviceOption{ - w.Name(service.Spec.ClusterIP), + w.Name(clusterIP), w.ResourceLabels(service.Labels), w.DisplayName(w.getDesiredDisplayName(service)), w.SystemCategories(category), w.Auto("name", service.Name), w.Auto("namespace", service.Namespace), - w.Auto("selflink", service.SelfLink), + w.Auto("selflink", util.SelfLink(w.Namespaced(), w.APIVersion(), w.Resource(), service.ObjectMeta)), w.Auto("uid", string(service.UID)), w.Custom(constants.K8sResourceCreatedOnPropertyKey, strconv.FormatInt(service.CreationTimestamp.Unix(), 10)), w.Custom(constants.K8sResourceNamePropertyKey, w.getDesiredDisplayName(service)), diff --git a/scripts/test.sh b/scripts/test.sh index 99a683935..238db045f 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -24,7 +24,7 @@ test_packages() { lint_packages() { echo "Linting packages" - gometalinter --aggregate --vendor --exclude="zz_generated" --enable-all --disable=gas --disable=gotype --disable=lll --disable=safesql --disable=gochecknoinits --disable=gochecknoglobals --deadline=900s ./... + gometalinter --aggregate --vendor --exclude="zz_generated" --exclude="mocks" --enable-all --disable=gas --disable=gotype --disable=lll --disable=safesql --disable=gochecknoinits --disable=gochecknoglobals --deadline=900s ./... } format_files() { diff --git a/vendor/github.com/golang/mock/AUTHORS b/vendor/github.com/golang/mock/AUTHORS new file mode 100644 index 000000000..660b8ccc8 --- /dev/null +++ b/vendor/github.com/golang/mock/AUTHORS @@ -0,0 +1,12 @@ +# This is the official list of GoMock authors for copyright purposes. +# This file is distinct from the CONTRIBUTORS files. +# See the latter for an explanation. + +# Names should be added to this file as +# Name or Organization +# The email address is not required for organizations. + +# Please keep the list sorted. + +Alex Reece +Google Inc. diff --git a/vendor/github.com/golang/mock/CONTRIBUTORS b/vendor/github.com/golang/mock/CONTRIBUTORS new file mode 100644 index 000000000..def849cab --- /dev/null +++ b/vendor/github.com/golang/mock/CONTRIBUTORS @@ -0,0 +1,37 @@ +# This is the official list of people who can contribute (and typically +# have contributed) code to the gomock repository. +# The AUTHORS file lists the copyright holders; this file +# lists people. For example, Google employees are listed here +# but not in AUTHORS, because Google holds the copyright. +# +# The submission process automatically checks to make sure +# that people submitting code are listed in this file (by email address). +# +# Names should be added to this file only after verifying that +# the individual or the individual's organization has agreed to +# the appropriate Contributor License Agreement, found here: +# +# http://code.google.com/legal/individual-cla-v1.0.html +# http://code.google.com/legal/corporate-cla-v1.0.html +# +# The agreement for individuals can be filled out on the web. +# +# When adding J Random Contributor's name to this file, +# either J's name or J's organization's name should be +# added to the AUTHORS file, depending on whether the +# individual or corporate CLA was used. + +# Names should be added to this file like so: +# Name +# +# An entry with two email addresses specifies that the +# first address should be used in the submit logs and +# that the second address should be recognized as the +# same person when interacting with Rietveld. + +# Please keep the list sorted. + +Aaron Jacobs +Alex Reece +David Symonds +Ryan Barrett diff --git a/vendor/github.com/golang/mock/LICENSE b/vendor/github.com/golang/mock/LICENSE new file mode 100644 index 000000000..d64569567 --- /dev/null +++ b/vendor/github.com/golang/mock/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/github.com/golang/mock/gomock/call.go b/vendor/github.com/golang/mock/gomock/call.go new file mode 100644 index 000000000..7345f6540 --- /dev/null +++ b/vendor/github.com/golang/mock/gomock/call.go @@ -0,0 +1,427 @@ +// Copyright 2010 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package gomock + +import ( + "fmt" + "reflect" + "strconv" + "strings" +) + +// Call represents an expected call to a mock. +type Call struct { + t TestHelper // for triggering test failures on invalid call setup + + receiver interface{} // the receiver of the method call + method string // the name of the method + methodType reflect.Type // the type of the method + args []Matcher // the args + origin string // file and line number of call setup + + preReqs []*Call // prerequisite calls + + // Expectations + minCalls, maxCalls int + + numCalls int // actual number made + + // actions are called when this Call is called. Each action gets the args and + // can set the return values by returning a non-nil slice. Actions run in the + // order they are created. + actions []func([]interface{}) []interface{} +} + +// newCall creates a *Call. It requires the method type in order to support +// unexported methods. +func newCall(t TestHelper, receiver interface{}, method string, methodType reflect.Type, args ...interface{}) *Call { + t.Helper() + + // TODO: check arity, types. + margs := make([]Matcher, len(args)) + for i, arg := range args { + if m, ok := arg.(Matcher); ok { + margs[i] = m + } else if arg == nil { + // Handle nil specially so that passing a nil interface value + // will match the typed nils of concrete args. + margs[i] = Nil() + } else { + margs[i] = Eq(arg) + } + } + + origin := callerInfo(3) + actions := []func([]interface{}) []interface{}{func([]interface{}) []interface{} { + // Synthesize the zero value for each of the return args' types. + rets := make([]interface{}, methodType.NumOut()) + for i := 0; i < methodType.NumOut(); i++ { + rets[i] = reflect.Zero(methodType.Out(i)).Interface() + } + return rets + }} + return &Call{t: t, receiver: receiver, method: method, methodType: methodType, + args: margs, origin: origin, minCalls: 1, maxCalls: 1, actions: actions} +} + +// AnyTimes allows the expectation to be called 0 or more times +func (c *Call) AnyTimes() *Call { + c.minCalls, c.maxCalls = 0, 1e8 // close enough to infinity + return c +} + +// MinTimes requires the call to occur at least n times. If AnyTimes or MaxTimes have not been called or if MaxTimes +// was previously called with 1, MinTimes also sets the maximum number of calls to infinity. +func (c *Call) MinTimes(n int) *Call { + c.minCalls = n + if c.maxCalls == 1 { + c.maxCalls = 1e8 + } + return c +} + +// MaxTimes limits the number of calls to n times. If AnyTimes or MinTimes have not been called or if MinTimes was +// previously called with 1, MaxTimes also sets the minimum number of calls to 0. +func (c *Call) MaxTimes(n int) *Call { + c.maxCalls = n + if c.minCalls == 1 { + c.minCalls = 0 + } + return c +} + +// DoAndReturn declares the action to run when the call is matched. +// The return values from this function are returned by the mocked function. +// It takes an interface{} argument to support n-arity functions. +func (c *Call) DoAndReturn(f interface{}) *Call { + // TODO: Check arity and types here, rather than dying badly elsewhere. + v := reflect.ValueOf(f) + + c.addAction(func(args []interface{}) []interface{} { + vargs := make([]reflect.Value, len(args)) + ft := v.Type() + for i := 0; i < len(args); i++ { + if args[i] != nil { + vargs[i] = reflect.ValueOf(args[i]) + } else { + // Use the zero value for the arg. + vargs[i] = reflect.Zero(ft.In(i)) + } + } + vrets := v.Call(vargs) + rets := make([]interface{}, len(vrets)) + for i, ret := range vrets { + rets[i] = ret.Interface() + } + return rets + }) + return c +} + +// Do declares the action to run when the call is matched. The function's +// return values are ignored to retain backward compatibility. To use the +// return values call DoAndReturn. +// It takes an interface{} argument to support n-arity functions. +func (c *Call) Do(f interface{}) *Call { + // TODO: Check arity and types here, rather than dying badly elsewhere. + v := reflect.ValueOf(f) + + c.addAction(func(args []interface{}) []interface{} { + vargs := make([]reflect.Value, len(args)) + ft := v.Type() + for i := 0; i < len(args); i++ { + if args[i] != nil { + vargs[i] = reflect.ValueOf(args[i]) + } else { + // Use the zero value for the arg. + vargs[i] = reflect.Zero(ft.In(i)) + } + } + v.Call(vargs) + return nil + }) + return c +} + +// Return declares the values to be returned by the mocked function call. +func (c *Call) Return(rets ...interface{}) *Call { + c.t.Helper() + + mt := c.methodType + if len(rets) != mt.NumOut() { + c.t.Fatalf("wrong number of arguments to Return for %T.%v: got %d, want %d [%s]", + c.receiver, c.method, len(rets), mt.NumOut(), c.origin) + } + for i, ret := range rets { + if got, want := reflect.TypeOf(ret), mt.Out(i); got == want { + // Identical types; nothing to do. + } else if got == nil { + // Nil needs special handling. + switch want.Kind() { + case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice: + // ok + default: + c.t.Fatalf("argument %d to Return for %T.%v is nil, but %v is not nillable [%s]", + i, c.receiver, c.method, want, c.origin) + } + } else if got.AssignableTo(want) { + // Assignable type relation. Make the assignment now so that the generated code + // can return the values with a type assertion. + v := reflect.New(want).Elem() + v.Set(reflect.ValueOf(ret)) + rets[i] = v.Interface() + } else { + c.t.Fatalf("wrong type of argument %d to Return for %T.%v: %v is not assignable to %v [%s]", + i, c.receiver, c.method, got, want, c.origin) + } + } + + c.addAction(func([]interface{}) []interface{} { + return rets + }) + + return c +} + +// Times declares the exact number of times a function call is expected to be executed. +func (c *Call) Times(n int) *Call { + c.minCalls, c.maxCalls = n, n + return c +} + +// SetArg declares an action that will set the nth argument's value, +// indirected through a pointer. Or, in the case of a slice, SetArg +// will copy value's elements into the nth argument. +func (c *Call) SetArg(n int, value interface{}) *Call { + c.t.Helper() + + mt := c.methodType + // TODO: This will break on variadic methods. + // We will need to check those at invocation time. + if n < 0 || n >= mt.NumIn() { + c.t.Fatalf("SetArg(%d, ...) called for a method with %d args [%s]", + n, mt.NumIn(), c.origin) + } + // Permit setting argument through an interface. + // In the interface case, we don't (nay, can't) check the type here. + at := mt.In(n) + switch at.Kind() { + case reflect.Ptr: + dt := at.Elem() + if vt := reflect.TypeOf(value); !vt.AssignableTo(dt) { + c.t.Fatalf("SetArg(%d, ...) argument is a %v, not assignable to %v [%s]", + n, vt, dt, c.origin) + } + case reflect.Interface: + // nothing to do + case reflect.Slice: + // nothing to do + default: + c.t.Fatalf("SetArg(%d, ...) referring to argument of non-pointer non-interface non-slice type %v [%s]", + n, at, c.origin) + } + + c.addAction(func(args []interface{}) []interface{} { + v := reflect.ValueOf(value) + switch reflect.TypeOf(args[n]).Kind() { + case reflect.Slice: + setSlice(args[n], v) + default: + reflect.ValueOf(args[n]).Elem().Set(v) + } + return nil + }) + return c +} + +// isPreReq returns true if other is a direct or indirect prerequisite to c. +func (c *Call) isPreReq(other *Call) bool { + for _, preReq := range c.preReqs { + if other == preReq || preReq.isPreReq(other) { + return true + } + } + return false +} + +// After declares that the call may only match after preReq has been exhausted. +func (c *Call) After(preReq *Call) *Call { + c.t.Helper() + + if c == preReq { + c.t.Fatalf("A call isn't allowed to be its own prerequisite") + } + if preReq.isPreReq(c) { + c.t.Fatalf("Loop in call order: %v is a prerequisite to %v (possibly indirectly).", c, preReq) + } + + c.preReqs = append(c.preReqs, preReq) + return c +} + +// Returns true if the minimum number of calls have been made. +func (c *Call) satisfied() bool { + return c.numCalls >= c.minCalls +} + +// Returns true if the maximum number of calls have been made. +func (c *Call) exhausted() bool { + return c.numCalls >= c.maxCalls +} + +func (c *Call) String() string { + args := make([]string, len(c.args)) + for i, arg := range c.args { + args[i] = arg.String() + } + arguments := strings.Join(args, ", ") + return fmt.Sprintf("%T.%v(%s) %s", c.receiver, c.method, arguments, c.origin) +} + +// Tests if the given call matches the expected call. +// If yes, returns nil. If no, returns error with message explaining why it does not match. +func (c *Call) matches(args []interface{}) error { + if !c.methodType.IsVariadic() { + if len(args) != len(c.args) { + return fmt.Errorf("expected call at %s has the wrong number of arguments. Got: %d, want: %d", + c.origin, len(args), len(c.args)) + } + + for i, m := range c.args { + if !m.Matches(args[i]) { + got := fmt.Sprintf("%v", args[i]) + if gs, ok := m.(GotFormatter); ok { + got = gs.Got(args[i]) + } + + return fmt.Errorf( + "expected call at %s doesn't match the argument at index %d.\nGot: %v\nWant: %v", + c.origin, i, got, m, + ) + } + } + } else { + if len(c.args) < c.methodType.NumIn()-1 { + return fmt.Errorf("expected call at %s has the wrong number of matchers. Got: %d, want: %d", + c.origin, len(c.args), c.methodType.NumIn()-1) + } + if len(c.args) != c.methodType.NumIn() && len(args) != len(c.args) { + return fmt.Errorf("expected call at %s has the wrong number of arguments. Got: %d, want: %d", + c.origin, len(args), len(c.args)) + } + if len(args) < len(c.args)-1 { + return fmt.Errorf("expected call at %s has the wrong number of arguments. Got: %d, want: greater than or equal to %d", + c.origin, len(args), len(c.args)-1) + } + + for i, m := range c.args { + if i < c.methodType.NumIn()-1 { + // Non-variadic args + if !m.Matches(args[i]) { + return fmt.Errorf("expected call at %s doesn't match the argument at index %s.\nGot: %v\nWant: %v", + c.origin, strconv.Itoa(i), args[i], m) + } + continue + } + // The last arg has a possibility of a variadic argument, so let it branch + + // sample: Foo(a int, b int, c ...int) + if i < len(c.args) && i < len(args) { + if m.Matches(args[i]) { + // Got Foo(a, b, c) want Foo(matcherA, matcherB, gomock.Any()) + // Got Foo(a, b, c) want Foo(matcherA, matcherB, someSliceMatcher) + // Got Foo(a, b, c) want Foo(matcherA, matcherB, matcherC) + // Got Foo(a, b) want Foo(matcherA, matcherB) + // Got Foo(a, b, c, d) want Foo(matcherA, matcherB, matcherC, matcherD) + continue + } + } + + // The number of actual args don't match the number of matchers, + // or the last matcher is a slice and the last arg is not. + // If this function still matches it is because the last matcher + // matches all the remaining arguments or the lack of any. + // Convert the remaining arguments, if any, into a slice of the + // expected type. + vargsType := c.methodType.In(c.methodType.NumIn() - 1) + vargs := reflect.MakeSlice(vargsType, 0, len(args)-i) + for _, arg := range args[i:] { + vargs = reflect.Append(vargs, reflect.ValueOf(arg)) + } + if m.Matches(vargs.Interface()) { + // Got Foo(a, b, c, d, e) want Foo(matcherA, matcherB, gomock.Any()) + // Got Foo(a, b, c, d, e) want Foo(matcherA, matcherB, someSliceMatcher) + // Got Foo(a, b) want Foo(matcherA, matcherB, gomock.Any()) + // Got Foo(a, b) want Foo(matcherA, matcherB, someEmptySliceMatcher) + break + } + // Wrong number of matchers or not match. Fail. + // Got Foo(a, b) want Foo(matcherA, matcherB, matcherC, matcherD) + // Got Foo(a, b, c) want Foo(matcherA, matcherB, matcherC, matcherD) + // Got Foo(a, b, c, d) want Foo(matcherA, matcherB, matcherC, matcherD, matcherE) + // Got Foo(a, b, c, d, e) want Foo(matcherA, matcherB, matcherC, matcherD) + // Got Foo(a, b, c) want Foo(matcherA, matcherB) + return fmt.Errorf("Expected call at %s doesn't match the argument at index %s.\nGot: %v\nWant: %v", + c.origin, strconv.Itoa(i), args[i:], c.args[i]) + + } + } + + // Check that all prerequisite calls have been satisfied. + for _, preReqCall := range c.preReqs { + if !preReqCall.satisfied() { + return fmt.Errorf("Expected call at %s doesn't have a prerequisite call satisfied:\n%v\nshould be called before:\n%v", + c.origin, preReqCall, c) + } + } + + // Check that the call is not exhausted. + if c.exhausted() { + return fmt.Errorf("expected call at %s has already been called the max number of times", c.origin) + } + + return nil +} + +// dropPrereqs tells the expected Call to not re-check prerequisite calls any +// longer, and to return its current set. +func (c *Call) dropPrereqs() (preReqs []*Call) { + preReqs = c.preReqs + c.preReqs = nil + return +} + +func (c *Call) call() []func([]interface{}) []interface{} { + c.numCalls++ + return c.actions +} + +// InOrder declares that the given calls should occur in order. +func InOrder(calls ...*Call) { + for i := 1; i < len(calls); i++ { + calls[i].After(calls[i-1]) + } +} + +func setSlice(arg interface{}, v reflect.Value) { + va := reflect.ValueOf(arg) + for i := 0; i < v.Len(); i++ { + va.Index(i).Set(v.Index(i)) + } +} + +func (c *Call) addAction(action func([]interface{}) []interface{}) { + c.actions = append(c.actions, action) +} diff --git a/vendor/github.com/golang/mock/gomock/callset.go b/vendor/github.com/golang/mock/gomock/callset.go new file mode 100644 index 000000000..b046b525e --- /dev/null +++ b/vendor/github.com/golang/mock/gomock/callset.go @@ -0,0 +1,108 @@ +// Copyright 2011 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package gomock + +import ( + "bytes" + "fmt" +) + +// callSet represents a set of expected calls, indexed by receiver and method +// name. +type callSet struct { + // Calls that are still expected. + expected map[callSetKey][]*Call + // Calls that have been exhausted. + exhausted map[callSetKey][]*Call +} + +// callSetKey is the key in the maps in callSet +type callSetKey struct { + receiver interface{} + fname string +} + +func newCallSet() *callSet { + return &callSet{make(map[callSetKey][]*Call), make(map[callSetKey][]*Call)} +} + +// Add adds a new expected call. +func (cs callSet) Add(call *Call) { + key := callSetKey{call.receiver, call.method} + m := cs.expected + if call.exhausted() { + m = cs.exhausted + } + m[key] = append(m[key], call) +} + +// Remove removes an expected call. +func (cs callSet) Remove(call *Call) { + key := callSetKey{call.receiver, call.method} + calls := cs.expected[key] + for i, c := range calls { + if c == call { + // maintain order for remaining calls + cs.expected[key] = append(calls[:i], calls[i+1:]...) + cs.exhausted[key] = append(cs.exhausted[key], call) + break + } + } +} + +// FindMatch searches for a matching call. Returns error with explanation message if no call matched. +func (cs callSet) FindMatch(receiver interface{}, method string, args []interface{}) (*Call, error) { + key := callSetKey{receiver, method} + + // Search through the expected calls. + expected := cs.expected[key] + var callsErrors bytes.Buffer + for _, call := range expected { + err := call.matches(args) + if err != nil { + _, _ = fmt.Fprintf(&callsErrors, "\n%v", err) + } else { + return call, nil + } + } + + // If we haven't found a match then search through the exhausted calls so we + // get useful error messages. + exhausted := cs.exhausted[key] + for _, call := range exhausted { + if err := call.matches(args); err != nil { + _, _ = fmt.Fprintf(&callsErrors, "\n%v", err) + } + } + + if len(expected)+len(exhausted) == 0 { + _, _ = fmt.Fprintf(&callsErrors, "there are no expected calls of the method %q for that receiver", method) + } + + return nil, fmt.Errorf(callsErrors.String()) +} + +// Failures returns the calls that are not satisfied. +func (cs callSet) Failures() []*Call { + failures := make([]*Call, 0, len(cs.expected)) + for _, calls := range cs.expected { + for _, call := range calls { + if !call.satisfied() { + failures = append(failures, call) + } + } + } + return failures +} diff --git a/vendor/github.com/golang/mock/gomock/controller.go b/vendor/github.com/golang/mock/gomock/controller.go new file mode 100644 index 000000000..d7c3c656a --- /dev/null +++ b/vendor/github.com/golang/mock/gomock/controller.go @@ -0,0 +1,264 @@ +// Copyright 2010 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package gomock is a mock framework for Go. +// +// Standard usage: +// (1) Define an interface that you wish to mock. +// type MyInterface interface { +// SomeMethod(x int64, y string) +// } +// (2) Use mockgen to generate a mock from the interface. +// (3) Use the mock in a test: +// func TestMyThing(t *testing.T) { +// mockCtrl := gomock.NewController(t) +// defer mockCtrl.Finish() +// +// mockObj := something.NewMockMyInterface(mockCtrl) +// mockObj.EXPECT().SomeMethod(4, "blah") +// // pass mockObj to a real object and play with it. +// } +// +// By default, expected calls are not enforced to run in any particular order. +// Call order dependency can be enforced by use of InOrder and/or Call.After. +// Call.After can create more varied call order dependencies, but InOrder is +// often more convenient. +// +// The following examples create equivalent call order dependencies. +// +// Example of using Call.After to chain expected call order: +// +// firstCall := mockObj.EXPECT().SomeMethod(1, "first") +// secondCall := mockObj.EXPECT().SomeMethod(2, "second").After(firstCall) +// mockObj.EXPECT().SomeMethod(3, "third").After(secondCall) +// +// Example of using InOrder to declare expected call order: +// +// gomock.InOrder( +// mockObj.EXPECT().SomeMethod(1, "first"), +// mockObj.EXPECT().SomeMethod(2, "second"), +// mockObj.EXPECT().SomeMethod(3, "third"), +// ) +// +// TODO: +// - Handle different argument/return types (e.g. ..., chan, map, interface). +package gomock + +import ( + "context" + "fmt" + "reflect" + "runtime" + "sync" +) + +// A TestReporter is something that can be used to report test failures. It +// is satisfied by the standard library's *testing.T. +type TestReporter interface { + Errorf(format string, args ...interface{}) + Fatalf(format string, args ...interface{}) +} + +// TestHelper is a TestReporter that has the Helper method. It is satisfied +// by the standard library's *testing.T. +type TestHelper interface { + TestReporter + Helper() +} + +// A Controller represents the top-level control of a mock ecosystem. It +// defines the scope and lifetime of mock objects, as well as their +// expectations. It is safe to call Controller's methods from multiple +// goroutines. Each test should create a new Controller and invoke Finish via +// defer. +// +// func TestFoo(t *testing.T) { +// ctrl := gomock.NewController(t) +// defer ctrl.Finish() +// // .. +// } +// +// func TestBar(t *testing.T) { +// t.Run("Sub-Test-1", st) { +// ctrl := gomock.NewController(st) +// defer ctrl.Finish() +// // .. +// }) +// t.Run("Sub-Test-2", st) { +// ctrl := gomock.NewController(st) +// defer ctrl.Finish() +// // .. +// }) +// }) +type Controller struct { + // T should only be called within a generated mock. It is not intended to + // be used in user code and may be changed in future versions. T is the + // TestReporter passed in when creating the Controller via NewController. + // If the TestReporter does not implement a TestHelper it will be wrapped + // with a nopTestHelper. + T TestHelper + mu sync.Mutex + expectedCalls *callSet + finished bool +} + +// NewController returns a new Controller. It is the preferred way to create a +// Controller. +func NewController(t TestReporter) *Controller { + h, ok := t.(TestHelper) + if !ok { + h = nopTestHelper{t} + } + + return &Controller{ + T: h, + expectedCalls: newCallSet(), + } +} + +type cancelReporter struct { + TestHelper + cancel func() +} + +func (r *cancelReporter) Errorf(format string, args ...interface{}) { + r.TestHelper.Errorf(format, args...) +} +func (r *cancelReporter) Fatalf(format string, args ...interface{}) { + defer r.cancel() + r.TestHelper.Fatalf(format, args...) +} + +// WithContext returns a new Controller and a Context, which is cancelled on any +// fatal failure. +func WithContext(ctx context.Context, t TestReporter) (*Controller, context.Context) { + h, ok := t.(TestHelper) + if !ok { + h = nopTestHelper{t} + } + + ctx, cancel := context.WithCancel(ctx) + return NewController(&cancelReporter{h, cancel}), ctx +} + +type nopTestHelper struct { + TestReporter +} + +func (h nopTestHelper) Helper() {} + +// RecordCall is called by a mock. It should not be called by user code. +func (ctrl *Controller) RecordCall(receiver interface{}, method string, args ...interface{}) *Call { + ctrl.T.Helper() + + recv := reflect.ValueOf(receiver) + for i := 0; i < recv.Type().NumMethod(); i++ { + if recv.Type().Method(i).Name == method { + return ctrl.RecordCallWithMethodType(receiver, method, recv.Method(i).Type(), args...) + } + } + ctrl.T.Fatalf("gomock: failed finding method %s on %T", method, receiver) + panic("unreachable") +} + +// RecordCallWithMethodType is called by a mock. It should not be called by user code. +func (ctrl *Controller) RecordCallWithMethodType(receiver interface{}, method string, methodType reflect.Type, args ...interface{}) *Call { + ctrl.T.Helper() + + call := newCall(ctrl.T, receiver, method, methodType, args...) + + ctrl.mu.Lock() + defer ctrl.mu.Unlock() + ctrl.expectedCalls.Add(call) + + return call +} + +// Call is called by a mock. It should not be called by user code. +func (ctrl *Controller) Call(receiver interface{}, method string, args ...interface{}) []interface{} { + ctrl.T.Helper() + + // Nest this code so we can use defer to make sure the lock is released. + actions := func() []func([]interface{}) []interface{} { + ctrl.T.Helper() + ctrl.mu.Lock() + defer ctrl.mu.Unlock() + + expected, err := ctrl.expectedCalls.FindMatch(receiver, method, args) + if err != nil { + origin := callerInfo(2) + ctrl.T.Fatalf("Unexpected call to %T.%v(%v) at %s because: %s", receiver, method, args, origin, err) + } + + // Two things happen here: + // * the matching call no longer needs to check prerequite calls, + // * and the prerequite calls are no longer expected, so remove them. + preReqCalls := expected.dropPrereqs() + for _, preReqCall := range preReqCalls { + ctrl.expectedCalls.Remove(preReqCall) + } + + actions := expected.call() + if expected.exhausted() { + ctrl.expectedCalls.Remove(expected) + } + return actions + }() + + var rets []interface{} + for _, action := range actions { + if r := action(args); r != nil { + rets = r + } + } + + return rets +} + +// Finish checks to see if all the methods that were expected to be called +// were called. It should be invoked for each Controller. It is not idempotent +// and therefore can only be invoked once. +func (ctrl *Controller) Finish() { + ctrl.T.Helper() + + ctrl.mu.Lock() + defer ctrl.mu.Unlock() + + if ctrl.finished { + ctrl.T.Fatalf("Controller.Finish was called more than once. It has to be called exactly once.") + } + ctrl.finished = true + + // If we're currently panicking, probably because this is a deferred call, + // pass through the panic. + if err := recover(); err != nil { + panic(err) + } + + // Check that all remaining expected calls are satisfied. + failures := ctrl.expectedCalls.Failures() + for _, call := range failures { + ctrl.T.Errorf("missing call(s) to %v", call) + } + if len(failures) != 0 { + ctrl.T.Fatalf("aborting test due to missing call(s)") + } +} + +func callerInfo(skip int) string { + if _, file, line, ok := runtime.Caller(skip + 1); ok { + return fmt.Sprintf("%s:%d", file, line) + } + return "unknown file" +} diff --git a/vendor/github.com/golang/mock/gomock/matchers.go b/vendor/github.com/golang/mock/gomock/matchers.go new file mode 100644 index 000000000..7bfc07be4 --- /dev/null +++ b/vendor/github.com/golang/mock/gomock/matchers.go @@ -0,0 +1,255 @@ +// Copyright 2010 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package gomock + +import ( + "fmt" + "reflect" + "strings" +) + +// A Matcher is a representation of a class of values. +// It is used to represent the valid or expected arguments to a mocked method. +type Matcher interface { + // Matches returns whether x is a match. + Matches(x interface{}) bool + + // String describes what the matcher matches. + String() string +} + +// WantFormatter modifies the given Matcher's String() method to the given +// Stringer. This allows for control on how the "Want" is formatted when +// printing . +func WantFormatter(s fmt.Stringer, m Matcher) Matcher { + type matcher interface { + Matches(x interface{}) bool + } + + return struct { + matcher + fmt.Stringer + }{ + matcher: m, + Stringer: s, + } +} + +// StringerFunc type is an adapter to allow the use of ordinary functions as +// a Stringer. If f is a function with the appropriate signature, +// StringerFunc(f) is a Stringer that calls f. +type StringerFunc func() string + +// String implements fmt.Stringer. +func (f StringerFunc) String() string { + return f() +} + +// GotFormatter is used to better print failure messages. If a matcher +// implements GotFormatter, it will use the result from Got when printing +// the failure message. +type GotFormatter interface { + // Got is invoked with the received value. The result is used when + // printing the failure message. + Got(got interface{}) string +} + +// GotFormatterFunc type is an adapter to allow the use of ordinary +// functions as a GotFormatter. If f is a function with the appropriate +// signature, GotFormatterFunc(f) is a GotFormatter that calls f. +type GotFormatterFunc func(got interface{}) string + +// Got implements GotFormatter. +func (f GotFormatterFunc) Got(got interface{}) string { + return f(got) +} + +// GotFormatterAdapter attaches a GotFormatter to a Matcher. +func GotFormatterAdapter(s GotFormatter, m Matcher) Matcher { + return struct { + GotFormatter + Matcher + }{ + GotFormatter: s, + Matcher: m, + } +} + +type anyMatcher struct{} + +func (anyMatcher) Matches(interface{}) bool { + return true +} + +func (anyMatcher) String() string { + return "is anything" +} + +type eqMatcher struct { + x interface{} +} + +func (e eqMatcher) Matches(x interface{}) bool { + return reflect.DeepEqual(e.x, x) +} + +func (e eqMatcher) String() string { + return fmt.Sprintf("is equal to %v", e.x) +} + +type nilMatcher struct{} + +func (nilMatcher) Matches(x interface{}) bool { + if x == nil { + return true + } + + v := reflect.ValueOf(x) + switch v.Kind() { + case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, + reflect.Ptr, reflect.Slice: + return v.IsNil() + } + + return false +} + +func (nilMatcher) String() string { + return "is nil" +} + +type notMatcher struct { + m Matcher +} + +func (n notMatcher) Matches(x interface{}) bool { + return !n.m.Matches(x) +} + +func (n notMatcher) String() string { + // TODO: Improve this if we add a NotString method to the Matcher interface. + return "not(" + n.m.String() + ")" +} + +type assignableToTypeOfMatcher struct { + targetType reflect.Type +} + +func (m assignableToTypeOfMatcher) Matches(x interface{}) bool { + return reflect.TypeOf(x).AssignableTo(m.targetType) +} + +func (m assignableToTypeOfMatcher) String() string { + return "is assignable to " + m.targetType.Name() +} + +type allMatcher struct { + matchers []Matcher +} + +func (am allMatcher) Matches(x interface{}) bool { + for _, m := range am.matchers { + if !m.Matches(x) { + return false + } + } + return true +} + +func (am allMatcher) String() string { + ss := make([]string, 0, len(am.matchers)) + for _, matcher := range am.matchers { + ss = append(ss, matcher.String()) + } + return strings.Join(ss, "; ") +} + +type lenMatcher struct { + i int +} + +func (m lenMatcher) Matches(x interface{}) bool { + v := reflect.ValueOf(x) + switch v.Kind() { + case reflect.Array, reflect.Chan, reflect.Map, reflect.Slice, reflect.String: + return v.Len() == m.i + default: + return false + } +} + +func (m lenMatcher) String() string { + return fmt.Sprintf("has length %d", m.i) +} + +// Constructors + +// All returns a composite Matcher that returns true if and only all of the +// matchers return true. +func All(ms ...Matcher) Matcher { return allMatcher{ms} } + +// Any returns a matcher that always matches. +func Any() Matcher { return anyMatcher{} } + +// Eq returns a matcher that matches on equality. +// +// Example usage: +// Eq(5).Matches(5) // returns true +// Eq(5).Matches(4) // returns false +func Eq(x interface{}) Matcher { return eqMatcher{x} } + +// Len returns a matcher that matches on length. This matcher returns false if +// is compared to a type that is not an array, chan, map, slice, or string. +func Len(i int) Matcher { + return lenMatcher{i} +} + +// Nil returns a matcher that matches if the received value is nil. +// +// Example usage: +// var x *bytes.Buffer +// Nil().Matches(x) // returns true +// x = &bytes.Buffer{} +// Nil().Matches(x) // returns false +func Nil() Matcher { return nilMatcher{} } + +// Not reverses the results of its given child matcher. +// +// Example usage: +// Not(Eq(5)).Matches(4) // returns true +// Not(Eq(5)).Matches(5) // returns false +func Not(x interface{}) Matcher { + if m, ok := x.(Matcher); ok { + return notMatcher{m} + } + return notMatcher{Eq(x)} +} + +// AssignableToTypeOf is a Matcher that matches if the parameter to the mock +// function is assignable to the type of the parameter to this function. +// +// Example usage: +// var s fmt.Stringer = &bytes.Buffer{} +// AssignableToTypeOf(s).Matches(time.Second) // returns true +// AssignableToTypeOf(s).Matches(99) // returns false +// +// var ctx = reflect.TypeOf((*context.Context)).Elem() +// AssignableToTypeOf(ctx).Matches(context.Background()) // returns true +func AssignableToTypeOf(x interface{}) Matcher { + if xt, ok := x.(reflect.Type); ok { + return assignableToTypeOfMatcher{xt} + } + return assignableToTypeOfMatcher{reflect.TypeOf(x)} +} diff --git a/vendor/github.com/golang/mock/mockgen/model/model.go b/vendor/github.com/golang/mock/mockgen/model/model.go new file mode 100644 index 000000000..88783cfdf --- /dev/null +++ b/vendor/github.com/golang/mock/mockgen/model/model.go @@ -0,0 +1,470 @@ +// Copyright 2012 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package model contains the data model necessary for generating mock implementations. +package model + +import ( + "encoding/gob" + "fmt" + "io" + "reflect" + "strings" +) + +// pkgPath is the importable path for package model +const pkgPath = "github.com/golang/mock/mockgen/model" + +// Package is a Go package. It may be a subset. +type Package struct { + Name string + PkgPath string + Interfaces []*Interface + DotImports []string +} + +// Print writes the package name and its exported interfaces. +func (pkg *Package) Print(w io.Writer) { + _, _ = fmt.Fprintf(w, "package %s\n", pkg.Name) + for _, intf := range pkg.Interfaces { + intf.Print(w) + } +} + +// Imports returns the imports needed by the Package as a set of import paths. +func (pkg *Package) Imports() map[string]bool { + im := make(map[string]bool) + for _, intf := range pkg.Interfaces { + intf.addImports(im) + } + return im +} + +// Interface is a Go interface. +type Interface struct { + Name string + Methods []*Method +} + +// Print writes the interface name and its methods. +func (intf *Interface) Print(w io.Writer) { + _, _ = fmt.Fprintf(w, "interface %s\n", intf.Name) + for _, m := range intf.Methods { + m.Print(w) + } +} + +func (intf *Interface) addImports(im map[string]bool) { + for _, m := range intf.Methods { + m.addImports(im) + } +} + +// Method is a single method of an interface. +type Method struct { + Name string + In, Out []*Parameter + Variadic *Parameter // may be nil +} + +// Print writes the method name and its signature. +func (m *Method) Print(w io.Writer) { + _, _ = fmt.Fprintf(w, " - method %s\n", m.Name) + if len(m.In) > 0 { + _, _ = fmt.Fprintf(w, " in:\n") + for _, p := range m.In { + p.Print(w) + } + } + if m.Variadic != nil { + _, _ = fmt.Fprintf(w, " ...:\n") + m.Variadic.Print(w) + } + if len(m.Out) > 0 { + _, _ = fmt.Fprintf(w, " out:\n") + for _, p := range m.Out { + p.Print(w) + } + } +} + +func (m *Method) addImports(im map[string]bool) { + for _, p := range m.In { + p.Type.addImports(im) + } + if m.Variadic != nil { + m.Variadic.Type.addImports(im) + } + for _, p := range m.Out { + p.Type.addImports(im) + } +} + +// Parameter is an argument or return parameter of a method. +type Parameter struct { + Name string // may be empty + Type Type +} + +// Print writes a method parameter. +func (p *Parameter) Print(w io.Writer) { + n := p.Name + if n == "" { + n = `""` + } + _, _ = fmt.Fprintf(w, " - %v: %v\n", n, p.Type.String(nil, "")) +} + +// Type is a Go type. +type Type interface { + String(pm map[string]string, pkgOverride string) string + addImports(im map[string]bool) +} + +func init() { + gob.Register(&ArrayType{}) + gob.Register(&ChanType{}) + gob.Register(&FuncType{}) + gob.Register(&MapType{}) + gob.Register(&NamedType{}) + gob.Register(&PointerType{}) + + // Call gob.RegisterName to make sure it has the consistent name registered + // for both gob decoder and encoder. + // + // For a non-pointer type, gob.Register will try to get package full path by + // calling rt.PkgPath() for a name to register. If your project has vendor + // directory, it is possible that PkgPath will get a path like this: + // ../../../vendor/github.com/golang/mock/mockgen/model + gob.RegisterName(pkgPath+".PredeclaredType", PredeclaredType("")) +} + +// ArrayType is an array or slice type. +type ArrayType struct { + Len int // -1 for slices, >= 0 for arrays + Type Type +} + +func (at *ArrayType) String(pm map[string]string, pkgOverride string) string { + s := "[]" + if at.Len > -1 { + s = fmt.Sprintf("[%d]", at.Len) + } + return s + at.Type.String(pm, pkgOverride) +} + +func (at *ArrayType) addImports(im map[string]bool) { at.Type.addImports(im) } + +// ChanType is a channel type. +type ChanType struct { + Dir ChanDir // 0, 1 or 2 + Type Type +} + +func (ct *ChanType) String(pm map[string]string, pkgOverride string) string { + s := ct.Type.String(pm, pkgOverride) + if ct.Dir == RecvDir { + return "<-chan " + s + } + if ct.Dir == SendDir { + return "chan<- " + s + } + return "chan " + s +} + +func (ct *ChanType) addImports(im map[string]bool) { ct.Type.addImports(im) } + +// ChanDir is a channel direction. +type ChanDir int + +// Constants for channel directions. +const ( + RecvDir ChanDir = 1 + SendDir ChanDir = 2 +) + +// FuncType is a function type. +type FuncType struct { + In, Out []*Parameter + Variadic *Parameter // may be nil +} + +func (ft *FuncType) String(pm map[string]string, pkgOverride string) string { + args := make([]string, len(ft.In)) + for i, p := range ft.In { + args[i] = p.Type.String(pm, pkgOverride) + } + if ft.Variadic != nil { + args = append(args, "..."+ft.Variadic.Type.String(pm, pkgOverride)) + } + rets := make([]string, len(ft.Out)) + for i, p := range ft.Out { + rets[i] = p.Type.String(pm, pkgOverride) + } + retString := strings.Join(rets, ", ") + if nOut := len(ft.Out); nOut == 1 { + retString = " " + retString + } else if nOut > 1 { + retString = " (" + retString + ")" + } + return "func(" + strings.Join(args, ", ") + ")" + retString +} + +func (ft *FuncType) addImports(im map[string]bool) { + for _, p := range ft.In { + p.Type.addImports(im) + } + if ft.Variadic != nil { + ft.Variadic.Type.addImports(im) + } + for _, p := range ft.Out { + p.Type.addImports(im) + } +} + +// MapType is a map type. +type MapType struct { + Key, Value Type +} + +func (mt *MapType) String(pm map[string]string, pkgOverride string) string { + return "map[" + mt.Key.String(pm, pkgOverride) + "]" + mt.Value.String(pm, pkgOverride) +} + +func (mt *MapType) addImports(im map[string]bool) { + mt.Key.addImports(im) + mt.Value.addImports(im) +} + +// NamedType is an exported type in a package. +type NamedType struct { + Package string // may be empty + Type string // TODO: should this be typed Type? +} + +func (nt *NamedType) String(pm map[string]string, pkgOverride string) string { + // TODO: is this right? + if pkgOverride == nt.Package { + return nt.Type + } + prefix := pm[nt.Package] + if prefix != "" { + return prefix + "." + nt.Type + } + + return nt.Type +} + +func (nt *NamedType) addImports(im map[string]bool) { + if nt.Package != "" { + im[nt.Package] = true + } +} + +// PointerType is a pointer to another type. +type PointerType struct { + Type Type +} + +func (pt *PointerType) String(pm map[string]string, pkgOverride string) string { + return "*" + pt.Type.String(pm, pkgOverride) +} +func (pt *PointerType) addImports(im map[string]bool) { pt.Type.addImports(im) } + +// PredeclaredType is a predeclared type such as "int". +type PredeclaredType string + +func (pt PredeclaredType) String(map[string]string, string) string { return string(pt) } +func (pt PredeclaredType) addImports(map[string]bool) {} + +// The following code is intended to be called by the program generated by ../reflect.go. + +// InterfaceFromInterfaceType returns a pointer to an interface for the +// given reflection interface type. +func InterfaceFromInterfaceType(it reflect.Type) (*Interface, error) { + if it.Kind() != reflect.Interface { + return nil, fmt.Errorf("%v is not an interface", it) + } + intf := &Interface{} + + for i := 0; i < it.NumMethod(); i++ { + mt := it.Method(i) + // TODO: need to skip unexported methods? or just raise an error? + m := &Method{ + Name: mt.Name, + } + + var err error + m.In, m.Variadic, m.Out, err = funcArgsFromType(mt.Type) + if err != nil { + return nil, err + } + + intf.Methods = append(intf.Methods, m) + } + + return intf, nil +} + +// t's Kind must be a reflect.Func. +func funcArgsFromType(t reflect.Type) (in []*Parameter, variadic *Parameter, out []*Parameter, err error) { + nin := t.NumIn() + if t.IsVariadic() { + nin-- + } + var p *Parameter + for i := 0; i < nin; i++ { + p, err = parameterFromType(t.In(i)) + if err != nil { + return + } + in = append(in, p) + } + if t.IsVariadic() { + p, err = parameterFromType(t.In(nin).Elem()) + if err != nil { + return + } + variadic = p + } + for i := 0; i < t.NumOut(); i++ { + p, err = parameterFromType(t.Out(i)) + if err != nil { + return + } + out = append(out, p) + } + return +} + +func parameterFromType(t reflect.Type) (*Parameter, error) { + tt, err := typeFromType(t) + if err != nil { + return nil, err + } + return &Parameter{Type: tt}, nil +} + +var errorType = reflect.TypeOf((*error)(nil)).Elem() + +var byteType = reflect.TypeOf(byte(0)) + +func typeFromType(t reflect.Type) (Type, error) { + // Hack workaround for https://golang.org/issue/3853. + // This explicit check should not be necessary. + if t == byteType { + return PredeclaredType("byte"), nil + } + + if imp := t.PkgPath(); imp != "" { + return &NamedType{ + Package: impPath(imp), + Type: t.Name(), + }, nil + } + + // only unnamed or predeclared types after here + + // Lots of types have element types. Let's do the parsing and error checking for all of them. + var elemType Type + switch t.Kind() { + case reflect.Array, reflect.Chan, reflect.Map, reflect.Ptr, reflect.Slice: + var err error + elemType, err = typeFromType(t.Elem()) + if err != nil { + return nil, err + } + } + + switch t.Kind() { + case reflect.Array: + return &ArrayType{ + Len: t.Len(), + Type: elemType, + }, nil + case reflect.Bool, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr, + reflect.Float32, reflect.Float64, reflect.Complex64, reflect.Complex128, reflect.String: + return PredeclaredType(t.Kind().String()), nil + case reflect.Chan: + var dir ChanDir + switch t.ChanDir() { + case reflect.RecvDir: + dir = RecvDir + case reflect.SendDir: + dir = SendDir + } + return &ChanType{ + Dir: dir, + Type: elemType, + }, nil + case reflect.Func: + in, variadic, out, err := funcArgsFromType(t) + if err != nil { + return nil, err + } + return &FuncType{ + In: in, + Out: out, + Variadic: variadic, + }, nil + case reflect.Interface: + // Two special interfaces. + if t.NumMethod() == 0 { + return PredeclaredType("interface{}"), nil + } + if t == errorType { + return PredeclaredType("error"), nil + } + case reflect.Map: + kt, err := typeFromType(t.Key()) + if err != nil { + return nil, err + } + return &MapType{ + Key: kt, + Value: elemType, + }, nil + case reflect.Ptr: + return &PointerType{ + Type: elemType, + }, nil + case reflect.Slice: + return &ArrayType{ + Len: -1, + Type: elemType, + }, nil + case reflect.Struct: + if t.NumField() == 0 { + return PredeclaredType("struct{}"), nil + } + } + + // TODO: Struct, UnsafePointer + return nil, fmt.Errorf("can't yet turn %v (%v) into a model.Type", t, t.Kind()) +} + +// impPath sanitizes the package path returned by `PkgPath` method of a reflect Type so that +// it is importable. PkgPath might return a path that includes "vendor". These paths do not +// compile, so we need to remove everything up to and including "/vendor/". +// See https://github.com/golang/go/issues/12019. +func impPath(imp string) string { + if strings.HasPrefix(imp, "vendor/") { + imp = "/" + imp + } + if i := strings.LastIndex(imp, "/vendor/"); i != -1 { + imp = imp[i+len("/vendor/"):] + } + return imp +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 08af03ea7..3960973a2 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -57,6 +57,10 @@ github.com/go-stack/stack # github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d github.com/gogo/protobuf/proto github.com/gogo/protobuf/sortkeys +# github.com/golang/mock v1.4.4 +## explicit +github.com/golang/mock/gomock +github.com/golang/mock/mockgen/model # github.com/golang/protobuf v1.4.2 ## explicit github.com/golang/protobuf/proto