diff --git a/.golangci.yml b/.golangci.yml index 56f13cbea..602bb2c42 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -129,6 +129,7 @@ linters-settings: - github.com/logicmonitor/lm-sdk-go - github.com/logicmonitor/k8s-collectorset-controller - github.com/pkg/profile + - github.com/senseyeio/duration blocked: modules: diff --git a/go.mod b/go.mod index 31988a3ba..f0d614bdb 100644 --- a/go.mod +++ b/go.mod @@ -27,6 +27,7 @@ require ( github.com/pkg/profile v1.6.0 github.com/prometheus/client_golang v0.9.3 github.com/robfig/cron/v3 v3.0.1 + github.com/senseyeio/duration v0.0.0-20180430131211-7c2a214ada46 github.com/sirupsen/logrus v1.4.2 github.com/soheilhy/cmux v0.1.5 // indirect github.com/spf13/cobra v0.0.3 diff --git a/go.sum b/go.sum index deb2a1014..c8de29a6f 100644 --- a/go.sum +++ b/go.sum @@ -384,6 +384,8 @@ github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6L github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/senseyeio/duration v0.0.0-20180430131211-7c2a214ada46 h1:Dz0HrI1AtNSGCE8LXLLqoZU4iuOJXPWndenCsZfstA8= +github.com/senseyeio/duration v0.0.0-20180430131211-7c2a214ada46/go.mod h1:is8FVkzSi7PYLWEXT5MgWhglFsyyiW8ffxAoJqfuFZo= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= diff --git a/main.go b/main.go index c936fdb79..18ebecde1 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,8 @@ package main -import "github.com/logicmonitor/k8s-argus/cmd" +import ( + "github.com/logicmonitor/k8s-argus/cmd" +) func main() { cmd.Execute() diff --git a/pkg/config/config.go b/pkg/config/config.go index 42de4277b..0a7c72b44 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -37,7 +37,7 @@ type Config struct { EnableNewResourceTree bool `yaml:"enable_new_resource_tree"` EnableNamespacesDeletedGroups bool `yaml:"enable_namespaces_deleted_groups"` RegisterGenericFilter bool `yaml:"register_generic_filter"` - DeleteArgusPodAfter *string `yaml:"delete_argus_pod_after"` + DeleteInfraPodsAfter *string `yaml:"delete_infra_pods_after"` DisableResourceMonitoring []enums.ResourceType `yaml:"disable_resource_monitoring"` DisableResourceAlerting []enums.ResourceType `yaml:"disable_resource_alerting"` TelemetryCronString *string `yaml:"telemetry_cron_string"` @@ -282,9 +282,9 @@ func validateConfig(conf *Config) { defaultQueueSize := 100 conf.ParallelRunnerQueueSize = &defaultQueueSize } - if conf.DeleteArgusPodAfter == nil { + if conf.DeleteInfraPodsAfter == nil { scheduledDeleteTime := "P10DT0H0M0S" - conf.DeleteArgusPodAfter = &scheduledDeleteTime + conf.DeleteInfraPodsAfter = &scheduledDeleteTime } if conf.TelemetryCronString == nil { // Defaults to 10 minute if not specified diff --git a/pkg/enums/enums.go b/pkg/enums/enums.go index d1f851d7d..73a0c09dc 100644 --- a/pkg/enums/enums.go +++ b/pkg/enums/enums.go @@ -320,11 +320,11 @@ func (resourceType *ResourceType) TitlePlural() string { case Services: return "Services" case Hpas: - return "Hpas" + return "HorizontalPodAutoscalers" case Nodes: return "Nodes" case ETCD: - return "Etcds" + return "Etcd" case Namespaces: return "Namespaces" case DaemonSets: diff --git a/pkg/enums/enums.yaml b/pkg/enums/enums.yaml index 40029bb88..7fc75112c 100644 --- a/pkg/enums/enums.yaml +++ b/pkg/enums/enums.yaml @@ -60,6 +60,7 @@ resources: - hpa longName: Hpas lowerCase: horizontalpodautoscalers + titlePlural: HorizontalPodAutoscalers parsedResourceType: - horizontalpodautoscalers - horizontalpodautoscaler @@ -83,7 +84,7 @@ resources: shortName: - etcd longName: ETCD - titlePlural: Etcds + titlePlural: Etcd lowerCase: etcd parsedResourceType: - etcd diff --git a/pkg/resource/builder/builder.go b/pkg/resource/builder/builder.go index 24a8e2445..9180e4182 100644 --- a/pkg/resource/builder/builder.go +++ b/pkg/resource/builder/builder.go @@ -48,6 +48,13 @@ func (b *Builder) SystemCategory(category string, action enums.BuilderAction) ty return setProperty(constants.K8sSystemCategoriesPropertyKey, category, action) } +// DisableResourceAlerting implements types.ResourceBuilder +func (b *Builder) DisableResourceAlerting(disable bool) types.ResourceOption { + return func(device *models.Device) { + device.DisableAlerting = disable + } +} + // ResourceAnnotations implements types.ResourceBuilder func (b *Builder) ResourceAnnotations(properties map[string]string) types.ResourceOption { return func(resource *models.Device) { @@ -369,16 +376,20 @@ func (b *Builder) GetMarkDeleteOptions(lctx *lmctx.LMContext, rt enums.ResourceT options := []types.ResourceOption{ b.SystemCategory(rt.GetDeletedCategory(), enums.Add), + b.DisableResourceAlerting(true), b.DeletedOn(meta.DeletionTimestamp.Time), b.ChangePrimaryKeysToMarkDelete(), } + if val, ok := meta.Labels["logicmonitor/deleteafterduration"]; ok { + options = append(options, b.Custom("kubernetes.resourcedeleteafterduration", val)) + } // We are not deleting argus pod, as we need argus pod logs for troubleshooting if util.IsArgusPodObject(lctx, rt, meta) { // defaults to 10 days scheduledDeleteTime := "P10DT0H0M0S" conf, err := config.GetConfig(lctx) if err == nil { - scheduledDeleteTime = *conf.DeleteArgusPodAfter + scheduledDeleteTime = *conf.DeleteInfraPodsAfter } options = append(options, b.Custom("kubernetes.resourcedeleteafterduration", scheduledDeleteTime)) } diff --git a/pkg/resource/watcher.go b/pkg/resource/watcher.go index 45d30dcd4..99d1d2b4e 100644 --- a/pkg/resource/watcher.go +++ b/pkg/resource/watcher.go @@ -16,6 +16,7 @@ import ( "github.com/logicmonitor/k8s-argus/pkg/types" util "github.com/logicmonitor/k8s-argus/pkg/utilities" "github.com/logicmonitor/lm-sdk-go/models" + "github.com/senseyeio/duration" ) // AddFunc returns func @@ -59,7 +60,7 @@ func (m *Manager) UpdateFunc() func(*lmctx.LMContext, enums.ResourceType, interf } // DeleteFunc returns function -func (m *Manager) DeleteFunc() func(*lmctx.LMContext, enums.ResourceType, interface{}, ...types.ResourceOption) error { +func (m *Manager) DeleteFunc() func(*lmctx.LMContext, enums.ResourceType, interface{}, ...types.ResourceOption) error { //nolint:cyclop return func(lctx *lmctx.LMContext, rt enums.ResourceType, obj interface{}, options ...types.ResourceOption) error { log := lmlog.Logger(lctx) conf, err := config.GetConfig(lctx) @@ -71,8 +72,22 @@ func (m *Manager) DeleteFunc() func(*lmctx.LMContext, enums.ResourceType, interf if err != nil { return err } - if conf.DeleteResources && - !util.IsArgusPod(lctx, rt, resource) { + objMeta, err := rt.ObjectMeta(obj) + if err != nil { + return err + } + val, deleteAfterLabelExists := objMeta.Labels["logicmonitor/deleteafterduration"] + d := duration.Duration{} + if deleteAfterLabelExists { + du, err := duration.ParseISO8601(val) + if err != nil { + deleteAfterLabelExists = false + } else { + d = du + } + } + if (conf.DeleteResources && !util.IsArgusPod(lctx, rt, resource)) || + (!conf.DeleteResources && deleteAfterLabelExists && d.IsZero()) { err := m.Delete(lctx, rt, obj, options...) if err != nil { if util.GetHTTPStatusCodeFromLMSDKError(err) == http.StatusNotFound { diff --git a/pkg/sync/initsyncer.go b/pkg/sync/initsyncer.go index 120207dae..8d166045b 100644 --- a/pkg/sync/initsyncer.go +++ b/pkg/sync/initsyncer.go @@ -20,6 +20,7 @@ import ( "github.com/logicmonitor/k8s-argus/pkg/types" util "github.com/logicmonitor/k8s-argus/pkg/utilities" "github.com/logicmonitor/lm-sdk-go/models" + "github.com/senseyeio/duration" "github.com/sirupsen/logrus" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -92,11 +93,13 @@ func (i *InitSyncer) Sync(lctx *lmctx.LMContext) { }) childLctx = childLctx.LMContextWith(map[string]interface{}{constants.PartitionKey: fmt.Sprintf("%s-%s", cacheResourceName.Resource.String(), cacheResourceName.Name)}) - if cacheResourceName.Resource == enums.Namespaces && cacheResourceName.Name != constants.DeletedResourceGroup { - if err := i.deleteNamespace(allK8SResourcesStore, childLctx, cacheResourceName, cacheResourceMeta, log, conf); err != nil && !errors.Is(err, aerrors.ErrResourceGroupIsNotEmpty) && - !errors.Is(err, aerrors.ErrResourceGroupParentIsNotValid) && - !strings.Contains(err.Error(), util.ClusterGroupName(conf.ClusterName)) { - log.Errorf("failed to delete resource group: %s", err) + if cacheResourceName.Resource == enums.Namespaces { + if cacheResourceName.Name != constants.DeletedResourceGroup { + if err := i.deleteNamespace(allK8SResourcesStore, childLctx, cacheResourceName, cacheResourceMeta, log, conf); err != nil && !errors.Is(err, aerrors.ErrResourceGroupIsNotEmpty) && + !errors.Is(err, aerrors.ErrResourceGroupParentIsNotValid) && + !strings.Contains(err.Error(), util.ClusterGroupName(conf.ClusterName)) { + log.Errorf("failed to delete resource group: %s", err) + } } continue } @@ -198,15 +201,20 @@ func (i *InitSyncer) resolveConflicts(lctx *lmctx.LMContext, cacheMeta types.Res } } -func (i *InitSyncer) deleteResource(lctx *lmctx.LMContext, resourceName types.ResourceName, resourceMeta types.ResourceMeta) { +func (i *InitSyncer) deleteResource(lctx *lmctx.LMContext, resourceName types.ResourceName, resourceMeta types.ResourceMeta) { //nolint:cyclop log := lmlog.Logger(lctx) conf, err := config.GetConfig(lctx) if err != nil { log.Errorf("Failed to get config") return } - if conf.DeleteResources && - !util.IsArgusPodCacheMeta(lctx, resourceName.Resource, resourceMeta) { + argusDeleteAfter, err := duration.ParseISO8601(*conf.DeleteInfraPodsAfter) + if err != nil { + log.Errorf("Failed to parse delete argus after parameter to duration as per ISO 8601 format: %s", err) + return + } + if (conf.DeleteResources && !util.IsArgusPodCacheMeta(lctx, resourceName.Resource, resourceMeta)) || + (util.IsArgusPodCacheMeta(lctx, resourceName.Resource, resourceMeta) && argusDeleteAfter.IsZero() && conf.DeleteResources) { log.Info("Deleting resource") err := i.ResourceManager.DeleteResourceByID(lctx, resourceName.Resource, resourceMeta.LMID) if err != nil { @@ -223,7 +231,9 @@ func (i *InitSyncer) deleteResource(lctx *lmctx.LMContext, resourceName types.Re } } else if !resourceMeta.HasSysCategory(resourceName.Resource.GetDeletedCategory()) { log.Info("Soft delete") - deleteOptions := i.ResourceManager.GetMarkDeleteOptions(lctx, resourceName.Resource, meta.AsPartialObjectMetadata(&metav1.ObjectMeta{})) + deleteOptions := i.ResourceManager.GetMarkDeleteOptions(lctx, resourceName.Resource, meta.AsPartialObjectMetadata(&metav1.ObjectMeta{ + Labels: resourceMeta.Labels, + })) _, err = i.ResourceManager.UpdateResourceByID(lctx, resourceName.Resource, resourceMeta.LMID, deleteOptions...) if err != nil { diff --git a/pkg/tree/tree.go b/pkg/tree/tree.go index 75c6604c3..3ebf6bcad 100644 --- a/pkg/tree/tree.go +++ b/pkg/tree/tree.go @@ -8,6 +8,7 @@ import ( "github.com/logicmonitor/k8s-argus/pkg/enums" "github.com/logicmonitor/k8s-argus/pkg/lmctx" lmlog "github.com/logicmonitor/k8s-argus/pkg/log" + "github.com/logicmonitor/k8s-argus/pkg/permission" "github.com/logicmonitor/k8s-argus/pkg/resourcegroup" "github.com/logicmonitor/k8s-argus/pkg/resourcegroup/dgbuilder" "github.com/logicmonitor/k8s-argus/pkg/types" @@ -67,6 +68,7 @@ func GetResourceGroupTree(lctx *lmctx.LMContext, dgBuilder types.ResourceManager if !resource.IsNamespaceScopedResource() && resource != nodes && !conf.IsMonitoringDisabled(resource) { treeObj.ChildGroups = append(treeObj.ChildGroups, &types.ResourceGroupTree{ + DontCreate: !permission.HasPermissions(resource), Options: []types.ResourceGroupOption{ dgBuilder.GroupName(resource.TitlePlural()), dgBuilder.DisableAlerting(conf.ShouldDisableAlerting(resource)), @@ -89,24 +91,25 @@ func GetResourceGroupTree(lctx *lmctx.LMContext, dgBuilder types.ResourceManager for _, resource := range enums.ALLResourceTypes { if resource != enums.Namespaces && resource.IsNamespaceScopedResource() && !conf.IsMonitoringDisabled(resource) { - treeObj.ChildGroups = append(treeObj.ChildGroups, - &types.ResourceGroupTree{ - Options: []types.ResourceGroupOption{ - dgBuilder.GroupName(resource.TitlePlural()), - dgBuilder.DisableAlerting(conf.ShouldDisableAlerting(resource)), - dgBuilder.CustomProperties(dgbuilder.NewPropertyBuilder().AddProperties(conf.ResourceGroupProperties.Get(resource))), - }, - ChildGroups: []*types.ResourceGroupTree{ - { - DontCreate: doNotCreateDeletedGroup, - Options: []types.ResourceGroupOption{ - dgBuilder.GroupName(constants.DeletedResourceGroup), - dgBuilder.DisableAlerting(true), - dgBuilder.AppliesTo(dgbuilder.NewAppliesToBuilder().HasCategory(resource.GetDeletedCategory()).And().Auto("clustername").Equals(conf.ClusterName)), - }, + resourceTree := &types.ResourceGroupTree{ + DontCreate: !permission.HasPermissions(resource), + Options: []types.ResourceGroupOption{ + dgBuilder.GroupName(resource.TitlePlural()), + dgBuilder.DisableAlerting(conf.ShouldDisableAlerting(resource)), + dgBuilder.CustomProperties(dgbuilder.NewPropertyBuilder().AddProperties(conf.ResourceGroupProperties.Get(resource))), + }, + ChildGroups: []*types.ResourceGroupTree{ + { + DontCreate: doNotCreateDeletedGroup && !(resource == enums.Pods), + Options: []types.ResourceGroupOption{ + dgBuilder.GroupName(constants.DeletedResourceGroup), + dgBuilder.DisableAlerting(true), + dgBuilder.AppliesTo(dgbuilder.NewAppliesToBuilder().HasCategory(resource.GetDeletedCategory()).And().Auto("clustername").Equals(conf.ClusterName)), }, }, - }) + }, + } + treeObj.ChildGroups = append(treeObj.ChildGroups, resourceTree) } } return treeObj, nil @@ -131,6 +134,7 @@ func GetResourceGroupTree2(lctx *lmctx.LMContext, dgBuilder types.ResourceManage if !resource.IsNamespaceScopedResource() && resource != enums.Nodes { clusterscoped = append(clusterscoped, &types.ResourceGroupTree{ + DontCreate: !permission.HasPermissions(resource), Options: []types.ResourceGroupOption{ dgBuilder.GroupName(resource.TitlePlural()), dgBuilder.DisableAlerting(conf.ShouldDisableAlerting(resource)), diff --git a/pkg/types/types.go b/pkg/types/types.go index b97996a1d..3608391d0 100644 --- a/pkg/types/types.go +++ b/pkg/types/types.go @@ -163,6 +163,10 @@ type ResourceBuilder interface { // DeletedOn adds kubernetes.resourceDeletedOn property to the resource. DeletedOn(time.Time) ResourceOption + // DisableResourceAlerting disable alerting + DisableResourceAlerting(bool) ResourceOption + + // GetMarkDeleteOptions get deleted options GetMarkDeleteOptions(*lmctx.LMContext, enums.ResourceType, *metav1.PartialObjectMetadata) []ResourceOption } diff --git a/vendor/github.com/senseyeio/duration/.travis.yml b/vendor/github.com/senseyeio/duration/.travis.yml new file mode 100644 index 000000000..be839df16 --- /dev/null +++ b/vendor/github.com/senseyeio/duration/.travis.yml @@ -0,0 +1,14 @@ +language: go + +before_install: + - go get github.com/mattn/goveralls + - go get github.com/modocache/gover + +script: + - go test -race -v ./... + - ./coveralls.bash + +go: + - 1.9.x + - 1.10.x + - tip diff --git a/vendor/github.com/senseyeio/duration/LICENSE b/vendor/github.com/senseyeio/duration/LICENSE new file mode 100644 index 000000000..8d0173876 --- /dev/null +++ b/vendor/github.com/senseyeio/duration/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2018 Senseye +Adapted from https://github.com/ChannelMeter/iso8601duration, Copyright (c) 2014 ChannelMeter + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/senseyeio/duration/README.md b/vendor/github.com/senseyeio/duration/README.md new file mode 100644 index 000000000..b802bbb2a --- /dev/null +++ b/vendor/github.com/senseyeio/duration/README.md @@ -0,0 +1,84 @@ +Duration [![Build](https://travis-ci.org/senseyeio/duration.svg?branch=master)](https://travis-ci.org/senseyeio/duration) [![Coverage](https://coveralls.io/repos/github/senseyeio/duration/badge.svg?branch=master)](https://coveralls.io/github/senseyeio/duration?branch=master) [![Go Report Card](https://goreportcard.com/badge/senseyeio/duration)](https://goreportcard.com/report/senseyeio/duration) [![GoDoc](https://godoc.org/github.com/senseyeio/duration?status.svg)](https://godoc.org/github.com/senseyeio/duration) +======= +Parse ISO8601 duration strings, and use to shift dates/times. + +Basic Example +------------- + +```go +package main + +import ( + "fmt" + "time" + + "github.com/senseyeio/duration" +) + +func main() { + d, _ := iso8601.ParseISO8601("P1D") + today := time.Now() + tomorrow := d.Shift(today) + fmt.Println(today.Format("Jan _2")) + fmt.Println(tomorrow.Format("Jan _2")) +} +``` + +Why Does This Package Exist +--------------------------- +> Why can't we just use a `time.Duration` and `time.Add`? + +A very reasonable question. + +The code below repeatedly adds 24 hours to a `time.Time`. You might expect the time on that date to stay the same, but [_there are not always 24 hours in a day_](http://infiniteundo.com/post/25326999628/falsehoods-programmers-believe-about-time). When the clocks change in New York, the time will skew by an hour. As you can see from the output, duration.Duration.Shift() can increment the date without shifting the time. + +```go +package main + +import ( + "fmt" + "time" + + "github.com/senseyeio/duration" +) + +func main() { + loc, _ := time.LoadLocation("America/New_York") + d, _ := iso8601.ParseISO8601("P1D") + t1, _ := time.ParseInLocation("Jan 2, 2006 at 3:04pm", "Jan 1, 2006 at 3:04pm", loc) + t2 := t1 + for i := 0; i < 365; i++ { + t1 = t1.Add(24 * time.Hour) + t2 = d.Shift(t2) + fmt.Printf("time.Add:%d Duration.Shift:%d\n", t1.Hour(), t2.Hour()) + } +} + +// Outputs +// time.Add:15 Duration.Shift:15 +// time.Add:15 Duration.Shift:15 +// time.Add:15 Duration.Shift:15 +// ... +// time.Add:16 Duration.Shift:15 +// time.Add:16 Duration.Shift:15 +// time.Add:16 Duration.Shift:15 +// ... +``` + +------- +Months are tricky. Shifting by months uses `time.AddDate()`, which is great. However, be aware of how differing days in the month are accommodated. Dates will 'roll over' if the month you're shifting to has fewer days. e.g. if you start on Jan 30th and repeat every "P1M", you'll get this: + +``` +Jan 30, 2006 +Mar 2, 2006 +Apr 2, 2006 +May 2, 2006 +Jun 2, 2006 +Jul 2, 2006 +Aug 2, 2006 +Sep 2, 2006 +Oct 2, 2006 +Nov 2, 2006 +Dec 2, 2006 +Jan 2, 2007 +``` diff --git a/vendor/github.com/senseyeio/duration/coveralls.bash b/vendor/github.com/senseyeio/duration/coveralls.bash new file mode 100644 index 000000000..ac5b5716a --- /dev/null +++ b/vendor/github.com/senseyeio/duration/coveralls.bash @@ -0,0 +1,25 @@ +#!/usr/bin/env bash + +if ! type -P gover +then + echo gover missing: go get github.com/modocache/gover + exit 1 +fi + +if ! type -P goveralls +then + echo goveralls missing: go get github.com/mattn/goveralls + exit 1 +fi + +if [[ "$COVERALLS_TOKEN" == "" ]] +then + echo COVERALLS_TOKEN not set + exit 1 +fi + +go test -covermode count -coverprofile coverage.coverprofile + +gover +goveralls -coverprofile gover.coverprofile -service travis-ci -repotoken $COVERALLS_TOKEN +find . -name '*.coverprofile' -delete diff --git a/vendor/github.com/senseyeio/duration/duration.go b/vendor/github.com/senseyeio/duration/duration.go new file mode 100644 index 000000000..7163e3df9 --- /dev/null +++ b/vendor/github.com/senseyeio/duration/duration.go @@ -0,0 +1,146 @@ +// Package duration handles ISO8601-formatted durations. +package duration + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "html/template" + "regexp" + "strconv" + "time" +) + +// Duration represents an ISO8601 Duration +// https://en.wikipedia.org/wiki/ISO_8601#Durations +type Duration struct { + Y int + M int + W int + D int + // Time Component + TH int + TM int + TS int +} + +var pattern = regexp.MustCompile(`^P((?P\d+)Y)?((?P\d+)M)?((?P\d+)W)?((?P\d+)D)?(T((?P\d+)H)?((?P\d+)M)?((?P\d+)S)?)?$`) + +// ParseISO8601 parses an ISO8601 duration string. +func ParseISO8601(from string) (Duration, error) { + var match []string + var d Duration + + if pattern.MatchString(from) { + match = pattern.FindStringSubmatch(from) + } else { + return d, errors.New("could not parse duration string") + } + + for i, name := range pattern.SubexpNames() { + part := match[i] + if i == 0 || name == "" || part == "" { + continue + } + + val, err := strconv.Atoi(part) + if err != nil { + return d, err + } + switch name { + case "year": + d.Y = val + case "month": + d.M = val + case "week": + d.W = val + case "day": + d.D = val + case "hour": + d.TH = val + case "minute": + d.TM = val + case "second": + d.TS = val + default: + return d, fmt.Errorf("unknown field %s", name) + } + } + + return d, nil +} + +// IsZero reports whether d represents the zero duration, P0D. +func (d Duration) IsZero() bool { + return d.Y == 0 && d.M == 0 && d.W == 0 && d.D == 0 && d.TH == 0 && d.TM == 0 && d.TS == 0 +} + +// HasTimePart returns true if the time part of the duration is non-zero. +func (d Duration) HasTimePart() bool { + return d.TH > 0 || d.TM > 0 || d.TS > 0 +} + +// Shift returns a time.Time, shifted by the duration from the given start. +// +// NB: Shift uses time.AddDate for years, months, weeks, and days, and so +// shares its limitations. In particular, shifting by months is not recommended +// unless the start date is before the 28th of the month. Otherwise, dates will +// roll over, e.g. Aug 31 + P1M = Oct 1. +// +// Week and Day values will be combined as W*7 + D. +func (d Duration) Shift(t time.Time) time.Time { + if d.Y != 0 || d.M != 0 || d.W != 0 || d.D != 0 { + days := d.W*7 + d.D + t = t.AddDate(d.Y, d.M, days) + } + t = t.Add(d.timeDuration()) + return t +} + +func (d Duration) timeDuration() time.Duration { + var dur time.Duration + dur = dur + (time.Duration(d.TH) * time.Hour) + dur = dur + (time.Duration(d.TM) * time.Minute) + dur = dur + (time.Duration(d.TS) * time.Second) + return dur +} + +var tmpl = template.Must(template.New("duration").Parse(`P{{if .Y}}{{.Y}}Y{{end}}{{if .M}}{{.M}}M{{end}}{{if .W}}{{.W}}W{{end}}{{if .D}}{{.D}}D{{end}}{{if .HasTimePart}}T{{end }}{{if .TH}}{{.TH}}H{{end}}{{if .TM}}{{.TM}}M{{end}}{{if .TS}}{{.TS}}S{{end}}`)) + +// String returns an ISO8601-ish representation of the duration. +func (d Duration) String() string { + var s bytes.Buffer + + if d.IsZero() { + return "P0D" + } + + err := tmpl.Execute(&s, d) + if err != nil { + panic(err) + } + + return s.String() +} + +// MarshalJSON satisfies json.Marshaler. +func (d Duration) MarshalJSON() ([]byte, error) { + return json.Marshal(d.String()) +} + +// UnmarshalJSON satisfies json.Unmarshaler. +func (d *Duration) UnmarshalJSON(b []byte) error { + var s string + if err := json.Unmarshal(b, &s); err != nil { + return err + } + + tmp, err := ParseISO8601(s) + if err != nil { + return err + } + *d = tmp + + return nil +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 33a697e0f..ea77fb982 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -167,6 +167,9 @@ github.com/prometheus/procfs/internal/fs # github.com/robfig/cron/v3 v3.0.1 ## explicit github.com/robfig/cron/v3 +# github.com/senseyeio/duration v0.0.0-20180430131211-7c2a214ada46 +## explicit +github.com/senseyeio/duration # github.com/sirupsen/logrus v1.4.2 ## explicit github.com/sirupsen/logrus