From 7cedff7785a4ed210814da8d96f957fb0a1b413c Mon Sep 17 00:00:00 2001 From: Phil Lu Date: Wed, 14 Jul 2021 21:15:28 -0700 Subject: [PATCH 1/6] GH-268: rename ShortID() with ExtrinsicID() to indicate decoded UniqueID is supplied instead of randomly generated --- bloxid/extrinsicids.go | 41 +++++++++++ bloxid/interface.go | 6 +- bloxid/v0.go | 60 ++++++++++------ bloxid/v0_test.go | 159 ++++++++++++++++++++++++++++++++++++----- 4 files changed, 226 insertions(+), 40 deletions(-) create mode 100644 bloxid/extrinsicids.go diff --git a/bloxid/extrinsicids.go b/bloxid/extrinsicids.go new file mode 100644 index 00000000..d996dd9a --- /dev/null +++ b/bloxid/extrinsicids.go @@ -0,0 +1,41 @@ +package bloxid + +import ( + "errors" + "regexp" +) + +const ( + extrinsicIDPrefix = "EXTR" //the prefix needs to be uppercase +) + +var ( + extrinsicIDPrefixBytes = []byte(extrinsicIDPrefix) +) + +var ( + ErrEmptyExtrinsicID = errors.New("empty extrinsic id") + ErrInvalidExtrinsicID = errors.New("invalid extrinsic id") + + extrinsicIDRegex = regexp.MustCompile(`^[0-9A-Za-z_-]+$`) +) + +func validateGetExtrinsicID(id string) error { + if len(id) < 1 { + return ErrEmptyExtrinsicID + } + + if !extrinsicIDRegex.MatchString(id) { + return ErrInvalidExtrinsicID + } + + return nil +} + +func getExtrinsicID(id string) (string, error) { + if err := validateGetExtrinsicID(id); err != nil { + return "", err + } + + return id, nil +} diff --git a/bloxid/interface.go b/bloxid/interface.go index 921dd2fa..2372cf86 100644 --- a/bloxid/interface.go +++ b/bloxid/interface.go @@ -4,8 +4,6 @@ package bloxid type ID interface { // String returns the complete resource ID String() string - // ShortID returns a shortened ID that will be locally unique - ShortID() string // Version returns a serialized representation of the ID version // ie. `V0` Version() string // V0 @@ -16,4 +14,8 @@ type ID interface { // Realm is optional and returns the cloud realm that // the resource is found in ie. `us-com-1`, `eu-com-1`, ... Realm() string + // EncodedID returns the unique id in encoded format + // TODO: EncodedID() string + // DecodedID returns the unique id in decoded format + // TODO: DecodedID() string } diff --git a/bloxid/v0.go b/bloxid/v0.go index fb90d954..56dea7e5 100644 --- a/bloxid/v0.go +++ b/bloxid/v0.go @@ -1,6 +1,7 @@ package bloxid import ( + "bytes" "encoding/base32" "encoding/hex" "errors" @@ -9,6 +10,8 @@ import ( ) const ( + UniqueIDEncodedMinCharSize = 16 + VersionUnknown Version = iota Version0 Version = iota ) @@ -49,7 +52,10 @@ func NewV0(bloxid string) (*V0, error) { return parseV0(bloxid) } -const V0Delimiter = "." +const ( + V0Delimiter = "." + bloxidTypeLen = 4 +) func parseV0(bloxid string) (*V0, error) { if len(bloxid) == 0 { @@ -74,7 +80,17 @@ func parseV0(bloxid string) (*V0, error) { if err != nil { return nil, fmt.Errorf("unable to decode id: %s", err) } - v0.decoded = hex.EncodeToString(decoded) + + switch { + case bytes.HasPrefix(decoded, extrinsicIDPrefixBytes): + v0.decoded = strings.TrimSpace(string(decoded[bloxidTypeLen:])) + default: + if len(v0.encoded) < DefaultUniqueIDEncodedCharSize { + return nil, ErrInvalidUniqueIDLen + } + + v0.decoded = hex.EncodeToString(decoded) + } return v0, nil } @@ -97,10 +113,9 @@ func validateV0(bloxid string) error { return ErrInvalidEntityType } - if len(parts[4]) < DefaultUniqueIDEncodedCharSize { + if len(parts[4]) < UniqueIDEncodedMinCharSize { return ErrInvalidUniqueIDLen } - return nil } @@ -110,12 +125,10 @@ var _ ID = &V0{} type V0 struct { version Version realm string - customSuffix string decoded string encoded string entityDomain string entityType string - shortID string } // Serialize the typed guid as a string @@ -138,14 +151,6 @@ func (v *V0) Realm() string { return v.realm } -// ShortID implements ID.ShortID -func (v *V0) ShortID() string { - if v == nil { - return "" - } - return v.shortID -} - // Domain implements ID.Domain func (v *V0) Domain() string { if v == nil { @@ -175,14 +180,15 @@ type V0Options struct { Realm string EntityDomain string EntityType string - shortid string + extrinsicID string } type GenerateV0Opts func(o *V0Options) -func WithShortID(shortid string) func(o *V0Options) { +// WithExtrinsicID supplies a locally unique ID that is not randomly generated +func WithExtrinsicID(eid string) func(o *V0Options) { return func(o *V0Options) { - o.shortid = shortid + o.extrinsicID = eid } } @@ -205,8 +211,22 @@ func GenerateV0(opts *V0Options, fnOpts ...GenerateV0Opts) (*V0, error) { } func uniqueID(opts *V0Options) (encoded string, decoded string) { - rndm := randDefault() - decoded = hex.EncodeToString(rndm) - encoded = strings.ToLower(base32.StdEncoding.EncodeToString(rndm)) + if len(opts.extrinsicID) > 0 { + var err error + decoded, err = getExtrinsicID(opts.extrinsicID) + if err != nil { + return + } + + const rfc4648NoPaddingChars = 5 + rem := rfc4648NoPaddingChars - ((len(decoded) + len(extrinsicIDPrefix)) % rfc4648NoPaddingChars) + pad := strings.Repeat(" ", rem) + padded := extrinsicIDPrefix + decoded + pad + encoded = strings.ToLower(base32.StdEncoding.EncodeToString([]byte(padded))) + } else { + rndm := randDefault() + decoded = hex.EncodeToString(rndm) + encoded = strings.ToLower(base32.StdEncoding.EncodeToString(rndm)) + } return } diff --git a/bloxid/v0_test.go b/bloxid/v0_test.go index 6b497f3b..b72294a2 100644 --- a/bloxid/v0_test.go +++ b/bloxid/v0_test.go @@ -73,20 +73,93 @@ func TestNewV0(t *testing.T) { } } +type generateTestCase struct { + realm string + entityDomain string + entityType string + extrinsicID string + expected string + err error +} + func TestGenerateV0(t *testing.T) { - var testmap = []struct { - realm string - entityDomain string - entityType string - output string - expectedPrefix string - err error - }{ + var testmap = []generateTestCase{ + { + realm: "us-com-1", + entityDomain: "infra", + entityType: "host", + expected: "blox0.infra.host.us-com-1.", + }, + { + realm: "us-com-2", + entityDomain: "infra", + entityType: "host", + expected: "blox0.infra.host.us-com-2.", + }, + + // ensure `=` is not part of id when encoded + { + realm: "us-com-1", + entityDomain: "infra", + entityType: "host", + extrinsicID: "1", + expected: "blox0.infra.host.us-com-1.ivmfiurreaqcaiba", + }, + { + realm: "us-com-1", + entityDomain: "infra", + entityType: "host", + extrinsicID: "12", + expected: "blox0.infra.host.us-com-1.ivmfiurrgiqcaiba", + }, { - realm: "us-com-1", - entityDomain: "infra", - entityType: "host", - expectedPrefix: "blox0.infra.host.us-com-1.", + realm: "us-com-1", + entityDomain: "infra", + entityType: "host", + extrinsicID: "123", + expected: "blox0.infra.host.us-com-1.ivmfiurrgizsaiba", + }, + { + realm: "us-com-1", + entityDomain: "infra", + entityType: "host", + extrinsicID: "1234", + expected: "blox0.infra.host.us-com-1.ivmfiurrgiztiiba", + }, + { + realm: "us-com-1", + entityDomain: "infra", + entityType: "host", + extrinsicID: "12345", + expected: "blox0.infra.host.us-com-1.ivmfiurrgiztinja", + }, + { + realm: "us-com-1", + entityDomain: "infra", + entityType: "host", + extrinsicID: "123456", + expected: "blox0.infra.host.us-com-1.ivmfiurrgiztinjweaqcaiba", + }, + { + realm: "us-com-1", + entityDomain: "infra", + entityType: "host", + extrinsicID: "1234567", + expected: "blox0.infra.host.us-com-1.ivmfiurrgiztinjwg4qcaiba", + }, + { + realm: "us-com-1", + entityDomain: "infra", + entityType: "host", + extrinsicID: "12345678", + expected: "blox0.infra.host.us-com-1.ivmfiurrgiztinjwg44caiba", + }, + { + realm: "us-com-1", + entityDomain: "infra", + entityType: "host", + extrinsicID: "123456789", + expected: "blox0.infra.host.us-com-1.ivmfiurrgiztinjwg44dsiba", }, } @@ -95,23 +168,73 @@ func TestGenerateV0(t *testing.T) { EntityDomain: tm.entityDomain, EntityType: tm.entityType, Realm: tm.realm, - }) + }, + WithExtrinsicID(tm.extrinsicID), + ) if err != tm.err { - t.Errorf("got: %s wanted: %s", err, tm.err) + t.Logf("test: %#v", tm) + t.Errorf("got: %s wanted error: %s", err, tm.err) } if err != nil { continue } if v0 == nil { - t.Errorf("unexpected nil version") + t.Errorf("unexpected nil id") continue } - if strings.HasPrefix(tm.expectedPrefix, v0.String()) { - t.Errorf("got: %q wanted prefix: %q", v0, tm.expectedPrefix) - } t.Log(v0) t.Logf("%#v\n", v0) + + validateGenerateV0(t, tm, v0, err) + + parsed, err := NewV0(v0.String()) + if err != tm.err { + t.Logf("test: %#v", tm) + t.Errorf("got: %s wanted: %s", err, tm.err) + } + if err != nil { + continue + } + + validateGenerateV0(t, tm, parsed, err) + } +} + +func validateGenerateV0(t *testing.T, tm generateTestCase, v0 *V0, err error) { + if len(tm.extrinsicID) > 0 { + if v0.decoded != tm.extrinsicID { + t.Errorf("got: %q wanted decoded: %q", v0.decoded, tm.extrinsicID) + } + if v0.String() != tm.expected { + t.Errorf("got: %q wanted bloxid: %q", v0, tm.expected) + } + } else { + if strings.HasPrefix(tm.expected, v0.String()) { + t.Errorf("got: %q wanted prefix: %q", v0, tm.expected) + } + if len(v0.decoded) < 1 { + t.Errorf("got: %q wanted non-empty string in decoded", v0.decoded) + } + } + + if -1 != strings.Index(v0.String(), "=") { + t.Errorf("got: %q wanted bloxid without equal char", v0.String()) + } + + if v0.Realm() != tm.realm { + t.Errorf("got: %q wanted realm: %q", v0.Realm(), tm.realm) + } + if v0.Domain() != tm.entityDomain { + t.Errorf("got: %q wanted entity domain: %q", v0.Domain(), tm.entityDomain) + } + if v0.Type() != tm.entityType { + t.Errorf("got: %q wanted entity type: %q", v0.Type(), tm.entityType) + } + if len(tm.extrinsicID) > 0 { + if v0.decoded != tm.extrinsicID { + t.Errorf("got: %q wanted extrinsic id: %q", v0.decoded, tm.extrinsicID) + } } } From 28995ddf55de7f7117a89469fd3d47d402c07343 Mon Sep 17 00:00:00 2001 From: Thangaraj Date: Sat, 17 Jul 2021 08:18:16 +0530 Subject: [PATCH 2/6] Added hashid support to blox resource id --- bloxid/hashids.go | 110 ++++++++++++++ bloxid/hashids_test.go | 328 +++++++++++++++++++++++++++++++++++++++++ bloxid/interface.go | 6 +- bloxid/v0.go | 170 +++++++++++++++++---- bloxid/v0_test.go | 49 +++--- go.mod | 1 + go.sum | 2 + 7 files changed, 613 insertions(+), 53 deletions(-) create mode 100644 bloxid/hashids.go create mode 100644 bloxid/hashids_test.go diff --git a/bloxid/hashids.go b/bloxid/hashids.go new file mode 100644 index 00000000..efb72601 --- /dev/null +++ b/bloxid/hashids.go @@ -0,0 +1,110 @@ +package bloxid + +import ( + "errors" + + hashids "github.com/speps/go-hashids/v2" +) + +const ( + hashIDAllowedChar = "0123456789abcdef" + + //the prefix needs to be uppercase so there are no collisions with chars in hashIDAllowedChar + hashIDPrefix = "HIDZ" + + idSchemeHashID = "hashid" +) + +var ( + ErrInvalidSalt = errors.New("invalid salt") + ErrInvalidID = errors.New("invalid id") + + maxHashIDLen = DefaultUniqueIDDecodedCharSize - len(hashIDPrefix) + + hashIDPrefixBytes = []byte(hashIDPrefix) +) + +func newHashID(salt string) (*hashids.HashID, error) { + hid := hashids.HashIDData{ + Alphabet: hashIDAllowedChar, + MinLength: maxHashIDLen, + Salt: salt, + } + + return hashids.NewWithData(&hid) +} + +func validateGetHashID(id int64, salt string) error { + + if len(salt) < 1 { + return ErrInvalidSalt + } + + if id < 0 { + return ErrInvalidID + } + + return nil +} + +func getHashID(id int64, salt string) (string, error) { + if err := validateGetHashID(id, salt); err != nil { + return "", err + } + + h, err := newHashID(salt) + if err != nil { + return "", err + } + + eID, err := h.EncodeInt64([]int64{id}) + if err != nil { + return "", err + } + + return eID, err +} + +func validateGetint64FromHashID(id, salt string) error { + + if len(salt) < 1 { + return ErrInvalidSalt + } + + if len(id) != maxHashIDLen { + return ErrInvalidID + } + + return nil +} + +func getInt64FromHashID(id, salt string) (int64, error) { + if err := validateGetint64FromHashID(id, salt); err != nil { + return -1, err + } + + h, err := newHashID(salt) + if err != nil { + return -1, err + } + + dID, err := h.DecodeInt64WithError(id) + if err != nil { + return -1, err + } + + return dID[0], err +} + +func WithHashIDInt64(id int64) func(o *V0Options) { + return func(o *V0Options) { + o.hashIDInt64 = id + o.scheme = idSchemeHashID + } +} + +func WithHashIDSalt(salt string) func(o *V0Options) { + return func(o *V0Options) { + o.hashidSalt = salt + } +} diff --git a/bloxid/hashids_test.go b/bloxid/hashids_test.go new file mode 100644 index 00000000..0de52986 --- /dev/null +++ b/bloxid/hashids_test.go @@ -0,0 +1,328 @@ +package bloxid + +import ( + "errors" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestHashIDtoInt(t *testing.T) { + var tests = []struct { + name string + hashID string + int64ID int64 + salt string + err error + }{ + { + name: "Valid input", + hashID: "blox0.infra.host.us-com-1.jbeuiwrsmq3tkmzwmuzwcojsmrqwemrtgy3tqzbvhbsdizjvhe2dkn3cgzrdizlb", + int64ID: 1, + salt: "test", + err: nil, + }, + { + name: "Different salt", + hashID: "blox0.infra.host.us-com-1.jbeuiwrsmq3tkmzwmuzwcojsmrqwemrtgy3tqzbvhbsdizjvhe2dkn3cgzrdizlb", + int64ID: -1, + salt: "testi1", + err: errors.New("mismatch between encode and decode: 2d7536e3a92dab23678d58d4e59457b6b4ea start ed4b2a9764524ed6958d58237ba3eadb7695 re-encoded. result: [4]"), + }, + + { + name: "Invalid prefix HIDA with correct int value", + hashID: "blox0.infra.host.us-com-1.jbeuiqjsmq3tkmzwmuzwcojsmrqwemrtgy3tqzbvhbsdizjvhe2dkn3cgzrdizlb", + int64ID: -1, + salt: "test", + err: nil, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + v0_1, err := NewV0(test.hashID, WithHashIDSalt(test.salt)) + + assert.Equal(t, test.err, err) + assert.Equal(t, test.int64ID, v0_1.HashIDInt64()) + }) + } +} + +func TestHashIDInttoInt(t *testing.T) { + var tests = []struct { + name string + int64ID int64 + entityType string + domainType string + realm string + salt string + err error + }{ + { + name: "Valid input", + int64ID: 1, + entityType: "hostapp", + domainType: "infra", + realm: "us-com-1", + salt: "test", + err: nil, + }, + { + name: "Negative number", + int64ID: -1, + entityType: "hostapp", + domainType: "infra", + realm: "us-com-1", + salt: "test", + err: ErrInvalidID, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + v0, err := NewV0("", + WithEntityDomain(test.domainType), + WithEntityType(test.entityType), + WithRealm(test.realm), + WithHashIDInt64(test.int64ID), + WithHashIDSalt(test.salt)) + + assert.Equal(t, test.err, err) + + if err == nil { + v0_1, err := NewV0(v0.String(), WithHashIDSalt(test.salt)) + + assert.Equal(t, test.err, err) + assert.Equal(t, test.int64ID, v0_1.HashIDInt64()) + assert.Equal(t, v0, v0_1) + } + }) + } +} + +func TestGetHashID(t *testing.T) { + var tests = []struct { + name string + int64ID int64 + salt string + hashID string + err error + }{ + { + name: "Valid input", + int64ID: 1, + salt: "test", + hashID: "2d7536e3a92dab23678d58d4e59457b6b4ea", + err: nil, + }, + { + name: "zero number", + int64ID: 0, + salt: "test", + hashID: "e735d27d4a5e57d3648658a92be26b93b46a", + err: nil, + }, + { + name: "negative number", + int64ID: -1, + salt: "test1", + hashID: "", + err: ErrInvalidID, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + hashID, err := getHashID(test.int64ID, test.salt) + assert.Equal(t, test.err, err) + assert.Equal(t, test.hashID, hashID) + }) + } +} + +func TestGetIntFromHashID(t *testing.T) { + var tests = []struct { + name string + int64ID int64 + salt string + hashID string + err error + }{ + { + name: "Valid input", + int64ID: 1, + salt: "test", + hashID: "2d7536e3a92dab23678d58d4e59457b6b4ea", + err: nil, + }, + { + name: "negative number", + int64ID: -1, + salt: "test1", + hashID: "", + err: ErrInvalidID, + }, + { + name: "empty hash", + int64ID: -1, + salt: "test1", + hashID: "", + err: ErrInvalidID, + }, + { + name: "zero value", + int64ID: 0, + salt: "test", + hashID: "e735d27d4a5e57d3648658a92be26b93b46a", + err: nil, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + int64ID, err := getInt64FromHashID(test.hashID, test.salt) + assert.Equal(t, test.err, err) + assert.Equal(t, test.int64ID, int64ID) + }) + } +} + +func TestGenerateNewV0(t *testing.T) { + var testmap = []struct { + realm string + entityDomain string + entityType string + hashIntID int64 + expected string + err error + }{ + // ensure `=` is not part of id when encoded + { + realm: "us-com-1", + entityDomain: "infra", + entityType: "host", + hashIntID: 1, + expected: "blox0.infra.host.us-com-1.ivmfiurreaqcaiba", + }, + { + realm: "us-com-1", + entityDomain: "infra", + entityType: "host", + hashIntID: 12, + expected: "blox0.infra.host.us-com-1.ivmfiurrgiqcaiba", + }, + { + realm: "us-com-1", + entityDomain: "infra", + entityType: "host", + hashIntID: 123, + expected: "blox0.infra.host.us-com-1.ivmfiurrgizsaiba", + }, + { + realm: "us-com-1", + entityDomain: "infra", + entityType: "host", + hashIntID: 1234, + expected: "blox0.infra.host.us-com-1.ivmfiurrgiztiiba", + }, + { + realm: "us-com-1", + entityDomain: "infra", + entityType: "host", + hashIntID: 12345, + expected: "blox0.infra.host.us-com-1.ivmfiurrgiztinja", + }, + { + realm: "us-com-1", + entityDomain: "infra", + entityType: "host", + hashIntID: 123456, + expected: "blox0.infra.host.us-com-1.ivmfiurrgiztinjweaqcaiba", + }, + { + realm: "us-com-1", + entityDomain: "infra", + entityType: "host", + hashIntID: 1234567, + expected: "blox0.infra.host.us-com-1.ivmfiurrgiztinjwg4qcaiba", + }, + { + realm: "us-com-1", + entityDomain: "infra", + entityType: "host", + hashIntID: 12345678, + expected: "blox0.infra.host.us-com-1.ivmfiurrgiztinjwg44caiba", + }, + { + realm: "us-com-1", + entityDomain: "infra", + entityType: "host", + hashIntID: 123456789, + expected: "blox0.infra.host.us-com-1.ivmfiurrgiztinjwg44dsiba", + }, + } + + for index, tm := range testmap { + index++ + v0, err := NewV0("", + WithEntityDomain(tm.entityDomain), + WithEntityType(tm.entityType), + WithRealm(tm.realm), + WithHashIDInt64(tm.hashIntID), + WithHashIDSalt("test"), + ) + if err != tm.err { + // t.Logf("test: %#v", tm) + t.Errorf("index: %d got: %s wanted error: %s", index, err, tm.err) + } + if err != nil { + continue + } + + if v0 == nil { + t.Errorf("unexpected nil id") + continue + } + + if -1 != strings.Index(v0.String(), "=") { + t.Errorf("got: %q wanted bloxid without equal char", v0.String()) + } + } +} + +/* +func TestHashIDUniqueness(t *testing.T) { + + var salt string = "test" + var maxIntVal int64 = math.MaxInt64 + var i int64 + + checkUniq := make(map[string]struct{}) + for i = 0; i < maxIntVal; i++ { + v0, err := NewV0("", + WithHashIDInt64(i), + WithHashIDSalt(salt), + ) + if err != nil { + t.Errorf("Failed to convert int to hash id. Index: %v\n", i) + continue + } + + if _, ok := checkUniq[v0.EncodedID()]; ok { + fmt.Printf("The value is not unique for index: %d - hashid: %s\n", i, v0.EncodedID()) + } else { + checkUniq[v0.EncodedID()] = struct{}{} + } + + if -1 != strings.Index(v0.EncodedID(), "=") { + fmt.Printf("id has equal sign: index %v hashID %v\n", i, v0.EncodedID()) + } + if i%1000000 == 0 { + fmt.Printf("Completed: %v\n", i) + } + } + fmt.Println(i) +} +*/ diff --git a/bloxid/interface.go b/bloxid/interface.go index 2372cf86..59508ad5 100644 --- a/bloxid/interface.go +++ b/bloxid/interface.go @@ -15,7 +15,9 @@ type ID interface { // the resource is found in ie. `us-com-1`, `eu-com-1`, ... Realm() string // EncodedID returns the unique id in encoded format - // TODO: EncodedID() string + EncodedID() string // DecodedID returns the unique id in decoded format - // TODO: DecodedID() string + DecodedID() string + // Return the id scheme + Scheme() string } diff --git a/bloxid/v0.go b/bloxid/v0.go index 56dea7e5..73259433 100644 --- a/bloxid/v0.go +++ b/bloxid/v0.go @@ -14,6 +14,9 @@ const ( VersionUnknown Version = iota Version0 Version = iota + + idSchemeExtrinsic = "extrinsic" + idSchemeRandom = "random" ) type Version uint8 @@ -46,18 +49,36 @@ var ( ErrV0Parts error = errors.New("invalid number of parts found") ) -// NewV0 parse a string into a typed guid, return an error -// if the string fails validation. -func NewV0(bloxid string) (*V0, error) { - return parseV0(bloxid) -} - const ( V0Delimiter = "." bloxidTypeLen = 4 ) -func parseV0(bloxid string) (*V0, error) { +type EncodeDecodeOpts func(o *V0Options) + +// NewV0 parse a string into a typed guid, return an error +// if the string fails validation. +func NewV0(bloxid string, fnOpts ...EncodeDecodeOpts) (*V0, error) { + opts := generateV0Options(fnOpts...) + + if len(strings.TrimSpace(bloxid)) == 0 { + return generateV0(opts) + } + + return parseV0(bloxid, opts.hashidSalt) +} + +func generateV0Options(fnOpts ...EncodeDecodeOpts) *V0Options { + var opts *V0Options = new(V0Options) + + for _, fn := range fnOpts { + fn(opts) + } + + return opts +} + +func parseV0(bloxid, salt string) (*V0, error) { if len(bloxid) == 0 { return nil, ErrIDEmpty } @@ -82,14 +103,23 @@ func parseV0(bloxid string) (*V0, error) { } switch { + case bytes.HasPrefix(decoded, hashIDPrefixBytes): + v0.decoded = strings.TrimSpace(string(decoded[bloxidTypeLen:])) + v0.hashIDInt64, err = getInt64FromHashID(v0.decoded, salt) + if err != nil { + return nil, err + } + v0.scheme = idSchemeHashID case bytes.HasPrefix(decoded, extrinsicIDPrefixBytes): v0.decoded = strings.TrimSpace(string(decoded[bloxidTypeLen:])) + v0.scheme = idSchemeExtrinsic default: if len(v0.encoded) < DefaultUniqueIDEncodedCharSize { return nil, ErrInvalidUniqueIDLen } v0.decoded = hex.EncodeToString(decoded) + v0.scheme = idSchemeRandom } return v0, nil @@ -129,6 +159,8 @@ type V0 struct { encoded string entityDomain string entityType string + hashIDInt64 int64 + scheme string } // Serialize the typed guid as a string @@ -143,7 +175,7 @@ func (v *V0) String() string { return strings.Join(s, V0Delimiter) } -// Realm implements ID.Realm +// Realm implements ID.realm func (v *V0) Realm() string { if v == nil { return "" @@ -151,7 +183,7 @@ func (v *V0) Realm() string { return v.realm } -// Domain implements ID.Domain +// Domain implements ID.domain func (v *V0) Domain() string { if v == nil { return "" @@ -159,7 +191,31 @@ func (v *V0) Domain() string { return v.entityDomain } -// Type implements ID.Type +// HashIDInt64 implements ID.hashIDInt64 +func (v *V0) HashIDInt64() int64 { + if v == nil || v.scheme != idSchemeHashID { + return -1 + } + return v.hashIDInt64 +} + +// DecodedID implements ID.decoded +func (v *V0) DecodedID() string { + if v == nil { + return "" + } + return v.decoded +} + +// EncodedID implements ID.encoded +func (v *V0) EncodedID() string { + if v == nil { + return "" + } + return v.encoded +} + +// Type implements ID.entityType func (v *V0) Type() string { if v == nil { return "" @@ -175,58 +231,116 @@ func (v *V0) Version() string { return v.version.String() } +// Scheme of the id +func (v *V0) Scheme() string { + if v == nil { + return "" + } + return v.scheme +} + // V0Options required options to create a typed guid type V0Options struct { - Realm string - EntityDomain string - EntityType string + entityDomain string + entityType string + realm string extrinsicID string + hashIDInt64 int64 + hashidSalt string + scheme string } type GenerateV0Opts func(o *V0Options) +func WithEntityDomain(domain string) func(o *V0Options) { + return func(o *V0Options) { + o.entityDomain = domain + } +} + +func WithEntityType(eType string) func(o *V0Options) { + return func(o *V0Options) { + o.entityType = eType + } +} + +func WithRealm(realm string) func(o *V0Options) { + return func(o *V0Options) { + o.realm = realm + } +} + // WithExtrinsicID supplies a locally unique ID that is not randomly generated func WithExtrinsicID(eid string) func(o *V0Options) { return func(o *V0Options) { o.extrinsicID = eid + o.scheme = idSchemeExtrinsic } } -func GenerateV0(opts *V0Options, fnOpts ...GenerateV0Opts) (*V0, error) { - +func generateV0(opts *V0Options, fnOpts ...GenerateV0Opts) (*V0, error) { for _, fn := range fnOpts { fn(opts) } - encoded, decoded := uniqueID(opts) + encoded, decoded, err := uniqueID(opts) + if err != nil { + return nil, err + } return &V0{ version: Version0, - realm: opts.Realm, + realm: opts.realm, decoded: decoded, encoded: encoded, - entityDomain: opts.EntityDomain, - entityType: opts.EntityType, + entityDomain: opts.entityDomain, + entityType: opts.entityType, + hashIDInt64: opts.hashIDInt64, + scheme: opts.scheme, }, nil } -func uniqueID(opts *V0Options) (encoded string, decoded string) { - if len(opts.extrinsicID) > 0 { - var err error +func uniqueID(opts *V0Options) (encoded, decoded string, err error) { + + switch opts.scheme { + case idSchemeHashID: + + if opts.hashIDInt64 < 0 { + err = ErrInvalidID + return + } + + decoded, err = getHashID(opts.hashIDInt64, opts.hashidSalt) + if err != nil { + return + } + + encoded = encodeLowerAlphaNumeric(hashIDPrefix, decoded) + + case idSchemeExtrinsic: + decoded, err = getExtrinsicID(opts.extrinsicID) if err != nil { return } - const rfc4648NoPaddingChars = 5 - rem := rfc4648NoPaddingChars - ((len(decoded) + len(extrinsicIDPrefix)) % rfc4648NoPaddingChars) - pad := strings.Repeat(" ", rem) - padded := extrinsicIDPrefix + decoded + pad - encoded = strings.ToLower(base32.StdEncoding.EncodeToString([]byte(padded))) - } else { + encoded = encodeLowerAlphaNumeric(extrinsicIDPrefix, decoded) + + default: rndm := randDefault() decoded = hex.EncodeToString(rndm) encoded = strings.ToLower(base32.StdEncoding.EncodeToString(rndm)) + opts.scheme = idSchemeRandom } + return } + +func encodeLowerAlphaNumeric(idPrefix, decoded string) string { + + const rfc4648NoPaddingChars = 5 + rem := rfc4648NoPaddingChars - ((len(decoded) + len(idPrefix)) % rfc4648NoPaddingChars) + pad := strings.Repeat(" ", rem) + padded := idPrefix + decoded + pad + return strings.ToLower(base32.StdEncoding.EncodeToString([]byte(padded))) +} diff --git a/bloxid/v0_test.go b/bloxid/v0_test.go index b72294a2..6e2f7ddb 100644 --- a/bloxid/v0_test.go +++ b/bloxid/v0_test.go @@ -14,14 +14,16 @@ func TestNewV0(t *testing.T) { decoded string err error }{ - { - "", - "", - "", - "", - "", - ErrIDEmpty, - }, + /* + { + "", + "", + "", + "", + "", + ErrIDEmpty, + }, + */ { input: "bloxv0....", err: ErrInvalidVersion, @@ -48,27 +50,27 @@ func TestNewV0(t *testing.T) { }, } - for _, tm := range testmap { + for index, tm := range testmap { v0, err := NewV0(tm.input) if err != tm.err { t.Log(tm.input) - t.Errorf("got: %s wanted: %s", err, tm.err) + t.Errorf("index: %d got: %s wanted: %s", index, err, tm.err) } if err != nil { continue } if v0.String() != tm.output { - t.Errorf("got: %s wanted: %s", v0.String(), tm.output) + t.Errorf("index: %d got: %s wanted: %s", index, v0.String(), tm.output) } if v0.entityDomain != tm.entityDomain { - t.Errorf("got: %q wanted: %q", v0.entityDomain, tm.entityDomain) + t.Errorf("index: %d got: %q wanted: %q", index, v0.entityDomain, tm.entityDomain) } if v0.entityType != tm.entityType { - t.Errorf("got: %q wanted: %q", v0.entityType, tm.entityType) + t.Errorf("index: %d got: %q wanted: %q", index, v0.entityType, tm.entityType) } if v0.decoded != tm.decoded { - t.Errorf("got: %q wanted: %q", v0.decoded, tm.decoded) + t.Errorf("index: %d got: %q wanted: %q", index, v0.decoded, tm.decoded) } } } @@ -89,12 +91,14 @@ func TestGenerateV0(t *testing.T) { entityDomain: "infra", entityType: "host", expected: "blox0.infra.host.us-com-1.", + err: ErrEmptyExtrinsicID, }, { realm: "us-com-2", entityDomain: "infra", entityType: "host", expected: "blox0.infra.host.us-com-2.", + err: ErrEmptyExtrinsicID, }, // ensure `=` is not part of id when encoded @@ -163,17 +167,16 @@ func TestGenerateV0(t *testing.T) { }, } - for _, tm := range testmap { - v0, err := GenerateV0(&V0Options{ - EntityDomain: tm.entityDomain, - EntityType: tm.entityType, - Realm: tm.realm, - }, + for index, tm := range testmap { + v0, err := NewV0("", + WithEntityDomain(tm.entityDomain), + WithEntityType(tm.entityType), + WithRealm(tm.realm), WithExtrinsicID(tm.extrinsicID), ) if err != tm.err { t.Logf("test: %#v", tm) - t.Errorf("got: %s wanted error: %s", err, tm.err) + t.Errorf("index: %d got: %s wanted error: %s", index, err, tm.err) } if err != nil { continue @@ -184,8 +187,8 @@ func TestGenerateV0(t *testing.T) { continue } - t.Log(v0) - t.Logf("%#v\n", v0) + // t.Log(v0) + // t.Logf("%#v\n", v0) validateGenerateV0(t, tm, v0, err) diff --git a/go.mod b/go.mod index 95c02136..3138dfe4 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( github.com/jinzhu/inflection v1.0.0 github.com/lib/pq v1.3.1-0.20200116171513-9eb3fc897d6f github.com/sirupsen/logrus v1.8.0 + github.com/speps/go-hashids/v2 v2.0.1 github.com/stretchr/testify v1.5.1 go.opencensus.io v0.22.3 golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2 diff --git a/go.sum b/go.sum index d9531998..df8c59f9 100644 --- a/go.sum +++ b/go.sum @@ -135,6 +135,8 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.8.0 h1:nfhvjKcUMhBMVqbKHJlk5RPrrfYr/NMo3692g0dwfWU= github.com/sirupsen/logrus v1.8.0/go.mod h1:4GuYW9TZmE769R5STWrRakJc4UqQ3+QQ95fyz7ENv1A= +github.com/speps/go-hashids/v2 v2.0.1 h1:ViWOEqWES/pdOSq+C1SLVa8/Tnsd52XC34RY7lt7m4g= +github.com/speps/go-hashids/v2 v2.0.1/go.mod h1:47LKunwvDZki/uRVD6NImtyk712yFzIs3UF3KlHohGw= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= From 5a71208bbdaf8199d3c7b1297ae6e990a39371a9 Mon Sep 17 00:00:00 2001 From: Phil Lu Date: Fri, 16 Jul 2021 20:15:59 -0700 Subject: [PATCH 3/6] bloxid: exported IDScheme* --- bloxid/extrinsicids.go | 12 +++++-- bloxid/hashids.go | 6 ++-- bloxid/v0.go | 71 ++++++++++++++++++------------------------ 3 files changed, 44 insertions(+), 45 deletions(-) diff --git a/bloxid/extrinsicids.go b/bloxid/extrinsicids.go index d996dd9a..d92b2ed2 100644 --- a/bloxid/extrinsicids.go +++ b/bloxid/extrinsicids.go @@ -6,20 +6,28 @@ import ( ) const ( + IDSchemeExtrinsic = "extrinsic" + extrinsicIDPrefix = "EXTR" //the prefix needs to be uppercase ) var ( extrinsicIDPrefixBytes = []byte(extrinsicIDPrefix) -) -var ( ErrEmptyExtrinsicID = errors.New("empty extrinsic id") ErrInvalidExtrinsicID = errors.New("invalid extrinsic id") extrinsicIDRegex = regexp.MustCompile(`^[0-9A-Za-z_-]+$`) ) +// WithExtrinsicID supplies a locally unique ID that is not randomly generated +func WithExtrinsicID(eid string) func(o *V0Options) { + return func(o *V0Options) { + o.extrinsicID = eid + o.scheme = IDSchemeExtrinsic + } +} + func validateGetExtrinsicID(id string) error { if len(id) < 1 { return ErrEmptyExtrinsicID diff --git a/bloxid/hashids.go b/bloxid/hashids.go index efb72601..0c5b5ea5 100644 --- a/bloxid/hashids.go +++ b/bloxid/hashids.go @@ -7,12 +7,12 @@ import ( ) const ( + IDSchemeHashID = "hashid" + hashIDAllowedChar = "0123456789abcdef" //the prefix needs to be uppercase so there are no collisions with chars in hashIDAllowedChar hashIDPrefix = "HIDZ" - - idSchemeHashID = "hashid" ) var ( @@ -99,7 +99,7 @@ func getInt64FromHashID(id, salt string) (int64, error) { func WithHashIDInt64(id int64) func(o *V0Options) { return func(o *V0Options) { o.hashIDInt64 = id - o.scheme = idSchemeHashID + o.scheme = IDSchemeHashID } } diff --git a/bloxid/v0.go b/bloxid/v0.go index 73259433..bebfe95c 100644 --- a/bloxid/v0.go +++ b/bloxid/v0.go @@ -15,8 +15,7 @@ const ( VersionUnknown Version = iota Version0 Version = iota - idSchemeExtrinsic = "extrinsic" - idSchemeRandom = "random" + IDSchemeRandom = "random" ) type Version uint8 @@ -109,17 +108,17 @@ func parseV0(bloxid, salt string) (*V0, error) { if err != nil { return nil, err } - v0.scheme = idSchemeHashID + v0.scheme = IDSchemeHashID case bytes.HasPrefix(decoded, extrinsicIDPrefixBytes): v0.decoded = strings.TrimSpace(string(decoded[bloxidTypeLen:])) - v0.scheme = idSchemeExtrinsic + v0.scheme = IDSchemeExtrinsic default: if len(v0.encoded) < DefaultUniqueIDEncodedCharSize { return nil, ErrInvalidUniqueIDLen } v0.decoded = hex.EncodeToString(decoded) - v0.scheme = idSchemeRandom + v0.scheme = IDSchemeRandom } return v0, nil @@ -154,11 +153,11 @@ var _ ID = &V0{} // V0 represents a typed guid type V0 struct { version Version + entityDomain string + entityType string realm string decoded string encoded string - entityDomain string - entityType string hashIDInt64 int64 scheme string } @@ -175,12 +174,12 @@ func (v *V0) String() string { return strings.Join(s, V0Delimiter) } -// Realm implements ID.realm -func (v *V0) Realm() string { +// Version of the string +func (v *V0) Version() string { if v == nil { - return "" + return VersionUnknown.String() } - return v.realm + return v.version.String() } // Domain implements ID.domain @@ -191,12 +190,20 @@ func (v *V0) Domain() string { return v.entityDomain } -// HashIDInt64 implements ID.hashIDInt64 -func (v *V0) HashIDInt64() int64 { - if v == nil || v.scheme != idSchemeHashID { - return -1 +// Type implements ID.entityType +func (v *V0) Type() string { + if v == nil { + return "" } - return v.hashIDInt64 + return v.entityType +} + +// Realm implements ID.realm +func (v *V0) Realm() string { + if v == nil { + return "" + } + return v.realm } // DecodedID implements ID.decoded @@ -215,20 +222,12 @@ func (v *V0) EncodedID() string { return v.encoded } -// Type implements ID.entityType -func (v *V0) Type() string { - if v == nil { - return "" - } - return v.entityType -} - -// Version of the string -func (v *V0) Version() string { - if v == nil { - return VersionUnknown.String() +// HashIDInt64 implements ID.hashIDInt64 +func (v *V0) HashIDInt64() int64 { + if v == nil || v.scheme != IDSchemeHashID { + return -1 } - return v.version.String() + return v.hashIDInt64 } // Scheme of the id @@ -270,14 +269,6 @@ func WithRealm(realm string) func(o *V0Options) { } } -// WithExtrinsicID supplies a locally unique ID that is not randomly generated -func WithExtrinsicID(eid string) func(o *V0Options) { - return func(o *V0Options) { - o.extrinsicID = eid - o.scheme = idSchemeExtrinsic - } -} - func generateV0(opts *V0Options, fnOpts ...GenerateV0Opts) (*V0, error) { for _, fn := range fnOpts { fn(opts) @@ -303,7 +294,7 @@ func generateV0(opts *V0Options, fnOpts ...GenerateV0Opts) (*V0, error) { func uniqueID(opts *V0Options) (encoded, decoded string, err error) { switch opts.scheme { - case idSchemeHashID: + case IDSchemeHashID: if opts.hashIDInt64 < 0 { err = ErrInvalidID @@ -317,7 +308,7 @@ func uniqueID(opts *V0Options) (encoded, decoded string, err error) { encoded = encodeLowerAlphaNumeric(hashIDPrefix, decoded) - case idSchemeExtrinsic: + case IDSchemeExtrinsic: decoded, err = getExtrinsicID(opts.extrinsicID) if err != nil { @@ -330,7 +321,7 @@ func uniqueID(opts *V0Options) (encoded, decoded string, err error) { rndm := randDefault() decoded = hex.EncodeToString(rndm) encoded = strings.ToLower(base32.StdEncoding.EncodeToString(rndm)) - opts.scheme = idSchemeRandom + opts.scheme = IDSchemeRandom } return From f42570b9874540b4f83030d3a6c453d60023abfe Mon Sep 17 00:00:00 2001 From: Phil Lu Date: Fri, 16 Jul 2021 20:57:44 -0700 Subject: [PATCH 4/6] added bloxid/README.md --- bloxid/README.md | 78 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 bloxid/README.md diff --git a/bloxid/README.md b/bloxid/README.md new file mode 100644 index 00000000..e7e7a5d2 --- /dev/null +++ b/bloxid/README.md @@ -0,0 +1,78 @@ +# BloxID + +This package, bloxid, implements typed guids for identifying resource objects globally in the system. Resources are not specific to services/applications but must contain an entity type. + +Typed guids have the advantage of being globally unique, easily readable, and strongly typed for authorization and logging. The trailing characters provide sufficient entropy to make each resource universally unique. + +bloxid package provides methods for generating and parsing versioned typed guids. + +bloxid supports different schemes for the unique id portion of the bloxid +- `extrinsic` +- `hashid` +- `random` + + +## Scheme - extrinsic + +create bloxid from extrinsic id +```golang +v0, err := NewV0("", + WithEntityDomain("iam"), + WithEntityType("user"), + WithRealm("us-com-1"), + WithExtrinsic("123456"), + ) +// v0.String(): blox0.iam.user.us-com-1.ivmfiurrgiztinjweaqcaiba +// v0.Decoded(): "123456" +``` + +parse bloxid to retrieve extrinsic id +```golang +parsed, err := NewV0("blox0.iam.user.us-com-1.ivmfiurrgiztinjweaqcaiba") +// v0.Decoded(): 1 +``` + + +## Scheme - hashid + +create bloxid from hashid +```golang +v0, err := NewV0("", + WithEntityDomain("infra"), + WithEntityType("host"), + WithRealm("us-com-1"), + WithHashIDInt64(1), + WithHashIDSalt("test"), + ) +// v0.String(): blox0.infra.host.us-com-1.jbeuiwrsmq3tkmzwmuzwcojsmrqwemrtgy3tqzbvhbsdizjvhe2dkn3cgzrdizlb +// v0.HashIDInt64(): 1 +``` + +parse bloxid to retrieve hashid +```golang +parsed, err := NewV0("blox0.infra.host.us-com-1.jbeuiwrsmq3tkmzwmuzwcojsmrqwemrtgy3tqzbvhbsdizjvhe2dkn3cgzrdizlb", + WithHashIDSalt("test") +) +// v0.HashIDInt64(): 1 +``` + + +## Scheme - random + +create bloxid from generated random id +```golang +v0, err := NewV0("", + WithEntityDomain("iam"), + WithEntityType("group"), + WithRealm("us-com-1"), + ) +// v0.String(): blox0.iam.group.us-com-1.tshwyq3mfkgqqcfa76a5hbr2uaayzw3h +// v0.Decoded(): "9c8f6c436c2a8d0808a0ff81d3863aa0018cdb67" +``` + +parse bloxid to retrieve extrinsic id +```golang +parsed, err := NewV0("blox0.iam.group.us-com-1.tshwyq3mfkgqqcfa76a5hbr2uaayzw3h") +// v0.Decoded(): "9c8f6c436c2a8d0808a0ff81d3863aa0018cdb67" +``` + From f744f322c19b29ecf25b89e45900b15b185b4aba Mon Sep 17 00:00:00 2001 From: Anton Shkindzer Date: Mon, 19 Jul 2021 19:20:38 +0300 Subject: [PATCH 5/6] Prepending AtlasDefaultHeaderMatcher to avoid matcher overriding (#261) Co-authored-by: Anton Shkindzer --- server/server.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/server/server.go b/server/server.go index 672a2254..ee4624c7 100644 --- a/server/server.go +++ b/server/server.go @@ -151,11 +151,13 @@ func WithHealthChecksContext(checker health.CheckerContext) Option { func WithGateway(options ...gateway.Option) Option { return func(s *Server) error { s.registrars = append(s.registrars, func(mux *http.ServeMux) error { - _, err := gateway.NewGateway(append(options, + _, err := gateway.NewGateway(append([]gateway.Option{ gateway.WithGatewayOptions( runtime.WithIncomingHeaderMatcher( gateway.AtlasDefaultHeaderMatcher())), - gateway.WithMux(mux))...) + gateway.WithMux(mux)}, + options...)..., + ) return err }) return nil From 19f23e8045b61be17fa9d61d2644875d22d9ecde Mon Sep 17 00:00:00 2001 From: Anton Shkindzer Date: Mon, 19 Jul 2021 19:21:19 +0300 Subject: [PATCH 6/6] Added fast fail option into checksHandler (#265) * Added fast fail option into checksHandler * Added NewChecksHandlerFastFail * Using set\get for flag * Added tests for fast fail flag * Added fix for map order of elements at the test Co-authored-by: Anton Shkindzer --- health/handler.go | 18 +++++++++++++ health/handler_test.go | 61 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+) diff --git a/health/handler.go b/health/handler.go index ef8594b6..0a1504ff 100644 --- a/health/handler.go +++ b/health/handler.go @@ -13,6 +13,9 @@ type checksHandler struct { readinessPath string readinessChecks map[string]Check + + // if true first found error will fail the check stage + failFast bool } // Checker ... @@ -21,6 +24,8 @@ type Checker interface { AddReadiness(name string, check Check) Handler() http.Handler RegisterHandler(mux *http.ServeMux) + SetFailFast(failFast bool) + GetFailFast() bool } // NewChecksHandler accepts two strings: health and ready paths. @@ -42,6 +47,15 @@ func NewChecksHandler(healthPath, readyPath string) Checker { return ch } +// SetFailFast sets failFast flag for failing on the first error found +func (ch *checksHandler) SetFailFast(failFast bool) { + ch.failFast = failFast +} + +func (ch *checksHandler) GetFailFast() bool { + return ch.failFast +} + func (ch *checksHandler) AddLiveness(name string, check Check) { ch.lock.Lock() defer ch.lock.Unlock() @@ -100,6 +114,10 @@ func (ch *checksHandler) handle(rw http.ResponseWriter, r *http.Request, checksS if err := check(); err != nil { status = http.StatusServiceUnavailable errors[name] = err + if ch.failFast { + rw.WriteHeader(status) + return + } } } } diff --git a/health/handler_test.go b/health/handler_test.go index 0b747f69..dbbff7a6 100644 --- a/health/handler_test.go +++ b/health/handler_test.go @@ -4,6 +4,7 @@ import ( "errors" "net/http" "net/http/httptest" + "strconv" "testing" "github.com/stretchr/testify/assert" @@ -105,3 +106,63 @@ func TestNewHandler(t *testing.T) { }) } } + +func addNiceLiveness(h Checker, number int, counterCall *int) { + h.AddLiveness("Liveness"+strconv.Itoa(number), func() error { + *counterCall++ + return nil + }) +} + +func addFailedLiveness(h Checker, number int, counterCall *int) { + h.AddLiveness("Liveness"+strconv.Itoa(number), func() error { + *counterCall++ + return errors.New("Liveness" + strconv.Itoa(number) + " check failed") + }) +} + +func TestNoFailFastHandler(t *testing.T) { + h := NewChecksHandler("/healthz", "/ready") + + counterCall := 0 + expectedCalls := 3 + + addNiceLiveness(h, 1, &counterCall) + addFailedLiveness(h, 2, &counterCall) + addFailedLiveness(h, 3, &counterCall) + + req, err := http.NewRequest(http.MethodGet, "/healthz", nil) + assert.NoError(t, err) + + httpRecorder := httptest.NewRecorder() + h.Handler().ServeHTTP(httpRecorder, req) + + assert.Equal(t, expectedCalls, counterCall, "Excepted %d calls of check.", expectedCalls) + assert.Equal(t, http.StatusServiceUnavailable, httpRecorder.Code, + "Result codes don't match, current is '%s'.", http.StatusText(httpRecorder.Code)) +} + +func TestFailFastHandler(t *testing.T) { + h := NewChecksHandler("/healthz", "/ready") + h.SetFailFast(true) + + counterCall := 0 + expectedCalls := 2 + + addNiceLiveness(h, 1, &counterCall) + addFailedLiveness(h, 2, &counterCall) + addFailedLiveness(h, 3, &counterCall) + + req, err := http.NewRequest(http.MethodGet, "/healthz", nil) + assert.NoError(t, err) + + httpRecorder := httptest.NewRecorder() + h.Handler().ServeHTTP(httpRecorder, req) + + // we cannot determine the order of elements while iterationg over `checks` map in `handle` function + // so we just check that amount is between the range + assert.GreaterOrEqual(t, expectedCalls, counterCall, "Excepted less or equal %d calls of check.", expectedCalls) + assert.NotEqual(t, 0, counterCall, "Cannot be zero calls of check.") + assert.Equal(t, http.StatusServiceUnavailable, httpRecorder.Code, + "Result codes don't match, current is '%s'.", http.StatusText(httpRecorder.Code)) +}