diff --git a/docs/insights-archive-sample/insights-operator/conditional-gatherer-rules.json b/docs/insights-archive-sample/insights-operator/conditional-gatherer-rules.json index 600e4d7cb..8be34b67a 100644 --- a/docs/insights-archive-sample/insights-operator/conditional-gatherer-rules.json +++ b/docs/insights-archive-sample/insights-operator/conditional-gatherer-rules.json @@ -1,70 +1,106 @@ [ { - "conditions": [ - { - "type": "alert_is_firing", - "params": { - "name": "SamplesImagestreamImportFailing" + "rule": { + "conditions": [ + { + "type": "alert_is_firing", + "alert": { + "name": "SamplesImagestreamImportFailing" + } + } + ], + "gathering_functions": { + "image_streams_of_namespace": { + "namespace": "openshift-cluster-samples-operator" + }, + "logs_of_namespace": { + "namespace": "openshift-cluster-samples-operator", + "tail_lines": 100 } } - ], - "gathering_functions": { - "image_streams_of_namespace": { - "namespace": "openshift-cluster-samples-operator" - }, - "logs_of_namespace": { - "namespace": "openshift-cluster-samples-operator", - "tail_lines": 100 - } - } + }, + "errors": null, + "was_triggered": true }, { - "conditions": [ - { - "type": "alert_is_firing", - "params": { - "name": "APIRemovedInNextEUSReleaseInUse" + "rule": { + "conditions": [ + { + "type": "alert_is_firing", + "alert": { + "name": "APIRemovedInNextEUSReleaseInUse" + } + } + ], + "gathering_functions": { + "api_request_counts_of_resource_from_alert": { + "alert_name": "APIRemovedInNextEUSReleaseInUse" } } - ], - "gathering_functions": { - "api_request_counts_of_resource_from_alert": { - "alert_name": "APIRemovedInNextEUSReleaseInUse" - } - } + }, + "errors": null, + "was_triggered": false }, { - "conditions": [ - { - "type": "alert_is_firing", - "params": { - "name": "KubePodCrashLooping" + "rule": { + "conditions": [ + { + "type": "alert_is_firing", + "alert": { + "name": "KubePodCrashLooping" + } + } + ], + "gathering_functions": { + "logs_of_unhealthy_pods": { + "alert_name": "KubePodCrashLooping", + "tail_lines": 20, + "previous": true } } - ], - "gathering_functions": { - "logs_of_unhealthy_pods": { - "alert_name": "KubePodCrashLooping", - "tail_lines": 20, - "previous": true - } - } + }, + "errors": null, + "was_triggered": false }, { - "conditions": [ - { - "type": "alert_is_firing", - "params": { - "name": "KubePodNotReady" + "rule": { + "conditions": [ + { + "type": "alert_is_firing", + "alert": { + "name": "KubePodNotReady" + } + } + ], + "gathering_functions": { + "logs_of_unhealthy_pods": { + "alert_name": "KubePodNotReady", + "tail_lines": 100, + "previous": false } } - ], - "gathering_functions": { - "logs_of_unhealthy_pods": { - "alert_name": "KubePodNotReady", - "tail_lines": 100, - "previous": false + }, + "errors": null, + "was_triggered": false + }, + { + "rule": { + "conditions": [ + { + "type": "alert_is_firing", + "alert": { + "name": "AlertmanagerFailedToSendAlerts" + } + } + ], + "gathering_functions": { + "alertmanager_logs": { + "alert_name": "AlertmanagerFailedToSendAlerts", + "tail_lines": 50 + } } - } + }, + "errors": null, + "was_triggered": false } ] diff --git a/go.mod b/go.mod index 223875b4d..1a9350523 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/openshift/insights-operator go 1.16 require ( + github.com/blang/semver/v4 v4.0.0 github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da github.com/google/gofuzz v1.2.0 // indirect github.com/openshift/api v0.0.0-20210901140736-d8ed1449662d diff --git a/go.sum b/go.sum index 4c1b95098..5967def2f 100644 --- a/go.sum +++ b/go.sum @@ -72,6 +72,8 @@ github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJm github.com/blang/semver v3.5.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/certifi/gocertifi v0.0.0-20191021191039-0944d244cd40/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA= github.com/certifi/gocertifi v0.0.0-20200922220541-2c3bb06c6054/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA= diff --git a/pkg/gather/gather.go b/pkg/gather/gather.go index b2ee3d603..74354f77a 100644 --- a/pkg/gather/gather.go +++ b/pkg/gather/gather.go @@ -60,7 +60,7 @@ func CreateAllGatherers( gatherKubeConfig, gatherProtoKubeConfig, metricsGatherKubeConfig, anonymizer, controller.Interval, ) workloadsGatherer := workloads.New(gatherProtoKubeConfig) - conditionalGatherer := conditional.New(gatherProtoKubeConfig, metricsGatherKubeConfig) + conditionalGatherer := conditional.New(gatherProtoKubeConfig, metricsGatherKubeConfig, gatherKubeConfig) return []gatherers.Interface{clusterConfigGatherer, workloadsGatherer, conditionalGatherer} } diff --git a/pkg/gatherers/conditional/conditional_gatherer.go b/pkg/gatherers/conditional/conditional_gatherer.go index 6f92cfd07..dc4a5ba84 100644 --- a/pkg/gatherers/conditional/conditional_gatherer.go +++ b/pkg/gatherers/conditional/conditional_gatherer.go @@ -3,6 +3,10 @@ // they can be fetched from outside, checked that they make sense (we want to check the parameters, for example if // a rule tells to collect logs of a namespace on firing alert, we want to check that the namespace is created // by openshift and not by a user). Conditional gathering isn't considered prioritized, so we run it every 6 hours. +// +// To add a new condition, follow the next steps: +// 1. Add structures to conditions.go +// 2. Change areAllConditionsSatisfied function in conditional_gatherer.go package conditional import ( @@ -12,7 +16,10 @@ import ( "sort" "strings" + "github.com/blang/semver/v4" + configv1client "github.com/openshift/client-go/config/clientset/versioned/typed/config/v1" "github.com/prometheus/common/expfmt" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/rest" "k8s.io/klog/v2" @@ -158,13 +165,15 @@ type Gatherer struct { gatherProtoKubeConfig *rest.Config metricsGatherKubeConfig *rest.Config imageKubeConfig *rest.Config + gatherKubeConfig *rest.Config // there can be multiple instances of the same alert firingAlerts map[string][]AlertLabels gatheringRules []GatheringRule + clusterVersion string } // New creates a new instance of conditional gatherer with the appropriate configs -func New(gatherProtoKubeConfig, metricsGatherKubeConfig *rest.Config) *Gatherer { +func New(gatherProtoKubeConfig, metricsGatherKubeConfig, gatherKubeConfig *rest.Config) *Gatherer { var imageKubeConfig *rest.Config if gatherProtoKubeConfig != nil { // needed for getting image streams @@ -177,10 +186,18 @@ func New(gatherProtoKubeConfig, metricsGatherKubeConfig *rest.Config) *Gatherer gatherProtoKubeConfig: gatherProtoKubeConfig, metricsGatherKubeConfig: metricsGatherKubeConfig, imageKubeConfig: imageKubeConfig, + gatherKubeConfig: gatherKubeConfig, gatheringRules: defaultGatheringRules, } } +// GatheringRuleMetadata stores information about gathering rules +type GatheringRuleMetadata struct { + Rule GatheringRule `json:"rule"` + Errors []string `json:"errors"` + WasTriggered bool `json:"was_triggered"` +} + // GetName returns the name of the gatherer func (g *Gatherer) GetName() string { return "conditional" @@ -194,46 +211,55 @@ func (g *Gatherer) GetGatheringFunctions(ctx context.Context) (map[string]gather return nil, fmt.Errorf("got invalid config for conditional gatherer: %v", utils.SumErrors(errs)) } - err := g.updateAlertsCache(ctx) - if err != nil { - return nil, fmt.Errorf("conditional gatherer can't update alerts cache: %v", err) - } + g.updateCache(ctx) gatheringFunctions := make(map[string]gatherers.GatheringClosure) - gatheringFunctions["conditional_gatherer_rules"] = gatherers.GatheringClosure{ - Run: g.GatherConditionalGathererRules, - CanFail: canConditionalGathererFail, - } + var metadata []GatheringRuleMetadata for _, conditionalGathering := range g.gatheringRules { + ruleMetadata := GatheringRuleMetadata{ + Rule: conditionalGathering, + } + allConditionsAreSatisfied, err := g.areAllConditionsSatisfied(conditionalGathering.Conditions) if err != nil { - return nil, err + klog.Errorf("error checking conditions for a gathering rule: %v", err) + ruleMetadata.Errors = append(ruleMetadata.Errors, err.Error()) } + + ruleMetadata.WasTriggered = allConditionsAreSatisfied + if allConditionsAreSatisfied { functions, errs := g.createGatheringClosures(conditionalGathering.GatheringFunctions) if len(errs) > 0 { - return nil, err + klog.Errorf("error(s) creating a closure for a gathering rule: %v", errs) + for _, err := range errs { + ruleMetadata.Errors = append(ruleMetadata.Errors, err.Error()) + } } for funcName, function := range functions { gatheringFunctions[funcName] = function } } - } - return gatheringFunctions, nil -} + metadata = append(metadata, ruleMetadata) + } -// GatherConditionalGathererRules stores the gathering rules in insights-operator/conditional-gatherer-rules.json -func (g *Gatherer) GatherConditionalGathererRules(context.Context) ([]record.Record, []error) { - return []record.Record{ - { - Name: "insights-operator/conditional-gatherer-rules", - Item: record.JSONMarshaller{Object: g.gatheringRules}, + gatheringFunctions["conditional_gatherer_rules"] = gatherers.GatheringClosure{ + Run: func(context.Context) ([]record.Record, []error) { + return []record.Record{ + { + Name: "insights-operator/conditional-gatherer-rules", + Item: record.JSONMarshaller{Object: metadata}, + }, + }, nil }, - }, nil + CanFail: canConditionalGathererFail, + } + + return gatheringFunctions, nil } // areAllConditionsSatisfied returns true if all the conditions are satisfied, for example if the condition is @@ -246,8 +272,16 @@ func (g *Gatherer) areAllConditionsSatisfied(conditions []ConditionWithParams) ( return false, fmt.Errorf("alert field should not be nil") } - if !g.isAlertFiring(condition.Alert.Name) { - return false, nil + if firing, err := g.isAlertFiring(condition.Alert.Name); !firing || err != nil { + return false, err + } + case ClusterVersionMatches: + if condition.ClusterVersionMatches == nil { + return false, fmt.Errorf("cluster_version_matches field should not be nil") + } + + if doesMatch, err := g.doesClusterVersionMatch(condition.ClusterVersionMatches.Version); !doesMatch || err != nil { + return false, err } default: return false, fmt.Errorf("unknown condition type: %v", condition.Type) @@ -257,21 +291,30 @@ func (g *Gatherer) areAllConditionsSatisfied(conditions []ConditionWithParams) ( return true, nil } -// updateAlertsCache updates the cache with firing alerts -func (g *Gatherer) updateAlertsCache(ctx context.Context) error { +// updateCache updates alerts and version caches +func (g *Gatherer) updateCache(ctx context.Context) { if g.metricsGatherKubeConfig == nil { - return nil + return } metricsClient, err := rest.RESTClientFor(g.metricsGatherKubeConfig) if err != nil { - return err + klog.Errorf("unable to update alerts cache: %v", err) + } else if err := g.updateAlertsCache(ctx, metricsClient); err != nil { //nolint:govet + klog.Errorf("unable to update alerts cache: %v", err) + g.firingAlerts = nil } - return g.updateAlertsCacheFromClient(ctx, metricsClient) + configClient, err := configv1client.NewForConfig(g.gatherKubeConfig) + if err != nil { + klog.Errorf("unable to update version cache: %v", err) + } else if err := g.updateVersionCache(ctx, configClient); err != nil { + klog.Errorf("unable to update version cache: %v", err) + g.clusterVersion = "" + } } -func (g *Gatherer) updateAlertsCacheFromClient(ctx context.Context, metricsClient rest.Interface) error { +func (g *Gatherer) updateAlertsCache(ctx context.Context, metricsClient rest.Interface) error { const logPrefix = "conditional gatherer: " g.firingAlerts = make(map[string][]AlertLabels) @@ -282,20 +325,24 @@ func (g *Gatherer) updateAlertsCacheFromClient(ctx context.Context, metricsClien if err != nil { return err } + var parser expfmt.TextParser metricFamilies, err := parser.TextToMetricFamilies(bytes.NewReader(data)) if err != nil { return err } + if len(metricFamilies) > 1 { // just log cuz everything would still work klog.Warning(logPrefix + "unexpected output from prometheus metrics parser") } + metricFamily, found := metricFamilies["ALERTS"] if !found { klog.Info(logPrefix + "no alerts are firing") return nil } + for _, metric := range metricFamily.GetMetric() { if metric == nil { klog.Info(logPrefix + "metric is nil") @@ -320,10 +367,47 @@ func (g *Gatherer) updateAlertsCacheFromClient(ctx context.Context, metricsClien return nil } +func (g *Gatherer) updateVersionCache(ctx context.Context, configClient configv1client.ConfigV1Interface) error { + clusterVersion, err := configClient.ClusterVersions().Get(ctx, "version", metav1.GetOptions{}) + if err != nil { + return err + } + + g.clusterVersion = clusterVersion.Status.Desired.Version + + return nil +} + // isAlertFiring using the cache it returns true if the alert is firing -func (g *Gatherer) isAlertFiring(alertName string) bool { +func (g *Gatherer) isAlertFiring(alertName string) (bool, error) { + if g.firingAlerts == nil { + return false, fmt.Errorf("alerts cache is missing") + } + _, alertIsFiring := g.firingAlerts[alertName] - return alertIsFiring + return alertIsFiring, nil +} + +func (g *Gatherer) doesClusterVersionMatch(expectedVersionExpression string) (bool, error) { + if len(g.clusterVersion) == 0 { + return false, fmt.Errorf("cluster version is missing") + } + + clusterVersion, err := semver.Parse(g.clusterVersion) + if err != nil { + return false, err + } + + expectedRange, err := semver.ParseRange(expectedVersionExpression) + if err != nil { + return false, err + } + + // ignore everything after the first three numbers + clusterVersion.Pre = nil + clusterVersion.Build = nil + + return expectedRange(clusterVersion), nil } // createGatheringClosures produces gathering closures from the rules diff --git a/pkg/gatherers/conditional/conditional_gatherer_test.go b/pkg/gatherers/conditional/conditional_gatherer_test.go index 982add0b8..2fd459cf4 100644 --- a/pkg/gatherers/conditional/conditional_gatherer_test.go +++ b/pkg/gatherers/conditional/conditional_gatherer_test.go @@ -2,7 +2,6 @@ package conditional import ( "context" - "encoding/json" "io" "net/http" "strings" @@ -16,7 +15,7 @@ import ( ) func newEmptyGatherer() *Gatherer { - return New(nil, nil) + return New(nil, nil, nil) } func Test_Gatherer_Basic(t *testing.T) { @@ -35,7 +34,7 @@ func Test_Gatherer_Basic(t *testing.T) { func Test_Gatherer_GetGatheringFunctions(t *testing.T) { gatherer := newEmptyGatherer() - err := gatherer.updateAlertsCacheFromClient(context.TODO(), newFakeClientWithMetrics( + err := gatherer.updateAlertsCache(context.TODO(), newFakeClientWithMetrics( `ALERTS{alertname="SamplesImagestreamImportFailing",alertstate="firing"} 1 1621618110163`, )) assert.NoError(t, err) @@ -68,7 +67,7 @@ func Test_Gatherer_GetGatheringFunctions_InvalidConfig(t *testing.T) { }, } // invalid namespace (doesn't start with openshift-) - err := gatherer.updateAlertsCacheFromClient(context.TODO(), newFakeClientWithMetrics( + err := gatherer.updateAlertsCache(context.TODO(), newFakeClientWithMetrics( `ALERTS{alertname="SamplesImagestreamImportFailing",alertstate="firing"} 1 1621618110163`, )) assert.NoError(t, err) @@ -97,7 +96,7 @@ func Test_Gatherer_GetGatheringFunctions_NoConditionsAreSatisfied(t *testing.T) func Test_Gatherer_GetGatheringFunctions_ConditionIsSatisfied(t *testing.T) { gatherer := newEmptyGatherer() - err := gatherer.updateAlertsCacheFromClient(context.TODO(), newFakeClientWithMetrics( + err := gatherer.updateAlertsCache(context.TODO(), newFakeClientWithMetrics( "ALERTS{alertname=\"SamplesImagestreamImportFailing\",alertstate=\"firing\"} 1 1621618110163\n", )) assert.NoError(t, err) @@ -116,9 +115,11 @@ func Test_Gatherer_GetGatheringFunctions_ConditionIsSatisfied(t *testing.T) { _, found = gatheringFunctions["image_streams_of_namespace/namespace=openshift-cluster-samples-operator"] assert.True(t, found) - assert.True(t, gatherer.isAlertFiring("SamplesImagestreamImportFailing")) + firing, err := gatherer.isAlertFiring("SamplesImagestreamImportFailing") + assert.NoError(t, err) + assert.True(t, firing) - err = gatherer.updateAlertsCacheFromClient(context.TODO(), newFakeClientWithMetrics( + err = gatherer.updateAlertsCache(context.TODO(), newFakeClientWithMetrics( "ALERTS{alertname=\"OtherAlert\",alertstate=\"firing\"} 1 1621618110163\n", )) assert.NoError(t, err) @@ -137,7 +138,9 @@ func Test_Gatherer_GetGatheringFunctions_ConditionIsSatisfied(t *testing.T) { _, found = gatheringFunctions["image_streams_of_namespace/namespace=openshift-cluster-samples-operator"] assert.False(t, found) - assert.False(t, gatherer.isAlertFiring("SamplesImagestreamImportFailing")) + firing, err = gatherer.isAlertFiring("SamplesImagestreamImportFailing") + assert.NoError(t, err) + assert.False(t, firing) } func Test_getConditionalGatheringFunctionName(t *testing.T) { @@ -151,24 +154,6 @@ func Test_getConditionalGatheringFunctionName(t *testing.T) { assert.Equal(t, "func/param1=test,param2=5,param3=9", res) } -func Test_Gatherer_GatherConditionalGathererRules(t *testing.T) { - gatherer := newEmptyGatherer() - records, errs := gatherer.GatherConditionalGathererRules(context.TODO()) - assert.Empty(t, errs) - - assert.Len(t, records, 1) - assert.Equal(t, "insights-operator/conditional-gatherer-rules", records[0].Name) - - item, err := records[0].Item.Marshal(context.TODO()) - assert.NoError(t, err) - - var gotGatheringRules []GatheringRule - err = json.Unmarshal(item, &gotGatheringRules) - assert.NoError(t, err) - - assert.Len(t, gotGatheringRules, 5) -} - func newFakeClientWithMetrics(metrics string) *fake.RESTClient { fakeClient := &fake.RESTClient{ NegotiatedSerializer: scheme.Codecs.WithoutConversion(), @@ -182,3 +167,48 @@ func newFakeClientWithMetrics(metrics string) *fake.RESTClient { } return fakeClient } + +func Test_Gatherer_doesClusterVersionMatch(t *testing.T) { + g := newEmptyGatherer() + + type testCase struct { + expectedVersion string + shouldMatch bool + } + + g.clusterVersion = "4.8.0-0.nightly-2021-06-13-101614" + + for _, testCase := range []testCase{ + { + expectedVersion: "4.8.x", + shouldMatch: true, + }, + { + expectedVersion: "4.8.0", + shouldMatch: true, + }, + { + expectedVersion: "4.8.1", + shouldMatch: false, + }, + { + expectedVersion: ">=4.8.0", + shouldMatch: true, + }, + { + expectedVersion: ">1.0.0 <2.0.0", + shouldMatch: false, + }, + { + expectedVersion: ">1.0.0 <2.0.0 || >=3.0.0", + shouldMatch: true, + }, + } { + doesMatch, err := g.doesClusterVersionMatch(testCase.expectedVersion) + if err != nil { + assert.Error(t, err) + } + + assert.Equal(t, testCase.shouldMatch, doesMatch) + } +} diff --git a/pkg/gatherers/conditional/conditions.go b/pkg/gatherers/conditional/conditions.go index de04ee6aa..e40a9e715 100644 --- a/pkg/gatherers/conditional/conditions.go +++ b/pkg/gatherers/conditional/conditions.go @@ -2,8 +2,9 @@ package conditional // ConditionWithParams is a type holding a condition with its params type ConditionWithParams struct { - Type ConditionType `json:"type"` - Alert *AlertConditionParams `json:"alert,omitempty"` + Type ConditionType `json:"type"` + Alert *AlertConditionParams `json:"alert,omitempty"` + ClusterVersionMatches *ClusterVersionMatchesConditionParams `json:"cluster_version_matches,omitempty"` } // condition types: @@ -15,6 +16,10 @@ type ConditionType string // the params are in the field `alert` const AlertIsFiring ConditionType = "alert_is_firing" +// ClusterVersionMatches is a condition to check that the current cluster version +// matches the provided semantic versioning expression +const ClusterVersionMatches ConditionType = "cluster_version_matches" + // params: // AlertConditionParams is a type holding params for alert_is_firing condition @@ -22,3 +27,9 @@ type AlertConditionParams struct { // Name of the alert Name string `json:"name"` } + +// ClusterVersionMatchesConditionParams is a type holding params for cluster_version_matches condition +type ClusterVersionMatchesConditionParams struct { + // Version is a semantic versioning expression + Version string `json:"version"` +} diff --git a/pkg/gatherers/conditional/gathering_functions.go b/pkg/gatherers/conditional/gathering_functions.go index 32a5b2587..b35557554 100644 --- a/pkg/gatherers/conditional/gathering_functions.go +++ b/pkg/gatherers/conditional/gathering_functions.go @@ -24,14 +24,17 @@ const ( // GatherAPIRequestCounts is a function collecting api request counts for the resources read // from the corresponding alert + // See file gather_api_requests_count.go GatherAPIRequestCounts GatheringFunctionName = "api_request_counts_of_resource_from_alert" // GatherAlertmanagerLogs is the function collection the alertmanager logs from containers // See file alertmanager_logs.go GatherAlertmanagerLogs GatheringFunctionName = "alertmanager_logs" -) -const GatherLogsOfUnhealthyPods GatheringFunctionName = "logs_of_unhealthy_pods" + // GatherLogsOfUnhealthyPods is a function collecting logs of unhealthy pods + // See file gather_logs_of_unhealthy_pods.go + GatherLogsOfUnhealthyPods GatheringFunctionName = "logs_of_unhealthy_pods" +) func (name GatheringFunctionName) NewParams(jsonParams []byte) (interface{}, error) { switch name { diff --git a/pkg/gatherers/conditional/gathering_rule.schema.json b/pkg/gatherers/conditional/gathering_rule.schema.json index b479ca5bd..283b4c0bb 100644 --- a/pkg/gatherers/conditional/gathering_rule.schema.json +++ b/pkg/gatherers/conditional/gathering_rule.schema.json @@ -74,6 +74,40 @@ } } } + }, + { + "type": "object", + "title": "ConditionWithParams", + "description": "cluster_version_matches condition", + "required": [ + "type", + "cluster_version_matches" + ], + "properties": { + "type": { + "type": "string", + "title": "Type", + "description": "Type of the condition cluster_version_matches", + "const": "cluster_version_matches" + }, + "cluster_version_matches": { + "type": "object", + "title": "ClusterVersionMatchesConditionParams", + "description": "Parameters of the condition cluster_version_matches", + "required": [ + "version" + ], + "properties": { + "name": { + "type": "string", + "title": "Version", + "description": "Version contains a semantic versioning expression", + "minLength": 1, + "maxLength": 32 + } + } + } + } } ] } diff --git a/vendor/github.com/blang/semver/v4/LICENSE b/vendor/github.com/blang/semver/v4/LICENSE new file mode 100644 index 000000000..5ba5c86fc --- /dev/null +++ b/vendor/github.com/blang/semver/v4/LICENSE @@ -0,0 +1,22 @@ +The MIT License + +Copyright (c) 2014 Benedikt Lang + +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/blang/semver/v4/go.mod b/vendor/github.com/blang/semver/v4/go.mod new file mode 100644 index 000000000..06d262218 --- /dev/null +++ b/vendor/github.com/blang/semver/v4/go.mod @@ -0,0 +1,3 @@ +module github.com/blang/semver/v4 + +go 1.14 diff --git a/vendor/github.com/blang/semver/v4/json.go b/vendor/github.com/blang/semver/v4/json.go new file mode 100644 index 000000000..a74bf7c44 --- /dev/null +++ b/vendor/github.com/blang/semver/v4/json.go @@ -0,0 +1,23 @@ +package semver + +import ( + "encoding/json" +) + +// MarshalJSON implements the encoding/json.Marshaler interface. +func (v Version) MarshalJSON() ([]byte, error) { + return json.Marshal(v.String()) +} + +// UnmarshalJSON implements the encoding/json.Unmarshaler interface. +func (v *Version) UnmarshalJSON(data []byte) (err error) { + var versionString string + + if err = json.Unmarshal(data, &versionString); err != nil { + return + } + + *v, err = Parse(versionString) + + return +} diff --git a/vendor/github.com/blang/semver/v4/range.go b/vendor/github.com/blang/semver/v4/range.go new file mode 100644 index 000000000..95f7139b9 --- /dev/null +++ b/vendor/github.com/blang/semver/v4/range.go @@ -0,0 +1,416 @@ +package semver + +import ( + "fmt" + "strconv" + "strings" + "unicode" +) + +type wildcardType int + +const ( + noneWildcard wildcardType = iota + majorWildcard wildcardType = 1 + minorWildcard wildcardType = 2 + patchWildcard wildcardType = 3 +) + +func wildcardTypefromInt(i int) wildcardType { + switch i { + case 1: + return majorWildcard + case 2: + return minorWildcard + case 3: + return patchWildcard + default: + return noneWildcard + } +} + +type comparator func(Version, Version) bool + +var ( + compEQ comparator = func(v1 Version, v2 Version) bool { + return v1.Compare(v2) == 0 + } + compNE = func(v1 Version, v2 Version) bool { + return v1.Compare(v2) != 0 + } + compGT = func(v1 Version, v2 Version) bool { + return v1.Compare(v2) == 1 + } + compGE = func(v1 Version, v2 Version) bool { + return v1.Compare(v2) >= 0 + } + compLT = func(v1 Version, v2 Version) bool { + return v1.Compare(v2) == -1 + } + compLE = func(v1 Version, v2 Version) bool { + return v1.Compare(v2) <= 0 + } +) + +type versionRange struct { + v Version + c comparator +} + +// rangeFunc creates a Range from the given versionRange. +func (vr *versionRange) rangeFunc() Range { + return Range(func(v Version) bool { + return vr.c(v, vr.v) + }) +} + +// Range represents a range of versions. +// A Range can be used to check if a Version satisfies it: +// +// range, err := semver.ParseRange(">1.0.0 <2.0.0") +// range(semver.MustParse("1.1.1") // returns true +type Range func(Version) bool + +// OR combines the existing Range with another Range using logical OR. +func (rf Range) OR(f Range) Range { + return Range(func(v Version) bool { + return rf(v) || f(v) + }) +} + +// AND combines the existing Range with another Range using logical AND. +func (rf Range) AND(f Range) Range { + return Range(func(v Version) bool { + return rf(v) && f(v) + }) +} + +// ParseRange parses a range and returns a Range. +// If the range could not be parsed an error is returned. +// +// Valid ranges are: +// - "<1.0.0" +// - "<=1.0.0" +// - ">1.0.0" +// - ">=1.0.0" +// - "1.0.0", "=1.0.0", "==1.0.0" +// - "!1.0.0", "!=1.0.0" +// +// A Range can consist of multiple ranges separated by space: +// Ranges can be linked by logical AND: +// - ">1.0.0 <2.0.0" would match between both ranges, so "1.1.1" and "1.8.7" but not "1.0.0" or "2.0.0" +// - ">1.0.0 <3.0.0 !2.0.3-beta.2" would match every version between 1.0.0 and 3.0.0 except 2.0.3-beta.2 +// +// Ranges can also be linked by logical OR: +// - "<2.0.0 || >=3.0.0" would match "1.x.x" and "3.x.x" but not "2.x.x" +// +// AND has a higher precedence than OR. It's not possible to use brackets. +// +// Ranges can be combined by both AND and OR +// +// - `>1.0.0 <2.0.0 || >3.0.0 !4.2.1` would match `1.2.3`, `1.9.9`, `3.1.1`, but not `4.2.1`, `2.1.1` +func ParseRange(s string) (Range, error) { + parts := splitAndTrim(s) + orParts, err := splitORParts(parts) + if err != nil { + return nil, err + } + expandedParts, err := expandWildcardVersion(orParts) + if err != nil { + return nil, err + } + var orFn Range + for _, p := range expandedParts { + var andFn Range + for _, ap := range p { + opStr, vStr, err := splitComparatorVersion(ap) + if err != nil { + return nil, err + } + vr, err := buildVersionRange(opStr, vStr) + if err != nil { + return nil, fmt.Errorf("Could not parse Range %q: %s", ap, err) + } + rf := vr.rangeFunc() + + // Set function + if andFn == nil { + andFn = rf + } else { // Combine with existing function + andFn = andFn.AND(rf) + } + } + if orFn == nil { + orFn = andFn + } else { + orFn = orFn.OR(andFn) + } + + } + return orFn, nil +} + +// splitORParts splits the already cleaned parts by '||'. +// Checks for invalid positions of the operator and returns an +// error if found. +func splitORParts(parts []string) ([][]string, error) { + var ORparts [][]string + last := 0 + for i, p := range parts { + if p == "||" { + if i == 0 { + return nil, fmt.Errorf("First element in range is '||'") + } + ORparts = append(ORparts, parts[last:i]) + last = i + 1 + } + } + if last == len(parts) { + return nil, fmt.Errorf("Last element in range is '||'") + } + ORparts = append(ORparts, parts[last:]) + return ORparts, nil +} + +// buildVersionRange takes a slice of 2: operator and version +// and builds a versionRange, otherwise an error. +func buildVersionRange(opStr, vStr string) (*versionRange, error) { + c := parseComparator(opStr) + if c == nil { + return nil, fmt.Errorf("Could not parse comparator %q in %q", opStr, strings.Join([]string{opStr, vStr}, "")) + } + v, err := Parse(vStr) + if err != nil { + return nil, fmt.Errorf("Could not parse version %q in %q: %s", vStr, strings.Join([]string{opStr, vStr}, ""), err) + } + + return &versionRange{ + v: v, + c: c, + }, nil + +} + +// inArray checks if a byte is contained in an array of bytes +func inArray(s byte, list []byte) bool { + for _, el := range list { + if el == s { + return true + } + } + return false +} + +// splitAndTrim splits a range string by spaces and cleans whitespaces +func splitAndTrim(s string) (result []string) { + last := 0 + var lastChar byte + excludeFromSplit := []byte{'>', '<', '='} + for i := 0; i < len(s); i++ { + if s[i] == ' ' && !inArray(lastChar, excludeFromSplit) { + if last < i-1 { + result = append(result, s[last:i]) + } + last = i + 1 + } else if s[i] != ' ' { + lastChar = s[i] + } + } + if last < len(s)-1 { + result = append(result, s[last:]) + } + + for i, v := range result { + result[i] = strings.Replace(v, " ", "", -1) + } + + // parts := strings.Split(s, " ") + // for _, x := range parts { + // if s := strings.TrimSpace(x); len(s) != 0 { + // result = append(result, s) + // } + // } + return +} + +// splitComparatorVersion splits the comparator from the version. +// Input must be free of leading or trailing spaces. +func splitComparatorVersion(s string) (string, string, error) { + i := strings.IndexFunc(s, unicode.IsDigit) + if i == -1 { + return "", "", fmt.Errorf("Could not get version from string: %q", s) + } + return strings.TrimSpace(s[0:i]), s[i:], nil +} + +// getWildcardType will return the type of wildcard that the +// passed version contains +func getWildcardType(vStr string) wildcardType { + parts := strings.Split(vStr, ".") + nparts := len(parts) + wildcard := parts[nparts-1] + + possibleWildcardType := wildcardTypefromInt(nparts) + if wildcard == "x" { + return possibleWildcardType + } + + return noneWildcard +} + +// createVersionFromWildcard will convert a wildcard version +// into a regular version, replacing 'x's with '0's, handling +// special cases like '1.x.x' and '1.x' +func createVersionFromWildcard(vStr string) string { + // handle 1.x.x + vStr2 := strings.Replace(vStr, ".x.x", ".x", 1) + vStr2 = strings.Replace(vStr2, ".x", ".0", 1) + parts := strings.Split(vStr2, ".") + + // handle 1.x + if len(parts) == 2 { + return vStr2 + ".0" + } + + return vStr2 +} + +// incrementMajorVersion will increment the major version +// of the passed version +func incrementMajorVersion(vStr string) (string, error) { + parts := strings.Split(vStr, ".") + i, err := strconv.Atoi(parts[0]) + if err != nil { + return "", err + } + parts[0] = strconv.Itoa(i + 1) + + return strings.Join(parts, "."), nil +} + +// incrementMajorVersion will increment the minor version +// of the passed version +func incrementMinorVersion(vStr string) (string, error) { + parts := strings.Split(vStr, ".") + i, err := strconv.Atoi(parts[1]) + if err != nil { + return "", err + } + parts[1] = strconv.Itoa(i + 1) + + return strings.Join(parts, "."), nil +} + +// expandWildcardVersion will expand wildcards inside versions +// following these rules: +// +// * when dealing with patch wildcards: +// >= 1.2.x will become >= 1.2.0 +// <= 1.2.x will become < 1.3.0 +// > 1.2.x will become >= 1.3.0 +// < 1.2.x will become < 1.2.0 +// != 1.2.x will become < 1.2.0 >= 1.3.0 +// +// * when dealing with minor wildcards: +// >= 1.x will become >= 1.0.0 +// <= 1.x will become < 2.0.0 +// > 1.x will become >= 2.0.0 +// < 1.0 will become < 1.0.0 +// != 1.x will become < 1.0.0 >= 2.0.0 +// +// * when dealing with wildcards without +// version operator: +// 1.2.x will become >= 1.2.0 < 1.3.0 +// 1.x will become >= 1.0.0 < 2.0.0 +func expandWildcardVersion(parts [][]string) ([][]string, error) { + var expandedParts [][]string + for _, p := range parts { + var newParts []string + for _, ap := range p { + if strings.Contains(ap, "x") { + opStr, vStr, err := splitComparatorVersion(ap) + if err != nil { + return nil, err + } + + versionWildcardType := getWildcardType(vStr) + flatVersion := createVersionFromWildcard(vStr) + + var resultOperator string + var shouldIncrementVersion bool + switch opStr { + case ">": + resultOperator = ">=" + shouldIncrementVersion = true + case ">=": + resultOperator = ">=" + case "<": + resultOperator = "<" + case "<=": + resultOperator = "<" + shouldIncrementVersion = true + case "", "=", "==": + newParts = append(newParts, ">="+flatVersion) + resultOperator = "<" + shouldIncrementVersion = true + case "!=", "!": + newParts = append(newParts, "<"+flatVersion) + resultOperator = ">=" + shouldIncrementVersion = true + } + + var resultVersion string + if shouldIncrementVersion { + switch versionWildcardType { + case patchWildcard: + resultVersion, _ = incrementMinorVersion(flatVersion) + case minorWildcard: + resultVersion, _ = incrementMajorVersion(flatVersion) + } + } else { + resultVersion = flatVersion + } + + ap = resultOperator + resultVersion + } + newParts = append(newParts, ap) + } + expandedParts = append(expandedParts, newParts) + } + + return expandedParts, nil +} + +func parseComparator(s string) comparator { + switch s { + case "==": + fallthrough + case "": + fallthrough + case "=": + return compEQ + case ">": + return compGT + case ">=": + return compGE + case "<": + return compLT + case "<=": + return compLE + case "!": + fallthrough + case "!=": + return compNE + } + + return nil +} + +// MustParseRange is like ParseRange but panics if the range cannot be parsed. +func MustParseRange(s string) Range { + r, err := ParseRange(s) + if err != nil { + panic(`semver: ParseRange(` + s + `): ` + err.Error()) + } + return r +} diff --git a/vendor/github.com/blang/semver/v4/semver.go b/vendor/github.com/blang/semver/v4/semver.go new file mode 100644 index 000000000..307de610f --- /dev/null +++ b/vendor/github.com/blang/semver/v4/semver.go @@ -0,0 +1,476 @@ +package semver + +import ( + "errors" + "fmt" + "strconv" + "strings" +) + +const ( + numbers string = "0123456789" + alphas = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-" + alphanum = alphas + numbers +) + +// SpecVersion is the latest fully supported spec version of semver +var SpecVersion = Version{ + Major: 2, + Minor: 0, + Patch: 0, +} + +// Version represents a semver compatible version +type Version struct { + Major uint64 + Minor uint64 + Patch uint64 + Pre []PRVersion + Build []string //No Precedence +} + +// Version to string +func (v Version) String() string { + b := make([]byte, 0, 5) + b = strconv.AppendUint(b, v.Major, 10) + b = append(b, '.') + b = strconv.AppendUint(b, v.Minor, 10) + b = append(b, '.') + b = strconv.AppendUint(b, v.Patch, 10) + + if len(v.Pre) > 0 { + b = append(b, '-') + b = append(b, v.Pre[0].String()...) + + for _, pre := range v.Pre[1:] { + b = append(b, '.') + b = append(b, pre.String()...) + } + } + + if len(v.Build) > 0 { + b = append(b, '+') + b = append(b, v.Build[0]...) + + for _, build := range v.Build[1:] { + b = append(b, '.') + b = append(b, build...) + } + } + + return string(b) +} + +// FinalizeVersion discards prerelease and build number and only returns +// major, minor and patch number. +func (v Version) FinalizeVersion() string { + b := make([]byte, 0, 5) + b = strconv.AppendUint(b, v.Major, 10) + b = append(b, '.') + b = strconv.AppendUint(b, v.Minor, 10) + b = append(b, '.') + b = strconv.AppendUint(b, v.Patch, 10) + return string(b) +} + +// Equals checks if v is equal to o. +func (v Version) Equals(o Version) bool { + return (v.Compare(o) == 0) +} + +// EQ checks if v is equal to o. +func (v Version) EQ(o Version) bool { + return (v.Compare(o) == 0) +} + +// NE checks if v is not equal to o. +func (v Version) NE(o Version) bool { + return (v.Compare(o) != 0) +} + +// GT checks if v is greater than o. +func (v Version) GT(o Version) bool { + return (v.Compare(o) == 1) +} + +// GTE checks if v is greater than or equal to o. +func (v Version) GTE(o Version) bool { + return (v.Compare(o) >= 0) +} + +// GE checks if v is greater than or equal to o. +func (v Version) GE(o Version) bool { + return (v.Compare(o) >= 0) +} + +// LT checks if v is less than o. +func (v Version) LT(o Version) bool { + return (v.Compare(o) == -1) +} + +// LTE checks if v is less than or equal to o. +func (v Version) LTE(o Version) bool { + return (v.Compare(o) <= 0) +} + +// LE checks if v is less than or equal to o. +func (v Version) LE(o Version) bool { + return (v.Compare(o) <= 0) +} + +// Compare compares Versions v to o: +// -1 == v is less than o +// 0 == v is equal to o +// 1 == v is greater than o +func (v Version) Compare(o Version) int { + if v.Major != o.Major { + if v.Major > o.Major { + return 1 + } + return -1 + } + if v.Minor != o.Minor { + if v.Minor > o.Minor { + return 1 + } + return -1 + } + if v.Patch != o.Patch { + if v.Patch > o.Patch { + return 1 + } + return -1 + } + + // Quick comparison if a version has no prerelease versions + if len(v.Pre) == 0 && len(o.Pre) == 0 { + return 0 + } else if len(v.Pre) == 0 && len(o.Pre) > 0 { + return 1 + } else if len(v.Pre) > 0 && len(o.Pre) == 0 { + return -1 + } + + i := 0 + for ; i < len(v.Pre) && i < len(o.Pre); i++ { + if comp := v.Pre[i].Compare(o.Pre[i]); comp == 0 { + continue + } else if comp == 1 { + return 1 + } else { + return -1 + } + } + + // If all pr versions are the equal but one has further prversion, this one greater + if i == len(v.Pre) && i == len(o.Pre) { + return 0 + } else if i == len(v.Pre) && i < len(o.Pre) { + return -1 + } else { + return 1 + } + +} + +// IncrementPatch increments the patch version +func (v *Version) IncrementPatch() error { + v.Patch++ + return nil +} + +// IncrementMinor increments the minor version +func (v *Version) IncrementMinor() error { + v.Minor++ + v.Patch = 0 + return nil +} + +// IncrementMajor increments the major version +func (v *Version) IncrementMajor() error { + v.Major++ + v.Minor = 0 + v.Patch = 0 + return nil +} + +// Validate validates v and returns error in case +func (v Version) Validate() error { + // Major, Minor, Patch already validated using uint64 + + for _, pre := range v.Pre { + if !pre.IsNum { //Numeric prerelease versions already uint64 + if len(pre.VersionStr) == 0 { + return fmt.Errorf("Prerelease can not be empty %q", pre.VersionStr) + } + if !containsOnly(pre.VersionStr, alphanum) { + return fmt.Errorf("Invalid character(s) found in prerelease %q", pre.VersionStr) + } + } + } + + for _, build := range v.Build { + if len(build) == 0 { + return fmt.Errorf("Build meta data can not be empty %q", build) + } + if !containsOnly(build, alphanum) { + return fmt.Errorf("Invalid character(s) found in build meta data %q", build) + } + } + + return nil +} + +// New is an alias for Parse and returns a pointer, parses version string and returns a validated Version or error +func New(s string) (*Version, error) { + v, err := Parse(s) + vp := &v + return vp, err +} + +// Make is an alias for Parse, parses version string and returns a validated Version or error +func Make(s string) (Version, error) { + return Parse(s) +} + +// ParseTolerant allows for certain version specifications that do not strictly adhere to semver +// specs to be parsed by this library. It does so by normalizing versions before passing them to +// Parse(). It currently trims spaces, removes a "v" prefix, adds a 0 patch number to versions +// with only major and minor components specified, and removes leading 0s. +func ParseTolerant(s string) (Version, error) { + s = strings.TrimSpace(s) + s = strings.TrimPrefix(s, "v") + + // Split into major.minor.(patch+pr+meta) + parts := strings.SplitN(s, ".", 3) + // Remove leading zeros. + for i, p := range parts { + if len(p) > 1 { + p = strings.TrimLeft(p, "0") + if len(p) == 0 || !strings.ContainsAny(p[0:1], "0123456789") { + p = "0" + p + } + parts[i] = p + } + } + // Fill up shortened versions. + if len(parts) < 3 { + if strings.ContainsAny(parts[len(parts)-1], "+-") { + return Version{}, errors.New("Short version cannot contain PreRelease/Build meta data") + } + for len(parts) < 3 { + parts = append(parts, "0") + } + } + s = strings.Join(parts, ".") + + return Parse(s) +} + +// Parse parses version string and returns a validated Version or error +func Parse(s string) (Version, error) { + if len(s) == 0 { + return Version{}, errors.New("Version string empty") + } + + // Split into major.minor.(patch+pr+meta) + parts := strings.SplitN(s, ".", 3) + if len(parts) != 3 { + return Version{}, errors.New("No Major.Minor.Patch elements found") + } + + // Major + if !containsOnly(parts[0], numbers) { + return Version{}, fmt.Errorf("Invalid character(s) found in major number %q", parts[0]) + } + if hasLeadingZeroes(parts[0]) { + return Version{}, fmt.Errorf("Major number must not contain leading zeroes %q", parts[0]) + } + major, err := strconv.ParseUint(parts[0], 10, 64) + if err != nil { + return Version{}, err + } + + // Minor + if !containsOnly(parts[1], numbers) { + return Version{}, fmt.Errorf("Invalid character(s) found in minor number %q", parts[1]) + } + if hasLeadingZeroes(parts[1]) { + return Version{}, fmt.Errorf("Minor number must not contain leading zeroes %q", parts[1]) + } + minor, err := strconv.ParseUint(parts[1], 10, 64) + if err != nil { + return Version{}, err + } + + v := Version{} + v.Major = major + v.Minor = minor + + var build, prerelease []string + patchStr := parts[2] + + if buildIndex := strings.IndexRune(patchStr, '+'); buildIndex != -1 { + build = strings.Split(patchStr[buildIndex+1:], ".") + patchStr = patchStr[:buildIndex] + } + + if preIndex := strings.IndexRune(patchStr, '-'); preIndex != -1 { + prerelease = strings.Split(patchStr[preIndex+1:], ".") + patchStr = patchStr[:preIndex] + } + + if !containsOnly(patchStr, numbers) { + return Version{}, fmt.Errorf("Invalid character(s) found in patch number %q", patchStr) + } + if hasLeadingZeroes(patchStr) { + return Version{}, fmt.Errorf("Patch number must not contain leading zeroes %q", patchStr) + } + patch, err := strconv.ParseUint(patchStr, 10, 64) + if err != nil { + return Version{}, err + } + + v.Patch = patch + + // Prerelease + for _, prstr := range prerelease { + parsedPR, err := NewPRVersion(prstr) + if err != nil { + return Version{}, err + } + v.Pre = append(v.Pre, parsedPR) + } + + // Build meta data + for _, str := range build { + if len(str) == 0 { + return Version{}, errors.New("Build meta data is empty") + } + if !containsOnly(str, alphanum) { + return Version{}, fmt.Errorf("Invalid character(s) found in build meta data %q", str) + } + v.Build = append(v.Build, str) + } + + return v, nil +} + +// MustParse is like Parse but panics if the version cannot be parsed. +func MustParse(s string) Version { + v, err := Parse(s) + if err != nil { + panic(`semver: Parse(` + s + `): ` + err.Error()) + } + return v +} + +// PRVersion represents a PreRelease Version +type PRVersion struct { + VersionStr string + VersionNum uint64 + IsNum bool +} + +// NewPRVersion creates a new valid prerelease version +func NewPRVersion(s string) (PRVersion, error) { + if len(s) == 0 { + return PRVersion{}, errors.New("Prerelease is empty") + } + v := PRVersion{} + if containsOnly(s, numbers) { + if hasLeadingZeroes(s) { + return PRVersion{}, fmt.Errorf("Numeric PreRelease version must not contain leading zeroes %q", s) + } + num, err := strconv.ParseUint(s, 10, 64) + + // Might never be hit, but just in case + if err != nil { + return PRVersion{}, err + } + v.VersionNum = num + v.IsNum = true + } else if containsOnly(s, alphanum) { + v.VersionStr = s + v.IsNum = false + } else { + return PRVersion{}, fmt.Errorf("Invalid character(s) found in prerelease %q", s) + } + return v, nil +} + +// IsNumeric checks if prerelease-version is numeric +func (v PRVersion) IsNumeric() bool { + return v.IsNum +} + +// Compare compares two PreRelease Versions v and o: +// -1 == v is less than o +// 0 == v is equal to o +// 1 == v is greater than o +func (v PRVersion) Compare(o PRVersion) int { + if v.IsNum && !o.IsNum { + return -1 + } else if !v.IsNum && o.IsNum { + return 1 + } else if v.IsNum && o.IsNum { + if v.VersionNum == o.VersionNum { + return 0 + } else if v.VersionNum > o.VersionNum { + return 1 + } else { + return -1 + } + } else { // both are Alphas + if v.VersionStr == o.VersionStr { + return 0 + } else if v.VersionStr > o.VersionStr { + return 1 + } else { + return -1 + } + } +} + +// PreRelease version to string +func (v PRVersion) String() string { + if v.IsNum { + return strconv.FormatUint(v.VersionNum, 10) + } + return v.VersionStr +} + +func containsOnly(s string, set string) bool { + return strings.IndexFunc(s, func(r rune) bool { + return !strings.ContainsRune(set, r) + }) == -1 +} + +func hasLeadingZeroes(s string) bool { + return len(s) > 1 && s[0] == '0' +} + +// NewBuildVersion creates a new valid build version +func NewBuildVersion(s string) (string, error) { + if len(s) == 0 { + return "", errors.New("Buildversion is empty") + } + if !containsOnly(s, alphanum) { + return "", fmt.Errorf("Invalid character(s) found in build meta data %q", s) + } + return s, nil +} + +// FinalizeVersion returns the major, minor and patch number only and discards +// prerelease and build number. +func FinalizeVersion(s string) (string, error) { + v, err := Parse(s) + if err != nil { + return "", err + } + v.Pre = nil + v.Build = nil + + finalVer := v.String() + return finalVer, nil +} diff --git a/vendor/github.com/blang/semver/v4/sort.go b/vendor/github.com/blang/semver/v4/sort.go new file mode 100644 index 000000000..e18f88082 --- /dev/null +++ b/vendor/github.com/blang/semver/v4/sort.go @@ -0,0 +1,28 @@ +package semver + +import ( + "sort" +) + +// Versions represents multiple versions. +type Versions []Version + +// Len returns length of version collection +func (s Versions) Len() int { + return len(s) +} + +// Swap swaps two versions inside the collection by its indices +func (s Versions) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} + +// Less checks if version at index i is less than version at index j +func (s Versions) Less(i, j int) bool { + return s[i].LT(s[j]) +} + +// Sort sorts a slice of versions +func Sort(versions []Version) { + sort.Sort(Versions(versions)) +} diff --git a/vendor/github.com/blang/semver/v4/sql.go b/vendor/github.com/blang/semver/v4/sql.go new file mode 100644 index 000000000..db958134f --- /dev/null +++ b/vendor/github.com/blang/semver/v4/sql.go @@ -0,0 +1,30 @@ +package semver + +import ( + "database/sql/driver" + "fmt" +) + +// Scan implements the database/sql.Scanner interface. +func (v *Version) Scan(src interface{}) (err error) { + var str string + switch src := src.(type) { + case string: + str = src + case []byte: + str = string(src) + default: + return fmt.Errorf("version.Scan: cannot convert %T to string", src) + } + + if t, err := Parse(str); err == nil { + *v = t + } + + return +} + +// Value implements the database/sql/driver.Valuer interface. +func (v Version) Value() (driver.Value, error) { + return v.String(), nil +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 9b76238a0..742baa314 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -8,6 +8,9 @@ github.com/PuerkitoBio/urlesc github.com/beorn7/perks/quantile # github.com/blang/semver v3.5.1+incompatible github.com/blang/semver +# github.com/blang/semver/v4 v4.0.0 +## explicit +github.com/blang/semver/v4 # github.com/cespare/xxhash/v2 v2.1.1 github.com/cespare/xxhash/v2 # github.com/coreos/go-semver v0.3.0