mirror of
https://github.com/prometheus/alertmanager
synced 2025-02-27 16:20:24 +00:00
The current Alertmanager API v1 is undocumented and written by hand. This patch introduces a new Alertmanager API - v2. The API is fully generated via an OpenAPI 2.0 [1] specification (see `api/v2/openapi.yaml`) with the exception of the http handlers itself. Pros: - Generated server code - Ability to generate clients in all major languages (Go, Java, JS, Python, Ruby, Haskell, *elm* [3] ...) - Strict contract (OpenAPI spec) between server and clients. - Instant feedback on frontend-breaking changes, due to strictly typed frontend language elm. - Generated documentation (See Alertmanager online Swagger UI [4]) Cons: - Dependency on open api ecosystem including go-swagger [2] In addition this patch includes the following changes. - README.md: Add API section - test: Duplicate acceptance test to API v1 & API v2 version The Alertmanager acceptance test framework has a decent test coverage on the Alertmanager API. Introducing the Alertmanager API v2 does not go hand in hand with deprecating API v1. They should live alongside each other for a couple of minor Alertmanager versions. Instead of porting the acceptance test framework to use the new API v2, this patch duplicates the acceptance tests, one using the API v1, the other API v2. Once API v1 is removed we can simply remove `test/with_api_v1` and bring `test/with_api_v2` to `test/`. [1] https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md [2] https://github.com/go-swagger/go-swagger/ [3] https://github.com/ahultgren/swagger-elm [4] http://petstore.swagger.io/?url=https://raw.githubusercontent.com/mxinden/alertmanager/apiv2/api/v2/openapi.yaml Signed-off-by: Max Leonard Inden <IndenML@gmail.com>
485 lines
13 KiB
Go
485 lines
13 KiB
Go
// Copyright 2015 go-swagger maintainers
|
|
//
|
|
// 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 validate
|
|
|
|
import (
|
|
"fmt"
|
|
"reflect"
|
|
"strings"
|
|
|
|
"github.com/go-openapi/errors"
|
|
"github.com/go-openapi/spec"
|
|
)
|
|
|
|
// Result represents a validation result set, composed of
|
|
// errors and warnings.
|
|
//
|
|
// It is used to keep track of all detected errors and warnings during
|
|
// the validation of a specification.
|
|
//
|
|
// Matchcount is used to determine
|
|
// which errors are relevant in the case of AnyOf, OneOf
|
|
// schema validation. Results from the validation branch
|
|
// with most matches get eventually selected.
|
|
//
|
|
// TODO: keep path of key originating the error
|
|
type Result struct {
|
|
Errors []error
|
|
Warnings []error
|
|
MatchCount int
|
|
|
|
// the object data
|
|
data interface{}
|
|
|
|
// Schemata for the root object
|
|
rootObjectSchemata schemata
|
|
// Schemata for object fields
|
|
fieldSchemata []fieldSchemata
|
|
// Schemata for slice items
|
|
itemSchemata []itemSchemata
|
|
|
|
cachedFieldSchemta map[FieldKey][]*spec.Schema
|
|
cachedItemSchemata map[ItemKey][]*spec.Schema
|
|
}
|
|
|
|
// FieldKey is a pair of an object and a field, usable as a key for a map.
|
|
type FieldKey struct {
|
|
object reflect.Value // actually a map[string]interface{}, but the latter cannot be a key
|
|
field string
|
|
}
|
|
|
|
// ItemKey is a pair of a slice and an index, usable as a key for a map.
|
|
type ItemKey struct {
|
|
slice reflect.Value // actually a []interface{}, but the latter cannot be a key
|
|
index int
|
|
}
|
|
|
|
// NewFieldKey returns a pair of an object and field usable as a key of a map.
|
|
func NewFieldKey(obj map[string]interface{}, field string) FieldKey {
|
|
return FieldKey{object: reflect.ValueOf(obj), field: field}
|
|
}
|
|
|
|
// Object returns the underlying object of this key.
|
|
func (fk *FieldKey) Object() map[string]interface{} {
|
|
return fk.object.Interface().(map[string]interface{})
|
|
}
|
|
|
|
// Field returns the underlying field of this key.
|
|
func (fk *FieldKey) Field() string {
|
|
return fk.field
|
|
}
|
|
|
|
// NewItemKey returns a pair of a slice and index usable as a key of a map.
|
|
func NewItemKey(slice interface{}, i int) ItemKey {
|
|
return ItemKey{slice: reflect.ValueOf(slice), index: i}
|
|
}
|
|
|
|
// Slice returns the underlying slice of this key.
|
|
func (ik *ItemKey) Slice() []interface{} {
|
|
return ik.slice.Interface().([]interface{})
|
|
}
|
|
|
|
// Index returns the underlying index of this key.
|
|
func (ik *ItemKey) Index() int {
|
|
return ik.index
|
|
}
|
|
|
|
type fieldSchemata struct {
|
|
obj map[string]interface{}
|
|
field string
|
|
schemata schemata
|
|
}
|
|
|
|
type itemSchemata struct {
|
|
slice reflect.Value
|
|
index int
|
|
schemata schemata
|
|
}
|
|
|
|
// Merge merges this result with the other one(s), preserving match counts etc.
|
|
func (r *Result) Merge(others ...*Result) *Result {
|
|
for _, other := range others {
|
|
if other == nil {
|
|
continue
|
|
}
|
|
r.mergeWithoutRootSchemata(other)
|
|
r.rootObjectSchemata.Append(other.rootObjectSchemata)
|
|
}
|
|
return r
|
|
}
|
|
|
|
// Data returns the original data object used for validation. Mutating this renders
|
|
// the result invalid.
|
|
func (r *Result) Data() interface{} {
|
|
return r.data
|
|
}
|
|
|
|
// RootObjectSchemata returns the schemata which apply to the root object.
|
|
func (r *Result) RootObjectSchemata() []*spec.Schema {
|
|
return r.rootObjectSchemata.Slice()
|
|
}
|
|
|
|
// FieldSchemata returns the schemata which apply to fields in objects.
|
|
func (r *Result) FieldSchemata() map[FieldKey][]*spec.Schema {
|
|
if r.cachedFieldSchemta != nil {
|
|
return r.cachedFieldSchemta
|
|
}
|
|
|
|
ret := make(map[FieldKey][]*spec.Schema, len(r.fieldSchemata))
|
|
for _, fs := range r.fieldSchemata {
|
|
key := NewFieldKey(fs.obj, fs.field)
|
|
if fs.schemata.one != nil {
|
|
ret[key] = append(ret[key], fs.schemata.one)
|
|
} else if len(fs.schemata.multiple) > 0 {
|
|
ret[key] = append(ret[key], fs.schemata.multiple...)
|
|
}
|
|
}
|
|
r.cachedFieldSchemta = ret
|
|
return ret
|
|
}
|
|
|
|
// ItemSchemata returns the schemata which apply to items in slices.
|
|
func (r *Result) ItemSchemata() map[ItemKey][]*spec.Schema {
|
|
if r.cachedItemSchemata != nil {
|
|
return r.cachedItemSchemata
|
|
}
|
|
|
|
ret := make(map[ItemKey][]*spec.Schema, len(r.itemSchemata))
|
|
for _, ss := range r.itemSchemata {
|
|
key := NewItemKey(ss.slice, ss.index)
|
|
if ss.schemata.one != nil {
|
|
ret[key] = append(ret[key], ss.schemata.one)
|
|
} else if len(ss.schemata.multiple) > 0 {
|
|
ret[key] = append(ret[key], ss.schemata.multiple...)
|
|
}
|
|
}
|
|
r.cachedItemSchemata = ret
|
|
return ret
|
|
}
|
|
|
|
func (r *Result) resetCaches() {
|
|
r.cachedFieldSchemta = nil
|
|
r.cachedItemSchemata = nil
|
|
}
|
|
|
|
// mergeForField merges other into r, assigning other's root schemata to the given Object and field name.
|
|
func (r *Result) mergeForField(obj map[string]interface{}, field string, other *Result) *Result {
|
|
if other == nil {
|
|
return r
|
|
}
|
|
r.mergeWithoutRootSchemata(other)
|
|
|
|
if other.rootObjectSchemata.Len() > 0 {
|
|
if r.fieldSchemata == nil {
|
|
r.fieldSchemata = make([]fieldSchemata, len(obj))
|
|
}
|
|
r.fieldSchemata = append(r.fieldSchemata, fieldSchemata{
|
|
obj: obj,
|
|
field: field,
|
|
schemata: other.rootObjectSchemata,
|
|
})
|
|
}
|
|
|
|
return r
|
|
}
|
|
|
|
// mergeForSlice merges other into r, assigning other's root schemata to the given slice and index.
|
|
func (r *Result) mergeForSlice(slice reflect.Value, i int, other *Result) *Result {
|
|
if other == nil {
|
|
return r
|
|
}
|
|
r.mergeWithoutRootSchemata(other)
|
|
|
|
if other.rootObjectSchemata.Len() > 0 {
|
|
if r.itemSchemata == nil {
|
|
r.itemSchemata = make([]itemSchemata, slice.Len())
|
|
}
|
|
r.itemSchemata = append(r.itemSchemata, itemSchemata{
|
|
slice: slice,
|
|
index: i,
|
|
schemata: other.rootObjectSchemata,
|
|
})
|
|
}
|
|
|
|
return r
|
|
}
|
|
|
|
// addRootObjectSchemata adds the given schemata for the root object of the result.
|
|
// The slice schemata might be reused. I.e. do not modify it after being added to a result.
|
|
func (r *Result) addRootObjectSchemata(s *spec.Schema) {
|
|
r.rootObjectSchemata.Append(schemata{one: s})
|
|
}
|
|
|
|
// addPropertySchemata adds the given schemata for the object and field.
|
|
// The slice schemata might be reused. I.e. do not modify it after being added to a result.
|
|
func (r *Result) addPropertySchemata(obj map[string]interface{}, fld string, schema *spec.Schema) {
|
|
if r.fieldSchemata == nil {
|
|
r.fieldSchemata = make([]fieldSchemata, 0, len(obj))
|
|
}
|
|
r.fieldSchemata = append(r.fieldSchemata, fieldSchemata{obj: obj, field: fld, schemata: schemata{one: schema}})
|
|
}
|
|
|
|
// addSliceSchemata adds the given schemata for the slice and index.
|
|
// The slice schemata might be reused. I.e. do not modify it after being added to a result.
|
|
func (r *Result) addSliceSchemata(slice reflect.Value, i int, schema *spec.Schema) {
|
|
if r.itemSchemata == nil {
|
|
r.itemSchemata = make([]itemSchemata, 0, slice.Len())
|
|
}
|
|
r.itemSchemata = append(r.itemSchemata, itemSchemata{slice: slice, index: i, schemata: schemata{one: schema}})
|
|
}
|
|
|
|
// mergeWithoutRootSchemata merges other into r, ignoring the rootObject schemata.
|
|
func (r *Result) mergeWithoutRootSchemata(other *Result) {
|
|
r.resetCaches()
|
|
r.AddErrors(other.Errors...)
|
|
r.AddWarnings(other.Warnings...)
|
|
r.MatchCount += other.MatchCount
|
|
|
|
if other.fieldSchemata != nil {
|
|
if r.fieldSchemata == nil {
|
|
r.fieldSchemata = other.fieldSchemata
|
|
} else {
|
|
for _, x := range other.fieldSchemata {
|
|
r.fieldSchemata = append(r.fieldSchemata, x)
|
|
}
|
|
}
|
|
}
|
|
|
|
if other.itemSchemata != nil {
|
|
if r.itemSchemata == nil {
|
|
r.itemSchemata = other.itemSchemata
|
|
} else {
|
|
for _, x := range other.itemSchemata {
|
|
r.itemSchemata = append(r.itemSchemata, x)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MergeAsErrors merges this result with the other one(s), preserving match counts etc.
|
|
//
|
|
// Warnings from input are merged as Errors in the returned merged Result.
|
|
func (r *Result) MergeAsErrors(others ...*Result) *Result {
|
|
for _, other := range others {
|
|
if other != nil {
|
|
r.resetCaches()
|
|
r.AddErrors(other.Errors...)
|
|
r.AddErrors(other.Warnings...)
|
|
r.MatchCount += other.MatchCount
|
|
}
|
|
}
|
|
return r
|
|
}
|
|
|
|
// MergeAsWarnings merges this result with the other one(s), preserving match counts etc.
|
|
//
|
|
// Errors from input are merged as Warnings in the returned merged Result.
|
|
func (r *Result) MergeAsWarnings(others ...*Result) *Result {
|
|
for _, other := range others {
|
|
if other != nil {
|
|
r.resetCaches()
|
|
r.AddWarnings(other.Errors...)
|
|
r.AddWarnings(other.Warnings...)
|
|
r.MatchCount += other.MatchCount
|
|
}
|
|
}
|
|
return r
|
|
}
|
|
|
|
// AddErrors adds errors to this validation result (if not already reported).
|
|
//
|
|
// Since the same check may be passed several times while exploring the
|
|
// spec structure (via $ref, ...) reported messages are kept
|
|
// unique.
|
|
func (r *Result) AddErrors(errors ...error) {
|
|
for _, e := range errors {
|
|
found := false
|
|
if e != nil {
|
|
for _, isReported := range r.Errors {
|
|
if e.Error() == isReported.Error() {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
r.Errors = append(r.Errors, e)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// AddWarnings adds warnings to this validation result (if not already reported).
|
|
func (r *Result) AddWarnings(warnings ...error) {
|
|
for _, e := range warnings {
|
|
found := false
|
|
if e != nil {
|
|
for _, isReported := range r.Warnings {
|
|
if e.Error() == isReported.Error() {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
r.Warnings = append(r.Warnings, e)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (r *Result) keepRelevantErrors() *Result {
|
|
// TODO: this one is going to disapear...
|
|
// keepRelevantErrors strips a result from standard errors and keeps
|
|
// the ones which are supposedly more accurate.
|
|
//
|
|
// The original result remains unaffected (creates a new instance of Result).
|
|
// This method is used to work around the "matchCount" filter which would otherwise
|
|
// strip our result from some accurate error reporting from lower level validators.
|
|
//
|
|
// NOTE: this implementation with a placeholder (IMPORTANT!) is neither clean nor
|
|
// very efficient. On the other hand, relying on go-openapi/errors to manipulate
|
|
// codes would require to change a lot here. So, for the moment, let's go with
|
|
// placeholders.
|
|
strippedErrors := []error{}
|
|
for _, e := range r.Errors {
|
|
if strings.HasPrefix(e.Error(), "IMPORTANT!") {
|
|
strippedErrors = append(strippedErrors, fmt.Errorf(strings.TrimPrefix(e.Error(), "IMPORTANT!")))
|
|
}
|
|
}
|
|
strippedWarnings := []error{}
|
|
for _, e := range r.Warnings {
|
|
if strings.HasPrefix(e.Error(), "IMPORTANT!") {
|
|
strippedWarnings = append(strippedWarnings, fmt.Errorf(strings.TrimPrefix(e.Error(), "IMPORTANT!")))
|
|
}
|
|
}
|
|
strippedResult := new(Result)
|
|
strippedResult.Errors = strippedErrors
|
|
strippedResult.Warnings = strippedWarnings
|
|
return strippedResult
|
|
}
|
|
|
|
// IsValid returns true when this result is valid.
|
|
//
|
|
// Returns true on a nil *Result.
|
|
func (r *Result) IsValid() bool {
|
|
if r == nil {
|
|
return true
|
|
}
|
|
return len(r.Errors) == 0
|
|
}
|
|
|
|
// HasErrors returns true when this result is invalid.
|
|
//
|
|
// Returns false on a nil *Result.
|
|
func (r *Result) HasErrors() bool {
|
|
if r == nil {
|
|
return false
|
|
}
|
|
return !r.IsValid()
|
|
}
|
|
|
|
// HasWarnings returns true when this result contains warnings.
|
|
//
|
|
// Returns false on a nil *Result.
|
|
func (r *Result) HasWarnings() bool {
|
|
if r == nil {
|
|
return false
|
|
}
|
|
return len(r.Warnings) > 0
|
|
}
|
|
|
|
// HasErrorsOrWarnings returns true when this result contains
|
|
// either errors or warnings.
|
|
//
|
|
// Returns false on a nil *Result.
|
|
func (r *Result) HasErrorsOrWarnings() bool {
|
|
if r == nil {
|
|
return false
|
|
}
|
|
return len(r.Errors) > 0 || len(r.Warnings) > 0
|
|
}
|
|
|
|
// Inc increments the match count
|
|
func (r *Result) Inc() {
|
|
r.MatchCount++
|
|
}
|
|
|
|
// AsError renders this result as an error interface
|
|
//
|
|
// TODO: reporting / pretty print with path ordered and indented
|
|
func (r *Result) AsError() error {
|
|
if r.IsValid() {
|
|
return nil
|
|
}
|
|
return errors.CompositeValidationError(r.Errors...)
|
|
}
|
|
|
|
// schemata is an arbitrary number of schemata. It does a distinction between zero,
|
|
// one and many schemata to avoid slice allocations.
|
|
type schemata struct {
|
|
// one is set if there is exactly one schema. In that case multiple must be nil.
|
|
one *spec.Schema
|
|
// multiple is an arbitrary number of schemas. If it is set, one must be nil.
|
|
multiple []*spec.Schema
|
|
}
|
|
|
|
func (s *schemata) Len() int {
|
|
if s.one != nil {
|
|
return 1
|
|
}
|
|
return len(s.multiple)
|
|
}
|
|
|
|
func (s *schemata) Slice() []*spec.Schema {
|
|
if s == nil {
|
|
return nil
|
|
}
|
|
if s.one != nil {
|
|
return []*spec.Schema{s.one}
|
|
}
|
|
return s.multiple
|
|
}
|
|
|
|
// appendSchemata appends the schemata in other to s. It mutated s in-place.
|
|
func (s *schemata) Append(other schemata) {
|
|
if other.one == nil && len(other.multiple) == 0 {
|
|
return
|
|
}
|
|
if s.one == nil && len(s.multiple) == 0 {
|
|
*s = other
|
|
return
|
|
}
|
|
|
|
if s.one != nil {
|
|
if other.one != nil {
|
|
s.multiple = []*spec.Schema{s.one, other.one}
|
|
} else {
|
|
t := make([]*spec.Schema, 0, 1+len(other.multiple))
|
|
s.multiple = append(append(t, s.one), other.multiple...)
|
|
}
|
|
s.one = nil
|
|
} else {
|
|
if other.one != nil {
|
|
s.multiple = append(s.multiple, other.one)
|
|
} else {
|
|
if cap(s.multiple) >= len(s.multiple)+len(other.multiple) {
|
|
s.multiple = append(s.multiple, other.multiple...)
|
|
} else {
|
|
t := make([]*spec.Schema, 0, len(s.multiple)+len(other.multiple))
|
|
s.multiple = append(append(t, s.multiple...), other.multiple...)
|
|
}
|
|
}
|
|
}
|
|
}
|