Merge pull request #103 from rusenask/develop

Develop
pull/114/head
Karolis Rusenas 2017-09-28 19:29:50 +01:00 committed by GitHub
commit 8b07db1f47
275 changed files with 13518 additions and 3331 deletions

369
approvals/approvals.go Normal file
View File

@ -0,0 +1,369 @@
package approvals
import (
"context"
"errors"
"sync"
"sync/atomic"
"time"
"github.com/rusenask/keel/cache"
"github.com/rusenask/keel/types"
"github.com/rusenask/keel/util/codecs"
log "github.com/Sirupsen/logrus"
)
// Manager is used to manage updates
type Manager interface {
// Subscribe for approval request events, subscriber should provide
// its name. Indented to be used by extensions that collect
// approvals
Subscribe(ctx context.Context) (<-chan *types.Approval, error)
// SubscribeApproved - is used to get approved events by the manager
SubscribeApproved(ctx context.Context) (<-chan *types.Approval, error)
// request approval for deployment/release/etc..
Create(r *types.Approval) error
// Update whole approval object
Update(r *types.Approval) error
// Increases Approval votes by 1
Approve(identifier, voter string) (*types.Approval, error)
// Rejects Approval
Reject(identifier string) (*types.Approval, error)
Get(identifier string) (*types.Approval, error)
List() ([]*types.Approval, error)
Delete(identifier string) error
StartExpiryService(ctx context.Context) error
}
// Approvals related errors
var (
ErrApprovalAlreadyExists = errors.New("approval already exists")
)
// Approvals cache prefix
const (
ApprovalsPrefix = "approvals"
)
// DefaultManager - default manager implementation
type DefaultManager struct {
// cache is used to store approvals, key example:
// approvals/<provider name>/<identifier>
cache cache.Cache
serializer codecs.Serializer
// subscriber channels
channels map[uint64]chan *types.Approval
index uint64
// approved channels
approvedCh map[uint64]chan *types.Approval
mu *sync.Mutex
subMu *sync.RWMutex
}
// New create new instance of default manager
func New(cache cache.Cache, serializer codecs.Serializer) *DefaultManager {
man := &DefaultManager{
cache: cache,
serializer: serializer,
channels: make(map[uint64]chan *types.Approval),
approvedCh: make(map[uint64]chan *types.Approval),
index: 0,
mu: &sync.Mutex{},
subMu: &sync.RWMutex{},
}
return man
}
// StartExpiryService - starts approval expiry service which deletes approvals
// that already reached their deadline
func (m *DefaultManager) StartExpiryService(ctx context.Context) error {
ticker := time.NewTicker(60 * time.Minute)
err := m.expireEntries()
if err != nil {
log.WithFields(log.Fields{
"error": err,
}).Error("approvals.StartExpiryService: got error while performing initial expired approvals check")
}
for {
select {
case <-ctx.Done():
return nil
case <-ticker.C:
err := m.expireEntries()
if err != nil {
log.WithFields(log.Fields{
"error": err,
}).Error("approvals.StartExpiryService: got error while performing routinely expired approvals check")
}
}
}
}
func (m *DefaultManager) expireEntries() error {
approvals, err := m.cache.List(ApprovalsPrefix + "/")
if err != nil {
return err
}
for k, v := range approvals {
var approval types.Approval
err = m.serializer.Decode(v, &approval)
if err != nil {
log.WithFields(log.Fields{
"error": err,
"identifier": k,
}).Error("approvals.expireEntries: failed to decode approval into value")
continue
}
if approval.Expired() {
err = m.Delete(approval.Identifier)
if err != nil {
log.WithFields(log.Fields{
"error": err,
"identifier": k,
}).Error("approvals.expireEntries: failed to delete expired approval")
}
}
}
return nil
}
// Subscribe - subscribe for approval events
func (m *DefaultManager) Subscribe(ctx context.Context) (<-chan *types.Approval, error) {
m.subMu.Lock()
index := atomic.AddUint64(&m.index, 1)
approvalsCh := make(chan *types.Approval, 10)
m.channels[index] = approvalsCh
m.subMu.Unlock()
go func() {
for {
select {
case <-ctx.Done():
m.subMu.Lock()
delete(m.channels, index)
m.subMu.Unlock()
return
}
}
}()
return approvalsCh, nil
}
// SubscribeApproved - subscribe for approved update requests
func (m *DefaultManager) SubscribeApproved(ctx context.Context) (<-chan *types.Approval, error) {
m.subMu.Lock()
index := atomic.AddUint64(&m.index, 1)
approvedCh := make(chan *types.Approval, 10)
m.approvedCh[index] = approvedCh
m.subMu.Unlock()
go func() {
for {
select {
case <-ctx.Done():
m.subMu.Lock()
delete(m.approvedCh, index)
m.subMu.Unlock()
return
}
}
}()
return approvedCh, nil
}
func (m *DefaultManager) publishRequest(approval *types.Approval) error {
m.subMu.RLock()
defer m.subMu.RUnlock()
for _, subscriber := range m.channels {
subscriber <- approval
}
return nil
}
func (m *DefaultManager) publishApproved(approval *types.Approval) error {
m.subMu.RLock()
defer m.subMu.RUnlock()
for _, subscriber := range m.approvedCh {
subscriber <- approval
}
return nil
}
// Create - creates new approval request and publishes to all subscribers
func (m *DefaultManager) Create(r *types.Approval) error {
_, err := m.Get(r.Identifier)
if err == nil {
return ErrApprovalAlreadyExists
}
r.CreatedAt = time.Now()
r.UpdatedAt = time.Now()
bts, err := m.serializer.Encode(r)
if err != nil {
return err
}
err = m.cache.Put(getKey(r.Identifier), bts)
if err != nil {
return err
}
return m.publishRequest(r)
}
// Update - update approval
func (m *DefaultManager) Update(r *types.Approval) error {
existing, err := m.Get(r.Identifier)
if err != nil {
return err
}
r.CreatedAt = existing.CreatedAt
r.UpdatedAt = time.Now()
bts, err := m.serializer.Encode(r)
if err != nil {
return err
}
if r.Status() == types.ApprovalStatusApproved {
err = m.publishApproved(r)
if err != nil {
log.WithFields(log.Fields{
"error": err,
"approval": r.Identifier,
"provider": r.Provider,
}).Error("approvals.manager: failed to re-submit event after approvals were collected")
}
}
return m.cache.Put(getKey(r.Identifier), bts)
}
// Approve - increase VotesReceived by 1 and returns updated version
func (m *DefaultManager) Approve(identifier, voter string) (*types.Approval, error) {
m.mu.Lock()
defer m.mu.Unlock()
existing, err := m.Get(identifier)
if err != nil {
log.WithFields(log.Fields{
"identifier": identifier,
"error": err,
}).Error("approvals.manager: failed to get")
return nil, err
}
for _, v := range existing.Voters {
if v == voter {
// nothing to do, same voter
return existing, nil
}
}
existing.Voters = append(existing.Voters, voter)
existing.VotesReceived++
err = m.Update(existing)
if err != nil {
log.WithFields(log.Fields{
"identifier": identifier,
"error": err,
}).Error("approvals.manager: failed to update")
return nil, err
}
log.WithFields(log.Fields{
"identifier": identifier,
}).Info("approvals.manager: approved")
return existing, nil
}
// Reject - rejects approval (marks rejected=true), approval will not be valid even if it
// collects required votes
func (m *DefaultManager) Reject(identifier string) (*types.Approval, error) {
m.mu.Lock()
defer m.mu.Unlock()
existing, err := m.Get(identifier)
if err != nil {
return nil, err
}
existing.Rejected = true
err = m.Update(existing)
if err != nil {
return nil, err
}
return existing, nil
}
// Get - get specified approval
func (m *DefaultManager) Get(identifier string) (*types.Approval, error) {
bts, err := m.cache.Get(getKey(identifier))
if err != nil {
return nil, err
}
var approval types.Approval
err = m.serializer.Decode(bts, &approval)
return &approval, err
}
// List - list approvals
func (m *DefaultManager) List() ([]*types.Approval, error) {
bts, err := m.cache.List(ApprovalsPrefix)
if err != nil {
return nil, err
}
var approvals []*types.Approval
for _, v := range bts {
var approval types.Approval
err = m.serializer.Decode(v, &approval)
if err != nil {
log.WithFields(log.Fields{
"error": err,
}).Error("approvals.manager: failed to decode payload")
continue
}
approvals = append(approvals, &approval)
}
return approvals, nil
}
// Delete - delete specified approval
func (m *DefaultManager) Delete(identifier string) error {
return m.cache.Delete(getKey(identifier))
}
func getKey(identifier string) string {
return ApprovalsPrefix + "/" + identifier
}

366
approvals/approvals_test.go Normal file
View File

@ -0,0 +1,366 @@
package approvals
import (
"context"
"testing"
"time"
"github.com/rusenask/keel/cache/memory"
"github.com/rusenask/keel/types"
"github.com/rusenask/keel/util/codecs"
)
func TestCreateApproval(t *testing.T) {
mem := memory.NewMemoryCache(100*time.Millisecond, 100*time.Millisecond, 10*time.Millisecond)
am := New(mem, codecs.DefaultSerializer())
err := am.Create(&types.Approval{
Provider: types.ProviderTypeKubernetes,
Identifier: "xxx/app-1",
CurrentVersion: "1.2.3",
NewVersion: "1.2.5",
Deadline: time.Now().Add(5 * time.Minute),
})
if err != nil {
t.Fatalf("failed to create approval: %s", err)
}
stored, err := am.Get("xxx/app-1")
if err != nil {
t.Fatalf("failed to get approval: %s", err)
}
if stored.CurrentVersion != "1.2.3" {
t.Errorf("unexpected version: %s", stored.CurrentVersion)
}
}
func TestDeleteApproval(t *testing.T) {
mem := memory.NewMemoryCache(100*time.Millisecond, 100*time.Millisecond, 10*time.Millisecond)
am := New(mem, codecs.DefaultSerializer())
err := am.Create(&types.Approval{
Provider: types.ProviderTypeKubernetes,
Identifier: "xxx/app-1",
CurrentVersion: "1.2.3",
NewVersion: "1.2.5",
Deadline: time.Now().Add(5 * time.Minute),
})
if err != nil {
t.Fatalf("failed to create approval: %s", err)
}
err = am.Delete("xxx/app-1")
if err != nil {
t.Errorf("failed to delete approval: %s", err)
}
_, err = am.Get("xxx/app-1")
if err == nil {
t.Errorf("expected to get an error when retrieving deleted approval")
}
}
func TestUpdateApproval(t *testing.T) {
mem := memory.NewMemoryCache(100*time.Millisecond, 100*time.Millisecond, 10*time.Millisecond)
am := New(mem, codecs.DefaultSerializer())
err := am.Create(&types.Approval{
Provider: types.ProviderTypeKubernetes,
Identifier: "xxx/app-1",
CurrentVersion: "1.2.3",
NewVersion: "1.2.5",
VotesRequired: 1,
VotesReceived: 0,
Deadline: time.Now().Add(5 * time.Minute),
Event: &types.Event{
Repository: types.Repository{
Name: "very/repo",
Tag: "1.2.5",
},
},
})
if err != nil {
t.Fatalf("failed to create approval: %s", err)
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
ch, err := am.SubscribeApproved(ctx)
if err != nil {
t.Fatalf("failed to subscribe: %s", err)
}
err = am.Update(&types.Approval{
Provider: types.ProviderTypeKubernetes,
Identifier: "xxx/app-1",
CurrentVersion: "1.2.3",
NewVersion: "1.2.5",
VotesRequired: 1,
VotesReceived: 1,
Deadline: time.Now().Add(5 * time.Minute),
Event: &types.Event{
Repository: types.Repository{
Name: "very/repo",
Tag: "1.2.5",
},
},
})
approved := <-ch
if approved.Event.Repository.Name != "very/repo" {
t.Errorf("unexpected repo name in re-submitted event: %s", approved.Event.Repository.Name)
}
if approved.Event.Repository.Tag != "1.2.5" {
t.Errorf("unexpected repo tag in re-submitted event: %s", approved.Event.Repository.Tag)
}
}
func TestUpdateApprovalRejected(t *testing.T) {
mem := memory.NewMemoryCache(100*time.Millisecond, 100*time.Millisecond, 10*time.Millisecond)
am := New(mem, codecs.DefaultSerializer())
err := am.Create(&types.Approval{
Provider: types.ProviderTypeKubernetes,
Identifier: "xxx/app-1",
CurrentVersion: "1.2.3",
NewVersion: "1.2.5",
VotesRequired: 1,
VotesReceived: 0,
Deadline: time.Now().Add(5 * time.Minute),
Event: &types.Event{
Repository: types.Repository{
Name: "very/repo",
Tag: "1.2.5",
},
},
})
if err != nil {
t.Fatalf("failed to create approval: %s", err)
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
ch, err := am.SubscribeApproved(ctx)
if err != nil {
t.Fatalf("failed to subscribe: %s", err)
}
// rejecting
err = am.Update(&types.Approval{
Provider: types.ProviderTypeKubernetes,
Identifier: "xxx/app-1",
CurrentVersion: "1.2.3",
NewVersion: "1.2.5",
VotesRequired: 1,
VotesReceived: 0,
Rejected: true,
Deadline: time.Now().Add(5 * time.Minute),
Event: &types.Event{
Repository: types.Repository{
Name: "very/repo",
Tag: "1.2.5",
},
},
})
if err != nil {
t.Fatalf("failed to update approval: %s", err)
}
// sending vote
err = am.Update(&types.Approval{
Provider: types.ProviderTypeKubernetes,
Identifier: "xxx/app-1",
CurrentVersion: "1.2.3",
NewVersion: "1.2.5",
VotesRequired: 1,
VotesReceived: 1,
Rejected: true,
Deadline: time.Now().Add(5 * time.Minute),
Event: &types.Event{
Repository: types.Repository{
Name: "very/repo",
Tag: "1.2.5",
},
},
})
if err != nil {
t.Fatalf("failed to update approval: %s", err)
}
select {
case <-time.After(500 * time.Millisecond):
// success
return
case approval := <-ch:
t.Errorf("unexpected approval got: %s", approval.Identifier)
}
}
func TestApprove(t *testing.T) {
mem := memory.NewMemoryCache(100*time.Millisecond, 100*time.Millisecond, 10*time.Millisecond)
am := New(mem, codecs.DefaultSerializer())
err := am.Create(&types.Approval{
Provider: types.ProviderTypeKubernetes,
Identifier: "xxx/app-1:1.2.5",
CurrentVersion: "1.2.3",
NewVersion: "1.2.5",
Deadline: time.Now().Add(5 * time.Minute),
VotesRequired: 2,
VotesReceived: 0,
})
if err != nil {
t.Fatalf("failed to create approval: %s", err)
}
am.Approve("xxx/app-1:1.2.5", "warda")
stored, err := am.Get("xxx/app-1:1.2.5")
if err != nil {
t.Fatalf("failed to get approval: %s", err)
}
if stored.VotesReceived != 1 {
t.Errorf("unexpected number of received votes: %d", stored.VotesReceived)
}
}
func TestApproveTwiceSameVoter(t *testing.T) {
mem := memory.NewMemoryCache(100*time.Millisecond, 100*time.Millisecond, 10*time.Millisecond)
am := New(mem, codecs.DefaultSerializer())
err := am.Create(&types.Approval{
Provider: types.ProviderTypeKubernetes,
Identifier: "xxx/app-1:1.2.5",
CurrentVersion: "1.2.3",
NewVersion: "1.2.5",
Deadline: time.Now().Add(5 * time.Minute),
VotesRequired: 2,
VotesReceived: 0,
})
if err != nil {
t.Fatalf("failed to create approval: %s", err)
}
am.Approve("xxx/app-1:1.2.5", "warda")
am.Approve("xxx/app-1:1.2.5", "warda")
stored, err := am.Get("xxx/app-1:1.2.5")
if err != nil {
t.Fatalf("failed to get approval: %s", err)
}
// should still be the same
if stored.VotesReceived != 1 {
t.Errorf("unexpected number of received votes: %d", stored.VotesReceived)
}
}
func TestApproveTwoVoters(t *testing.T) {
mem := memory.NewMemoryCache(100*time.Millisecond, 100*time.Millisecond, 10*time.Millisecond)
am := New(mem, codecs.DefaultSerializer())
err := am.Create(&types.Approval{
Provider: types.ProviderTypeKubernetes,
Identifier: "xxx/app-1:1.2.5",
CurrentVersion: "1.2.3",
NewVersion: "1.2.5",
Deadline: time.Now().Add(5 * time.Minute),
VotesRequired: 2,
VotesReceived: 0,
})
if err != nil {
t.Fatalf("failed to create approval: %s", err)
}
am.Approve("xxx/app-1:1.2.5", "w")
am.Approve("xxx/app-1:1.2.5", "k")
stored, err := am.Get("xxx/app-1:1.2.5")
if err != nil {
t.Fatalf("failed to get approval: %s", err)
}
// should still be the same
if stored.VotesReceived != 2 {
t.Errorf("unexpected number of received votes: %d", stored.VotesReceived)
}
}
func TestReject(t *testing.T) {
mem := memory.NewMemoryCache(100*time.Millisecond, 100*time.Millisecond, 10*time.Millisecond)
am := New(mem, codecs.DefaultSerializer())
err := am.Create(&types.Approval{
Provider: types.ProviderTypeKubernetes,
Identifier: "xxx/app-1",
CurrentVersion: "1.2.3",
NewVersion: "1.2.5",
Deadline: time.Now().Add(5 * time.Minute),
VotesRequired: 2,
VotesReceived: 0,
})
if err != nil {
t.Fatalf("failed to create approval: %s", err)
}
am.Reject("xxx/app-1")
stored, err := am.Get("xxx/app-1")
if err != nil {
t.Fatalf("failed to get approval: %s", err)
}
if !stored.Rejected {
t.Errorf("unexpected approval to be rejected")
}
}
func TestExpire(t *testing.T) {
mem := memory.NewMemoryCache(100*time.Millisecond, 100*time.Millisecond, 10*time.Millisecond)
am := New(mem, codecs.DefaultSerializer())
err := am.Create(&types.Approval{
Provider: types.ProviderTypeKubernetes,
Identifier: "xxx/app-1",
CurrentVersion: "1.2.3",
NewVersion: "1.2.5",
Deadline: time.Now().Add(-5 * time.Minute),
VotesRequired: 2,
VotesReceived: 0,
})
if err != nil {
t.Fatalf("failed to create approval: %s", err)
}
err = am.expireEntries()
if err != nil {
t.Errorf("got error while expiring entries: %s", err)
}
_, err = am.Get("xxx/app-1")
if err == nil {
t.Errorf("expected approval to be deleted but didn't get an error")
}
}

286
bot/approvals.go Normal file
View File

@ -0,0 +1,286 @@
package bot
import (
"bytes"
"fmt"
"strings"
"github.com/nlopes/slack"
"github.com/rusenask/keel/bot/formatter"
"github.com/rusenask/keel/types"
log "github.com/Sirupsen/logrus"
)
func (b *Bot) subscribeForApprovals() error {
approvalsCh, err := b.approvalsManager.Subscribe(b.ctx)
if err != nil {
return err
}
for {
select {
case <-b.ctx.Done():
return nil
case a := <-approvalsCh:
err = b.requestApproval(a)
if err != nil {
log.WithFields(log.Fields{
"error": err,
"approval": a.Identifier,
}).Error("bot.subscribeForApprovals: approval request failed")
}
}
}
}
// Request - request approval
func (b *Bot) requestApproval(req *types.Approval) error {
return b.postMessage(
"Approval required",
req.Message,
types.LevelSuccess.Color(),
[]slack.AttachmentField{
slack.AttachmentField{
Title: "Approval required!",
Value: req.Message + "\n" + fmt.Sprintf("To vote for change type '%s approve %s' to reject it: '%s reject %s'.", b.name, req.Identifier, b.name, req.Identifier),
Short: false,
},
slack.AttachmentField{
Title: "Votes",
Value: fmt.Sprintf("%d/%d", req.VotesReceived, req.VotesRequired),
Short: true,
},
slack.AttachmentField{
Title: "Delta",
Value: req.Delta(),
Short: true,
},
slack.AttachmentField{
Title: "Identifier",
Value: req.Identifier,
Short: true,
},
slack.AttachmentField{
Title: "Provider",
Value: req.Provider.String(),
Short: true,
},
})
}
func (b *Bot) processApprovalResponses() error {
for {
select {
case <-b.ctx.Done():
return nil
case resp := <-b.approvalsRespCh:
switch resp.Status {
case types.ApprovalStatusApproved:
err := b.processApprovedResponse(resp)
if err != nil {
log.WithFields(log.Fields{
"error": err,
}).Error("bot.processApprovalResponses: failed to process approval response message")
}
case types.ApprovalStatusRejected:
err := b.processRejectedResponse(resp)
if err != nil {
log.WithFields(log.Fields{
"error": err,
}).Error("bot.processApprovalResponses: failed to process approval reject response message")
}
}
}
}
}
func (b *Bot) processApprovedResponse(approvalResponse *approvalResponse) error {
trimmed := strings.TrimPrefix(approvalResponse.Text, approvalResponseKeyword)
identifiers := strings.Split(trimmed, " ")
if len(identifiers) == 0 {
return nil
}
for _, identifier := range identifiers {
if identifier == "" {
continue
}
approval, err := b.approvalsManager.Approve(identifier, approvalResponse.User)
if err != nil {
log.WithFields(log.Fields{
"error": err,
"identifier": identifier,
}).Error("bot.processApprovedResponse: failed to approve")
continue
}
err = b.replyToApproval(approval)
if err != nil {
log.WithFields(log.Fields{
"error": err,
"identifier": identifier,
}).Error("bot.processApprovedResponse: got error while replying after processing approved approval")
}
}
return nil
}
func (b *Bot) processRejectedResponse(approvalResponse *approvalResponse) error {
trimmed := strings.TrimPrefix(approvalResponse.Text, rejectResponseKeyword)
identifiers := strings.Split(trimmed, " ")
if len(identifiers) == 0 {
return nil
}
for _, identifier := range identifiers {
approval, err := b.approvalsManager.Reject(identifier)
if err != nil {
log.WithFields(log.Fields{
"error": err,
"identifier": identifier,
}).Error("bot.processApprovedResponse: failed to reject")
continue
}
err = b.replyToApproval(approval)
if err != nil {
log.WithFields(log.Fields{
"error": err,
"identifier": identifier,
}).Error("bot.processApprovedResponse: got error while replying after processing rejected approval")
}
}
return nil
}
func (b *Bot) replyToApproval(approval *types.Approval) error {
switch approval.Status() {
case types.ApprovalStatusPending:
b.postMessage(
"Vote received",
"All approvals received, thanks for voting!",
types.LevelInfo.Color(),
[]slack.AttachmentField{
slack.AttachmentField{
Title: "vote received!",
Value: "Waiting for remaining votes.",
Short: false,
},
slack.AttachmentField{
Title: "Votes",
Value: fmt.Sprintf("%d/%d", approval.VotesReceived, approval.VotesRequired),
Short: true,
},
slack.AttachmentField{
Title: "Delta",
Value: approval.Delta(),
Short: true,
},
slack.AttachmentField{
Title: "Identifier",
Value: approval.Identifier,
Short: true,
},
})
case types.ApprovalStatusRejected:
b.postMessage(
"Change rejected",
"Change was rejected",
types.LevelWarn.Color(),
[]slack.AttachmentField{
slack.AttachmentField{
Title: "change rejected",
Value: "Change was rejected.",
Short: false,
},
slack.AttachmentField{
Title: "Status",
Value: approval.Status().String(),
Short: true,
},
slack.AttachmentField{
Title: "Votes",
Value: fmt.Sprintf("%d/%d", approval.VotesReceived, approval.VotesRequired),
Short: true,
},
slack.AttachmentField{
Title: "Delta",
Value: approval.Delta(),
Short: true,
},
slack.AttachmentField{
Title: "Identifier",
Value: approval.Identifier,
Short: true,
},
})
case types.ApprovalStatusApproved:
b.postMessage(
"approval received",
"All approvals received, thanks for voting!",
types.LevelSuccess.Color(),
[]slack.AttachmentField{
slack.AttachmentField{
Title: "update approved!",
Value: "All approvals received, thanks for voting!",
Short: false,
},
slack.AttachmentField{
Title: "Votes",
Value: fmt.Sprintf("%d/%d", approval.VotesReceived, approval.VotesRequired),
Short: true,
},
slack.AttachmentField{
Title: "Delta",
Value: approval.Delta(),
Short: true,
},
slack.AttachmentField{
Title: "Identifier",
Value: approval.Identifier,
Short: true,
},
})
}
return nil
}
func (b *Bot) approvalsResponse() string {
approvals, err := b.approvalsManager.List()
if err != nil {
return fmt.Sprintf("got error while fetching approvals: %s", err)
}
if len(approvals) == 0 {
return fmt.Sprintf("there are currently no request waiting to be approved.")
}
buf := &bytes.Buffer{}
approvalCtx := formatter.Context{
Output: buf,
Format: formatter.NewApprovalsFormat(formatter.TableFormatKey, false),
}
err = formatter.ApprovalWrite(approvalCtx, approvals)
if err != nil {
return fmt.Sprintf("got error while formatting approvals: %s", err)
}
return buf.String()
}
func (b *Bot) removeApprovalHandler(identifier string) string {
err := b.approvalsManager.Delete(identifier)
if err != nil {
return fmt.Sprintf("failed to remove '%s' approval: %s.", identifier, err)
}
return fmt.Sprintf("approval '%s' removed.", identifier)
}

View File

@ -2,22 +2,35 @@ package bot
import (
"context"
"encoding/json"
"errors"
"fmt"
"strconv"
"strings"
"time"
"github.com/nlopes/slack"
"github.com/rusenask/keel/approvals"
"github.com/rusenask/keel/provider/kubernetes"
"github.com/rusenask/keel/types"
log "github.com/Sirupsen/logrus"
)
const (
removeApprovalPrefix = "rm approval"
)
var (
botEventTextToResponse = map[string][]string{
"help": {
`Here's a list of supported commands`,
`- "get deployments" -> get a list of all deployments`,
`- "get approvals" -> get a list of approvals`,
`- "rm approval <approval identifier>" -> remove approval`,
`- "approve <approval identifier>" -> approve update request`,
`- "reject <approval identifier>" -> reject update request`,
// `- "get deployments all" -> get a list of all deployments`,
// `- "describe deployment <deployment>" -> get details for specified deployment`,
},
@ -25,14 +38,30 @@ var (
// static bot commands can be used straight away
staticBotCommands = map[string]bool{
"get deployments": true,
"get deployments all": true,
"get deployments": true,
"get approvals": true,
}
// dynamic bot command prefixes have to be matched
dynamicBotCommandPrefixes = []string{"describe deployment"}
dynamicBotCommandPrefixes = []string{removeApprovalPrefix}
approvalResponseKeyword = "approve"
rejectResponseKeyword = "reject"
)
// SlackImplementer - implementes slack HTTP functionality, used to
// send messages with attachments
type SlackImplementer interface {
PostMessage(channel, text string, params slack.PostMessageParameters) (string, string, error)
}
// approvalResponse - used to track approvals once vote begins
type approvalResponse struct {
User string
Status types.ApprovalStatus
Text string
}
// Bot - main slack bot container
type Bot struct {
id string // bot id
@ -45,20 +74,31 @@ type Bot struct {
slackClient *slack.Client
slackRTM *slack.RTM
slackHTTPClient SlackImplementer
approvalsRespCh chan *approvalResponse
approvalsManager approvals.Manager
k8sImplementer kubernetes.Implementer
ctx context.Context
}
// New - create new bot instance
func New(name, token string, k8sImplementer kubernetes.Implementer) *Bot {
func New(name, token string, k8sImplementer kubernetes.Implementer, approvalsManager approvals.Manager) *Bot {
client := slack.New(token)
return &Bot{
slackClient: client,
k8sImplementer: k8sImplementer,
name: name,
bot := &Bot{
slackClient: client,
slackHTTPClient: client,
k8sImplementer: k8sImplementer,
name: name,
approvalsManager: approvalsManager,
approvalsRespCh: make(chan *approvalResponse), // don't add buffer to make it blocking
}
return bot
}
// Start - start bot
@ -90,8 +130,15 @@ func (b *Bot) Start(ctx context.Context) error {
b.msgPrefix = strings.ToLower("<@" + b.id + ">")
// processing messages coming from slack RTM client
go b.startInternal()
// processing slack approval responses
go b.processApprovalResponses()
// subscribing for approval requests
go b.subscribeForApprovals()
return nil
}
@ -140,6 +187,49 @@ func (b *Bot) startInternal() error {
}
}
func (b *Bot) postMessage(title, message, color string, fields []slack.AttachmentField) error {
params := slack.NewPostMessageParameters()
params.Username = b.name
params.Attachments = []slack.Attachment{
slack.Attachment{
Fallback: message,
Color: color,
Fields: fields,
Footer: "https://keel.sh",
Ts: json.Number(strconv.Itoa(int(time.Now().Unix()))),
},
}
_, _, err := b.slackHTTPClient.PostMessage("general", "", params)
if err != nil {
log.WithFields(log.Fields{
"error": err,
}).Error("bot.postMessage: failed to send message")
}
return err
}
func (b *Bot) isApproval(event *slack.MessageEvent, eventText string) (resp *approvalResponse, ok bool) {
if strings.HasPrefix(strings.ToLower(eventText), approvalResponseKeyword) {
return &approvalResponse{
User: event.User,
Status: types.ApprovalStatusApproved,
Text: eventText,
}, true
}
if strings.HasPrefix(strings.ToLower(eventText), rejectResponseKeyword) {
return &approvalResponse{
User: event.User,
Status: types.ApprovalStatusRejected,
Text: eventText,
}, true
}
return nil, false
}
func (b *Bot) handleMessage(event *slack.MessageEvent) {
if event.BotID != "" || event.User == "" || event.SubType == "bot_message" {
log.WithFields(log.Fields{
@ -152,12 +242,16 @@ func (b *Bot) handleMessage(event *slack.MessageEvent) {
eventText := strings.Trim(strings.ToLower(event.Text), " \n\r")
// All messages past this point are directed to @gopher itself
if !b.isBotMessage(event, eventText) {
return
}
eventText = b.trimBot(eventText)
approval, ok := b.isApproval(event, eventText)
if ok {
b.approvalsRespCh <- approval
return
}
// Responses that are just a canned string response
if responseLines, ok := botEventTextToResponse[eventText]; ok {
@ -200,6 +294,16 @@ func (b *Bot) handleCommand(event *slack.MessageEvent, eventText string) {
response := b.deploymentsResponse(Filter{})
b.respond(event, formatAsSnippet(response))
return
case "get approvals":
response := b.approvalsResponse()
b.respond(event, formatAsSnippet(response))
return
}
// handle dynamic commands
if strings.HasPrefix(eventText, removeApprovalPrefix) {
b.respond(event, formatAsSnippet(b.removeApprovalHandler(strings.TrimSpace(strings.TrimPrefix(eventText, removeApprovalPrefix)))))
return
}
log.Info("command not found")

315
bot/bot_test.go Normal file
View File

@ -0,0 +1,315 @@
package bot
import (
"context"
"fmt"
"os"
"time"
"github.com/nlopes/slack"
"github.com/rusenask/keel/approvals"
"github.com/rusenask/keel/cache/memory"
"github.com/rusenask/keel/constants"
"github.com/rusenask/keel/extension/approval"
"github.com/rusenask/keel/types"
"github.com/rusenask/keel/util/codecs"
"testing"
testutil "github.com/rusenask/keel/util/testing"
)
type fakeProvider struct {
submitted []types.Event
images []*types.TrackedImage
}
func (p *fakeProvider) Submit(event types.Event) error {
p.submitted = append(p.submitted, event)
return nil
}
func (p *fakeProvider) TrackedImages() ([]*types.TrackedImage, error) {
return p.images, nil
}
func (p *fakeProvider) List() []string {
return []string{"fakeprovider"}
}
func (p *fakeProvider) Stop() {
return
}
func (p *fakeProvider) GetName() string {
return "fp"
}
type postedMessage struct {
channel string
text string
params slack.PostMessageParameters
}
type fakeSlackImplementer struct {
postedMessages []postedMessage
}
func (i *fakeSlackImplementer) PostMessage(channel, text string, params slack.PostMessageParameters) (string, string, error) {
i.postedMessages = append(i.postedMessages, postedMessage{
channel: channel,
text: text,
params: params,
})
return "", "", nil
}
func TestBotRequest(t *testing.T) {
f8s := &testutil.FakeK8sImplementer{}
fi := &fakeSlackImplementer{}
mem := memory.NewMemoryCache(100*time.Second, 100*time.Second, 10*time.Second)
token := os.Getenv(constants.EnvSlackToken)
if token == "" {
t.Skip()
}
am := approvals.New(mem, codecs.DefaultSerializer())
bot := New("keel", token, f8s, am)
// replacing slack client so we can receive webhooks
bot.slackHTTPClient = fi
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
err := bot.Start(ctx)
if err != nil {
t.Fatalf("failed to start bot: %s", err)
}
time.Sleep(1 * time.Second)
err = am.Create(&types.Approval{
Identifier: "k8s/project/repo:1.2.3",
VotesRequired: 1,
CurrentVersion: "2.3.4",
NewVersion: "3.4.5",
Event: &types.Event{
Repository: types.Repository{
Name: "project/repo",
Tag: "2.3.4",
},
},
})
if err != nil {
t.Fatalf("unexpected error while creating : %s", err)
}
time.Sleep(1 * time.Second)
if len(fi.postedMessages) != 1 {
t.Errorf("expected to find one message, but got: %d", len(fi.postedMessages))
}
}
func TestProcessApprovedResponse(t *testing.T) {
f8s := &testutil.FakeK8sImplementer{}
fi := &fakeSlackImplementer{}
mem := memory.NewMemoryCache(100*time.Second, 100*time.Second, 10*time.Second)
token := os.Getenv(constants.EnvSlackToken)
if token == "" {
t.Skip()
}
am := approvals.New(mem, codecs.DefaultSerializer())
bot := New("keel", token, f8s, am)
// replacing slack client so we can receive webhooks
bot.slackHTTPClient = fi
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
err := bot.Start(ctx)
if err != nil {
t.Fatalf("failed to start bot: %s", err)
}
time.Sleep(1 * time.Second)
err = am.Create(&types.Approval{
Identifier: "k8s/project/repo:1.2.3",
VotesRequired: 1,
CurrentVersion: "2.3.4",
NewVersion: "3.4.5",
Event: &types.Event{
Repository: types.Repository{
Name: "project/repo",
Tag: "2.3.4",
},
},
})
if err != nil {
t.Fatalf("unexpected error while creating : %s", err)
}
time.Sleep(1 * time.Second)
if len(fi.postedMessages) != 1 {
t.Errorf("expected to find one message")
}
}
func TestProcessApprovalReply(t *testing.T) {
f8s := &testutil.FakeK8sImplementer{}
fi := &fakeSlackImplementer{}
mem := memory.NewMemoryCache(100*time.Second, 100*time.Second, 10*time.Second)
token := os.Getenv(constants.EnvSlackToken)
if token == "" {
t.Skip()
}
am := approvals.New(mem, codecs.DefaultSerializer())
identifier := "k8s/project/repo:1.2.3"
// creating initial approve request
err := am.Create(&types.Approval{
Identifier: identifier,
VotesRequired: 2,
CurrentVersion: "2.3.4",
NewVersion: "3.4.5",
Event: &types.Event{
Repository: types.Repository{
Name: "project/repo",
Tag: "2.3.4",
},
},
})
if err != nil {
t.Fatalf("unexpected error while creating : %s", err)
}
bot := New("keel", token, f8s, am)
// replacing slack client so we can receive webhooks
bot.slackHTTPClient = fi
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
bot.ctx = ctx
go bot.processApprovalResponses()
time.Sleep(1 * time.Second)
// approval resp
bot.approvalsRespCh <- &approvalResponse{
User: "123",
Status: types.ApprovalStatusApproved,
Text: fmt.Sprintf("%s %s", approvalResponseKeyword, identifier),
}
time.Sleep(1 * time.Second)
updated, err := am.Get(identifier)
if err != nil {
t.Fatalf("failed to get approval, error: %s", err)
}
if updated.VotesReceived != 1 {
t.Errorf("expected to find 1 received vote, found %d", updated.VotesReceived)
}
if updated.Status() != types.ApprovalStatusPending {
t.Errorf("expected approval to be in status pending but got: %s", updated.Status())
}
if len(fi.postedMessages) != 1 {
t.Errorf("expected to find one message")
}
}
func TestProcessRejectedReply(t *testing.T) {
f8s := &testutil.FakeK8sImplementer{}
fi := &fakeSlackImplementer{}
mem := memory.NewMemoryCache(100*time.Hour, 100*time.Hour, 100*time.Hour)
// token := os.Getenv(constants.EnvSlackToken)
// if token == "" {
// t.Skip()
// }
identifier := "k8s/project/repo:1.2.3"
am := approvals.New(mem, codecs.DefaultSerializer())
// creating initial approve request
err := am.Create(&types.Approval{
Identifier: identifier,
VotesRequired: 2,
CurrentVersion: "2.3.4",
NewVersion: "3.4.5",
Event: &types.Event{
Repository: types.Repository{
Name: "project/repo",
Tag: "2.3.4",
},
},
})
if err != nil {
t.Fatalf("unexpected error while creating : %s", err)
}
bot := New("keel", "random", f8s, am)
collector := approval.New()
collector.Configure(am)
// replacing slack client so we can receive webhooks
bot.slackHTTPClient = fi
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
bot.ctx = ctx
go bot.processApprovalResponses()
time.Sleep(1 * time.Second)
// approval resp
bot.approvalsRespCh <- &approvalResponse{
User: "123",
Status: types.ApprovalStatusRejected,
Text: fmt.Sprintf("%s %s", rejectResponseKeyword, identifier),
}
time.Sleep(1 * time.Second)
updated, err := am.Get(identifier)
if err != nil {
t.Fatalf("failed to get approval, error: %s", err)
}
if updated.VotesReceived != 0 {
t.Errorf("expected to find 0 received vote, found %d", updated.VotesReceived)
}
// if updated.Status() != types.ApprovalStatusRejected {
if updated.Status() != types.ApprovalStatusRejected {
t.Errorf("expected approval to be in status rejected but got: %s", updated.Status())
}
fmt.Println(updated.Status())
if len(fi.postedMessages) != 1 {
t.Errorf("expected to find one message")
}
}

View File

@ -0,0 +1,93 @@
package formatter
import (
"fmt"
"strconv"
"github.com/rusenask/keel/types"
)
// Formatter headers
const (
defaultApprovalQuietFormat = "{{.Identifier}} {{.Delta}}"
defaultApprovalTableFormat = "table {{.Identifier}}\t{{.Delta}}\t{{.Votes}}\t{{.Rejected}}\t{{.Provider}}\t{{.Created}}"
ApprovalIdentifierHeader = "Identifier"
ApprovalDeltaHeader = "Delta"
ApprovalVotesHeader = "Votes"
ApprovalRejectedHeader = "Rejected"
ApprovalProviderHeader = "Provider"
ApprovalCreatedHeader = "Created"
)
// NewApprovalsFormat returns a format for use with a approval Context
func NewApprovalsFormat(source string, quiet bool) Format {
switch source {
case TableFormatKey:
if quiet {
return defaultApprovalQuietFormat
}
return defaultApprovalTableFormat
case RawFormatKey:
if quiet {
return `name: {{.Identifier}}`
}
return `name: {{.Identifier}}\n`
}
return Format(source)
}
// ApprovalWrite writes formatted approvals using the Context
func ApprovalWrite(ctx Context, approvals []*types.Approval) error {
render := func(format func(subContext subContext) error) error {
for _, approval := range approvals {
if err := format(&ApprovalContext{v: *approval}); err != nil {
return err
}
}
return nil
}
return ctx.Write(&DeploymentContext{}, render)
}
// ApprovalContext - approval context is a container for each line
type ApprovalContext struct {
HeaderContext
v types.Approval
}
// MarshalJSON - marshal to json (inspect)
func (c *ApprovalContext) MarshalJSON() ([]byte, error) {
return marshalJSON(c)
}
func (c *ApprovalContext) Identifier() string {
c.AddHeader(ApprovalIdentifierHeader)
return c.v.Identifier
}
func (c *ApprovalContext) Delta() string {
c.AddHeader(ApprovalDeltaHeader)
return c.v.Delta()
}
func (c *ApprovalContext) Votes() string {
c.AddHeader(ApprovalVotesHeader)
return fmt.Sprintf("%d/%d", c.v.VotesReceived, c.v.VotesRequired)
}
func (c *ApprovalContext) Rejected() string {
c.AddHeader(ApprovalRejectedHeader)
return strconv.FormatBool(c.v.Rejected)
}
func (c *ApprovalContext) Provider() string {
c.AddHeader(ApprovalProviderHeader)
return c.v.Provider.String()
}
func (c *ApprovalContext) Created() string {
c.AddHeader(ApprovalCreatedHeader)
return c.v.CreatedAt.String()
}

45
cache/cache.go vendored Normal file
View File

@ -0,0 +1,45 @@
package cache
import (
"context"
"errors"
"time"
)
// Cache - generic cache interface
// type Cache interface {
// Put(ctx context.Context, key string, value []byte) error
// Get(ctx context.Context, key string) (value []byte, err error)
// Delete(ctx context.Context, key string) error
// List(prefix string) ([][]byte, error)
// }
type Cache interface {
Put(key string, value []byte) error
Get(key string) (value []byte, err error)
Delete(key string) error
List(prefix string) (map[string][]byte, error)
}
type expirationContextKeyType int
const expirationContextKey expirationContextKeyType = 1
// SetContextExpiration - set cache expiration context
func SetContextExpiration(ctx context.Context, expiration time.Duration) context.Context {
return context.WithValue(ctx, expirationContextKey, expiration)
}
// GetContextExpiration - gets expiration from context, returns it and also returns
// ok - true/false to indicate whether ctx value was found
func GetContextExpiration(ctx context.Context) (exp time.Duration, ok bool) {
expiration := ctx.Value(expirationContextKey)
if expiration != nil {
return expiration.(time.Duration), true
}
return 0, false
}
var (
ErrNotFound = errors.New("not found")
ErrExpired = errors.New("entry expired")
)

46
cache/kubekv/kv.go vendored Normal file
View File

@ -0,0 +1,46 @@
package kubekv
import (
"github.com/rusenask/keel/cache"
"github.com/rusenask/k8s-kv/kv"
)
type KubeKV struct {
kv *kv.KV
}
func New(implementer kv.ConfigMapInterface, bucket string) (*KubeKV, error) {
kvdb, err := kv.New(implementer, "keel", bucket)
if err != nil {
return nil, err
}
return &KubeKV{
kv: kvdb,
}, nil
}
func (k *KubeKV) Put(key string, value []byte) error {
return k.kv.Put(key, value)
}
func (k *KubeKV) Get(key string) (value []byte, err error) {
value, err = k.kv.Get(key)
if err != nil {
if err == kv.ErrNotFound {
return []byte(""), cache.ErrNotFound
}
}
return
}
func (k *KubeKV) Delete(key string) error {
return k.kv.Delete(key)
}
func (k *KubeKV) List(prefix string) (map[string][]byte, error) {
return k.kv.List(prefix)
}

209
cache/memory/memory.go vendored Normal file
View File

@ -0,0 +1,209 @@
package memory
import (
"fmt"
"strings"
"time"
"github.com/rusenask/keel/cache"
)
type requestType int
// Request types
const (
GET requestType = iota
SET
DELETE
EXPIRE
COPY
)
type (
// Value - value is stored together with access and creation time
Value struct {
ctime time.Time
atime time.Time
value []byte
}
// Cache - cache container with a map of values and defaults
Cache struct {
cache map[string]*Value
ctimeExpiry time.Duration // creation time
atimeExpiry time.Duration // access time
expiryTick time.Duration
requestChannel chan *request
}
request struct {
requestType
key string
value []byte
responseChannel chan *response
}
response struct {
error
existingValue []byte
mapCopy map[string][]byte
value []byte
}
)
func (c *Cache) isOld(v *Value) bool {
if (c.ctimeExpiry != time.Duration(0)) && (time.Now().Sub(v.ctime) > c.ctimeExpiry) {
return true
}
if (c.atimeExpiry != time.Duration(0)) && (time.Now().Sub(v.atime) > c.atimeExpiry) {
return true
}
return false
}
// NewMemoryCache - creates new cache
func NewMemoryCache(ctimeExpiry, atimeExpiry, expiryTick time.Duration) *Cache {
c := &Cache{
cache: make(map[string]*Value),
ctimeExpiry: ctimeExpiry,
atimeExpiry: atimeExpiry,
expiryTick: expiryTick,
requestChannel: make(chan *request),
}
go c.service()
if ctimeExpiry != time.Duration(0) || atimeExpiry != time.Duration(0) {
go c.expiryService()
}
return c
}
func (c *Cache) service() {
for {
req := <-c.requestChannel
resp := &response{}
switch req.requestType {
case GET:
val, ok := c.cache[req.key]
if !ok {
resp.error = cache.ErrNotFound
} else if c.isOld(val) {
resp.error = cache.ErrExpired
delete(c.cache, req.key)
} else {
// update atime
val.atime = time.Now()
c.cache[req.key] = val
resp.value = val.value
}
req.responseChannel <- resp
case SET:
c.cache[req.key] = &Value{
value: req.value,
ctime: time.Now(),
atime: time.Now(),
}
req.responseChannel <- resp
case DELETE:
delete(c.cache, req.key)
req.responseChannel <- resp
case EXPIRE:
for k, v := range c.cache {
if c.isOld(v) {
delete(c.cache, k)
}
}
// no response
case COPY:
resp.mapCopy = make(map[string][]byte)
for k, v := range c.cache {
resp.mapCopy[k] = v.value
}
req.responseChannel <- resp
default:
resp.error = fmt.Errorf("invalid request type: %v", req.requestType)
req.responseChannel <- resp
}
}
}
// Get - looks up value and returns it
func (c *Cache) Get(key string) ([]byte, error) {
respChannel := make(chan *response)
c.requestChannel <- &request{
requestType: GET,
key: key,
responseChannel: respChannel,
}
resp := <-respChannel
return resp.value, resp.error
}
// Put - sets key/string. Overwrites existing key
func (c *Cache) Put(key string, value []byte) error {
respChannel := make(chan *response)
c.requestChannel <- &request{
requestType: SET,
key: key,
value: value,
responseChannel: respChannel,
}
resp := <-respChannel
return resp.error
}
// Delete - deletes key
func (c *Cache) Delete(key string) error {
respChannel := make(chan *response)
c.requestChannel <- &request{
requestType: DELETE,
key: key,
responseChannel: respChannel,
}
resp := <-respChannel
return resp.error
}
// List all values for specified prefix
func (c *Cache) List(prefix string) (map[string][]byte, error) {
respChannel := make(chan *response)
c.requestChannel <- &request{
requestType: COPY,
responseChannel: respChannel,
}
resp := <-respChannel
list := make(map[string][]byte)
for k, v := range resp.mapCopy {
if strings.HasPrefix(k, prefix) {
list[k] = v
}
}
return list, nil
}
// Copy - makes a copy of inmemory map
func (c *Cache) Copy() map[string][]byte {
respChannel := make(chan *response)
c.requestChannel <- &request{
requestType: COPY,
responseChannel: respChannel,
}
resp := <-respChannel
return resp.mapCopy
}
func (c *Cache) expiryService() {
ticker := time.NewTicker(c.expiryTick)
defer ticker.Stop()
for {
select {
case <-ticker.C:
c.requestChannel <- &request{
requestType: EXPIRE,
}
}
}
}

83
cache/memory/memory_test.go vendored Normal file
View File

@ -0,0 +1,83 @@
package memory
import (
"log"
"testing"
"time"
)
func TestCacheSetGet(t *testing.T) {
c := NewMemoryCache(100*time.Millisecond, 100*time.Millisecond, 10*time.Millisecond)
err := c.Put("a", []byte("b"))
if err != nil {
t.Errorf("failed to SET a key, got error: %s", err)
}
val, err := c.Get("a")
if err != nil {
t.Errorf("failed to GET a key, got error: %s", err)
}
if string(val) != "b" {
log.Panicf("value %v", val)
}
cc := c.Copy()
if len(cc) != 1 {
t.Errorf("expected 1 item, got: %d", len(cc))
}
}
func TestCacheDel(t *testing.T) {
c := NewMemoryCache(100*time.Millisecond, 100*time.Millisecond, 10*time.Millisecond)
err := c.Put("a", []byte("b"))
if err != nil {
t.Errorf("failed to SET a key, got error: %s", err)
}
val, err := c.Get("a")
if err != nil {
t.Errorf("failed to GET a key, got error: %s", err)
}
if string(val) != "b" {
log.Panicf("value %v", val)
}
err = c.Delete("a")
if err != nil {
t.Errorf("faield to delete entry, got error: %s", err)
}
_, err = c.Get("a")
if err == nil {
t.Errorf("expected to get an error after deletion, but got nil")
}
}
func TestCacheExpiration(t *testing.T) {
c := NewMemoryCache(100*time.Millisecond, 100*time.Millisecond, 10*time.Millisecond)
err := c.Put("a", []byte("b"))
if err != nil {
t.Errorf("failed to SET a key, got error: %s", err)
}
val, err := c.Get("a")
if err != nil {
t.Errorf("failed to GET a key, got error: %s", err)
}
if string(val) != "b" {
log.Panicf("value %v", val)
}
time.Sleep(200 * time.Millisecond)
_, err = c.Get("a")
if err == nil {
t.Errorf("expected to get an error after deletion, but got nil")
}
}

View File

@ -3,6 +3,9 @@ package constants
// DefaultDockerRegistry - default docker registry
const DefaultDockerRegistry = "https://index.docker.io"
// DefaultNamespace - default namespace to initialise configmaps based kv
const DefaultNamespace = "kube-system"
// WebhookEndpointEnv if set - enables webhook notifications
const WebhookEndpointEnv = "WEBHOOK_ENDPOINT"

View File

@ -0,0 +1,96 @@
package approval
import (
"sync"
"github.com/rusenask/keel/approvals"
log "github.com/Sirupsen/logrus"
)
var (
collectorsM sync.RWMutex
collectors = make(map[string]Collector)
)
// Collector - generic interface for implementing approval mechanisms
type Collector interface {
Configure(approvalsManager approvals.Manager) (bool, error)
}
func RegisterCollector(name string, c Collector) {
if name == "" {
panic("approval collector:: could not register a Sender with an empty name")
}
if c == nil {
panic("approval collector:: could not register a nil Sender")
}
collectorsM.Lock()
defer collectorsM.Unlock()
if _, dup := collectors[name]; dup {
panic("approval collector: RegisterCollector called twice for " + name)
}
log.WithFields(log.Fields{
"name": name,
}).Info("approval.RegisterCollector: collector registered")
collectors[name] = c
}
// MainCollector holds all registered collectors
type MainCollector struct {
approvalsManager approvals.Manager
}
// New - create new sender
func New() *MainCollector {
return &MainCollector{}
}
// Configure - configure is used to register multiple notification senders
func (m *MainCollector) Configure(approvalsManager approvals.Manager) (bool, error) {
m.approvalsManager = approvalsManager
// Configure registered notifiers.
for collectorName, collector := range m.Collectors() {
if configured, err := collector.Configure(approvalsManager); configured {
log.WithFields(log.Fields{
"name": collectorName,
}).Info("extension.approval.Configure: collector configured")
} else {
m.UnregisterCollector(collectorName)
if err != nil {
log.WithFields(log.Fields{
"name": collectorName,
"error": err,
}).Error("extension.approval.Configure: could not configure collector")
}
}
}
return true, nil
}
// Collectors returns the list of the registered Collectors.
func (m *MainCollector) Collectors() map[string]Collector {
collectorsM.RLock()
defer collectorsM.RUnlock()
ret := make(map[string]Collector)
for k, v := range collectors {
ret[k] = v
}
return ret
}
// UnregisterCollector removes a Collector with a particular name from the list.
func (m *MainCollector) UnregisterCollector(name string) {
collectorsM.Lock()
defer collectorsM.Unlock()
delete(collectors, name)
}

24
glide.lock generated
View File

@ -1,5 +1,5 @@
hash: 4f5cf366af304cb50b974df8ea9c020057f1d3b2bf1d1f9bd462a1e436c4f13b
updated: 2017-08-04T22:06:13.059396344+01:00
hash: 7745d67060c2a5cdcec48b6caad7fe970628b1a8cf19496cce7060f9ede1421a
updated: 2017-09-12T19:36:03.621062651+03:00
imports:
- name: cloud.google.com/go
version: b4e9a381a01e953e880e6d2cf7fd02d412977cae
@ -75,11 +75,11 @@ imports:
- name: github.com/google/gofuzz
version: 44d81051d367757e1c7c6a5a86423ece9afcf63c
- name: github.com/googleapis/gax-go
version: 84ed26760e7f6f80887a2fbfb50db3cc415d2cea
version: 8c160ca1523d8eea3932fbaa494c8964b7724aa8
- name: github.com/gorilla/context
version: 08b5f424b9271eedf6f9f0ce86cb9396ed337a42
- name: github.com/gorilla/mux
version: bcd8bc72b08df0f70df986b97f95590779502d31
version: 24fca303ac6da784b9e8269f724ddeb0b2eea5e7
- name: github.com/howeyc/gopass
version: 3ca23474a7c7203e0a0a070fd33508f6efdb9b3d
- name: github.com/imdario/mergo
@ -108,8 +108,16 @@ imports:
version: 315973e9173738626b8c81cb39ba247f8cb190e5
subpackages:
- registry
- name: github.com/rusenask/k8s-kv
version: 5a87a1fe426104552279cfe8e8de6b48e537a2d9
subpackages:
- kv
- name: github.com/Sirupsen/logrus
version: 181d419aa9e2223811b824e8f0b4af96f9ba9302
version: 89742aefa4b206dcf400792f3bd35b542998eb3b
repo: https://github.com/sirupsen/logrus.git
vcs: git
- name: github.com/sirupsen/logrus
version: 89742aefa4b206dcf400792f3bd35b542998eb3b
- name: github.com/spf13/pflag
version: 9ff6c6923cfffbcd502984b8e0c80539a94968b7
- name: github.com/ugorji/go
@ -119,7 +127,7 @@ imports:
- name: github.com/urfave/negroni
version: fde5e16d32adc7ad637e9cd9ad21d4ebc6192535
- name: golang.org/x/crypto
version: 42ff06aea7c329876e5a0fe94acc96902accf0ad
version: 88e95fbb56610f02dbc78ebc3b207bec8cf56b86
subpackages:
- ssh/terminal
- name: golang.org/x/net
@ -191,7 +199,7 @@ imports:
- socket
- urlfetch
- name: google.golang.org/genproto
version: 09f6ed296fc66555a25fe4ce95173148778dfa85
version: 595979c8a7bf586b2d293fb42246bf91a0b893d9
subpackages:
- googleapis/api/annotations
- googleapis/iam/v1
@ -345,7 +353,7 @@ imports:
- util/homedir
- util/integer
- name: k8s.io/helm
version: 7cf31e8d9a026287041bae077b09165be247ae66
version: bbc1f71dc03afc5f00c6ac84b9308f8ecb4f39ac
subpackages:
- pkg/chartutil
- pkg/helm

View File

@ -12,7 +12,12 @@ import:
- internal
- package: github.com/Masterminds/semver
version: ^1.3.1
- package: github.com/sirupsen/logrus
version: master
- package: github.com/Sirupsen/logrus
repo: https://github.com/sirupsen/logrus.git
vcs: git
version: master
- package: github.com/docker/distribution
version: ^2.6.2
subpackages:

52
main.go
View File

@ -9,7 +9,10 @@ import (
netContext "golang.org/x/net/context"
"github.com/rusenask/keel/approvals"
"github.com/rusenask/keel/bot"
"github.com/rusenask/keel/cache/kubekv"
"github.com/rusenask/keel/constants"
"github.com/rusenask/keel/extension/notification"
"github.com/rusenask/keel/provider"
@ -21,6 +24,7 @@ import (
"github.com/rusenask/keel/trigger/poll"
"github.com/rusenask/keel/trigger/pubsub"
"github.com/rusenask/keel/types"
"github.com/rusenask/keel/util/codecs"
"github.com/rusenask/keel/version"
// extensions
@ -36,6 +40,8 @@ const (
EnvTriggerPoll = "POLL" // set to 1 or something to enable poll trigger
EnvProjectID = "PROJECT_ID"
EnvNamespace = "NAMESPACE" // Keel's namespace
EnvHelmProvider = "HELM_PROVIDER" // helm provider
EnvHelmTillerAddress = "TILLER_ADDRESS" // helm provider
)
@ -58,7 +64,7 @@ func main() {
"version": ver.Version,
"go_version": ver.GoVersion,
"arch": ver.Arch,
}).Info("keel starting..")
}).Info("keel starting...")
if os.Getenv(EnvDebug) != "" {
log.SetLevel(log.DebugLevel)
@ -95,14 +101,33 @@ func main() {
}).Fatal("main: failed to create kubernetes implementer")
}
keelsNamespace := constants.DefaultNamespace
if os.Getenv(EnvNamespace) != "" {
keelsNamespace = os.Getenv(EnvNamespace)
}
kkv, err := kubekv.New(implementer.ConfigMaps(keelsNamespace), "approvals")
if err != nil {
log.WithFields(log.Fields{
"error": err,
"namespace": keelsNamespace,
}).Fatal("main: failed to initialise kube-kv")
}
serializer := codecs.DefaultSerializer()
// mem := memory.NewMemoryCache(24*time.Hour, 24*time.Hour, 1*time.Minute)
approvalsManager := approvals.New(kkv, serializer)
go approvalsManager.StartExpiryService(ctx)
// setting up providers
providers := setupProviders(implementer, sender)
providers := setupProviders(implementer, sender, approvalsManager)
secretsGetter := secrets.NewGetter(implementer)
teardownTriggers := setupTriggers(ctx, providers, secretsGetter)
teardownTriggers := setupTriggers(ctx, providers, secretsGetter, approvalsManager)
teardownBot, err := setupBot(implementer)
teardownBot, err := setupBot(implementer, approvalsManager)
if err != nil {
log.WithFields(log.Fields{
"error": err,
@ -141,10 +166,10 @@ func main() {
// setupProviders - setting up available providers. New providers should be initialised here and added to
// provider map
func setupProviders(k8sImplementer kubernetes.Implementer, sender notification.Sender) (providers provider.Providers) {
func setupProviders(k8sImplementer kubernetes.Implementer, sender notification.Sender, approvalsManager approvals.Manager) (providers provider.Providers) {
var enabledProviders []provider.Provider
k8sProvider, err := kubernetes.NewProvider(k8sImplementer, sender)
k8sProvider, err := kubernetes.NewProvider(k8sImplementer, sender, approvalsManager)
if err != nil {
log.WithFields(log.Fields{
"error": err,
@ -156,18 +181,18 @@ func setupProviders(k8sImplementer kubernetes.Implementer, sender notification.S
if os.Getenv(EnvHelmProvider) == "1" {
tillerAddr := os.Getenv(EnvHelmTillerAddress)
helmImplementer := helm.NewHelmImplementer(tillerAddr)
helmProvider := helm.NewProvider(helmImplementer, sender)
helmProvider := helm.NewProvider(helmImplementer, sender, approvalsManager)
go helmProvider.Start()
enabledProviders = append(enabledProviders, helmProvider)
}
providers = provider.New(enabledProviders)
providers = provider.New(enabledProviders, approvalsManager)
return providers
}
func setupBot(k8sImplementer kubernetes.Implementer) (teardown func(), err error) {
func setupBot(k8sImplementer kubernetes.Implementer, approvalsManager approvals.Manager) (teardown func(), err error) {
if os.Getenv(constants.EnvSlackToken) != "" {
botName := "keel"
@ -177,7 +202,7 @@ func setupBot(k8sImplementer kubernetes.Implementer) (teardown func(), err error
}
token := os.Getenv(constants.EnvSlackToken)
slackBot := bot.New(botName, token, k8sImplementer)
slackBot := bot.New(botName, token, k8sImplementer, approvalsManager)
ctx, cancel := context.WithCancel(context.Background())
@ -200,12 +225,13 @@ func setupBot(k8sImplementer kubernetes.Implementer) (teardown func(), err error
// setupTriggers - setting up triggers. New triggers should be added to this function. Each trigger
// should go through all providers (or not if there is a reason) and submit events)
func setupTriggers(ctx context.Context, providers provider.Providers, secretsGetter secrets.Getter) (teardown func()) {
func setupTriggers(ctx context.Context, providers provider.Providers, secretsGetter secrets.Getter, approvalsManager approvals.Manager) (teardown func()) {
// setting up generic http webhook server
whs := http.NewTriggerServer(&http.Opts{
Port: types.KeelDefaultPort,
Providers: providers,
Port: types.KeelDefaultPort,
Providers: providers,
ApprovalManager: approvalsManager,
})
go whs.Start()

View File

@ -0,0 +1,75 @@
package helm
import (
"fmt"
"time"
"github.com/rusenask/keel/cache"
"github.com/rusenask/keel/types"
log "github.com/Sirupsen/logrus"
)
// namespace/release name/version
func getIdentifier(namespace, name, version string) string {
return namespace + "/" + name + ":" + version
}
func (p *Provider) checkForApprovals(event *types.Event, plans []*UpdatePlan) (approvedPlans []*UpdatePlan) {
approvedPlans = []*UpdatePlan{}
for _, plan := range plans {
approved, err := p.isApproved(event, plan)
if err != nil {
log.WithFields(log.Fields{
"error": err,
"release_name": plan.Name,
"namespace": plan.Namespace,
}).Error("provider.helm: failed to check approval status for deployment")
continue
}
if approved {
approvedPlans = append(approvedPlans, plan)
}
}
return approvedPlans
}
func (p *Provider) isApproved(event *types.Event, plan *UpdatePlan) (bool, error) {
if plan.Config.Approvals == 0 {
return true, nil
}
identifier := getIdentifier(plan.Namespace, plan.Name, plan.NewVersion)
// checking for existing approval
existing, err := p.approvalManager.Get(identifier)
if err != nil {
if err == cache.ErrNotFound {
// creating new one
approval := &types.Approval{
Provider: types.ProviderTypeHelm,
Identifier: identifier,
Event: event,
CurrentVersion: plan.CurrentVersion,
NewVersion: plan.NewVersion,
VotesRequired: plan.Config.Approvals,
VotesReceived: 0,
Rejected: false,
Deadline: time.Now().Add(time.Duration(plan.Config.ApprovalDeadline) * time.Hour),
}
approval.Message = fmt.Sprintf("New image is available for release %s/%s (%s).",
plan.Namespace,
plan.Name,
approval.Delta(),
)
return false, p.approvalManager.Create(approval)
}
return false, err
}
return existing.Status() == types.ApprovalStatusApproved, nil
}

View File

@ -5,6 +5,7 @@ import (
"strings"
"time"
"github.com/rusenask/keel/approvals"
"github.com/rusenask/keel/types"
"github.com/rusenask/keel/util/image"
"github.com/rusenask/keel/util/version"
@ -37,11 +38,18 @@ type UpdatePlan struct {
Namespace string
Name string
Config *KeelChartConfig
// chart
Chart *hapi_chart.Chart
// values to update path=value
Values map[string]string
// Current (last seen cluster version)
CurrentVersion string
// New version that's already in the deployment
NewVersion string
}
// keel:
@ -62,10 +70,12 @@ type Root struct {
// KeelChartConfig - keel related configuration taken from values.yaml
type KeelChartConfig struct {
Policy types.PolicyType `json:"policy"`
Trigger types.TriggerType `json:"trigger"`
PollSchedule string `json:"pollSchedule"`
Images []ImageDetails `json:"images"`
Policy types.PolicyType `json:"policy"`
Trigger types.TriggerType `json:"trigger"`
PollSchedule string `json:"pollSchedule"`
Approvals int `json:"approvals"` // Minimum required approvals
ApprovalDeadline int `json:"approvalDeadline"` // Deadline in hours
Images []ImageDetails `json:"images"`
}
// ImageDetails - image details
@ -80,17 +90,20 @@ type Provider struct {
sender notification.Sender
approvalManager approvals.Manager
events chan *types.Event
stop chan struct{}
}
// NewProvider - create new Helm provider
func NewProvider(implementer Implementer, sender notification.Sender) *Provider {
func NewProvider(implementer Implementer, sender notification.Sender, approvalManager approvals.Manager) *Provider {
return &Provider{
implementer: implementer,
sender: sender,
events: make(chan *types.Event, 100),
stop: make(chan struct{}),
implementer: implementer,
approvalManager: approvalManager,
sender: sender,
events: make(chan *types.Event, 100),
stop: make(chan struct{}),
}
}
@ -206,7 +219,9 @@ func (p *Provider) processEvent(event *types.Event) (err error) {
return err
}
return p.applyPlans(plans)
approved := p.checkForApprovals(event, plans)
return p.applyPlans(approved)
}
func (p *Provider) createUpdatePlans(event *types.Event) ([]*UpdatePlan, error) {
@ -228,7 +243,7 @@ func (p *Provider) createUpdatePlans(event *types.Event) ([]*UpdatePlan, error)
"error": err,
"deployment": release.Name,
"namespace": release.Namespace,
}).Error("provider.kubernetes: got error while checking unversioned release")
}).Error("provider.helm: got error while checking unversioned release")
continue
}
@ -266,7 +281,7 @@ func (p *Provider) applyPlans(plans []*UpdatePlan) error {
p.sender.Send(types.EventNotification{
Name: "update release",
Message: fmt.Sprintf("Preparing to update release %s/%s (%s)", plan.Namespace, plan.Name, strings.Join(mapToSlice(plan.Values), ", ")),
Message: fmt.Sprintf("Preparing to update release %s/%s %s->%s (%s)", plan.Namespace, plan.Name, plan.CurrentVersion, plan.NewVersion, strings.Join(mapToSlice(plan.Values), ", ")),
CreatedAt: time.Now(),
Type: types.NotificationPreReleaseUpdate,
Level: types.LevelDebug,
@ -282,7 +297,7 @@ func (p *Provider) applyPlans(plans []*UpdatePlan) error {
p.sender.Send(types.EventNotification{
Name: "update release",
Message: fmt.Sprintf("Release update feailed %s/%s (%s), error: %s", plan.Namespace, plan.Name, strings.Join(mapToSlice(plan.Values), ", "), err),
Message: fmt.Sprintf("Release update feailed %s/%s %s->%s (%s), error: %s", plan.Namespace, plan.Name, plan.CurrentVersion, plan.NewVersion, strings.Join(mapToSlice(plan.Values), ", "), err),
CreatedAt: time.Now(),
Type: types.NotificationReleaseUpdate,
Level: types.LevelError,
@ -292,7 +307,7 @@ func (p *Provider) applyPlans(plans []*UpdatePlan) error {
p.sender.Send(types.EventNotification{
Name: "update release",
Message: fmt.Sprintf("Successfully updated release %s/%s (%s)", plan.Namespace, plan.Name, strings.Join(mapToSlice(plan.Values), ", ")),
Message: fmt.Sprintf("Successfully updated release %s/%s %s->%s (%s)", plan.Namespace, plan.Name, plan.CurrentVersion, plan.NewVersion, strings.Join(mapToSlice(plan.Values), ", ")),
CreatedAt: time.Now(),
Type: types.NotificationReleaseUpdate,
Level: types.LevelSuccess,

View File

@ -3,10 +3,14 @@ package helm
import (
"reflect"
"testing"
"time"
"github.com/ghodss/yaml"
"github.com/rusenask/keel/approvals"
"github.com/rusenask/keel/cache/memory"
"github.com/rusenask/keel/extension/notification"
"github.com/rusenask/keel/types"
"github.com/rusenask/keel/util/codecs"
"k8s.io/helm/pkg/chartutil"
"k8s.io/helm/pkg/helm"
"k8s.io/helm/pkg/proto/hapi/chart"
@ -14,6 +18,12 @@ import (
rls "k8s.io/helm/pkg/proto/hapi/services"
)
func approver() *approvals.DefaultManager {
cache := memory.NewMemoryCache(10*time.Minute, 10*time.Minute, 10*time.Minute)
return approvals.New(cache, codecs.DefaultSerializer())
}
type fakeSender struct {
sentEvent types.EventNotification
}
@ -166,7 +176,7 @@ keel:
},
}
prov := NewProvider(fakeImpl, &fakeSender{})
prov := NewProvider(fakeImpl, &fakeSender{}, approver())
tracked, _ := prov.TrackedImages()
@ -214,7 +224,7 @@ keel:
},
}
prov := NewProvider(fakeImpl, &fakeSender{})
prov := NewProvider(fakeImpl, &fakeSender{}, approver())
tracked, _ := prov.TrackedImages()
@ -316,7 +326,7 @@ keel:
},
}
provider := NewProvider(fakeImpl, &fakeSender{})
provider := NewProvider(fakeImpl, &fakeSender{}, approver())
err := provider.processEvent(&types.Event{
Repository: types.Repository{

View File

@ -4,6 +4,8 @@ import (
"k8s.io/helm/pkg/helm"
"k8s.io/helm/pkg/proto/hapi/chart"
rls "k8s.io/helm/pkg/proto/hapi/services"
log "github.com/Sirupsen/logrus"
)
// TillerAddress - default tiller address
@ -26,6 +28,8 @@ type HelmImplementer struct {
func NewHelmImplementer(address string) *HelmImplementer {
if address == "" {
address = TillerAddress
} else {
log.Infof("provider.helm: tiller address '%s' supplied", address)
}
return &HelmImplementer{

View File

@ -73,6 +73,9 @@ func checkUnversionedRelease(repo *types.Repository, namespace, name string, cha
path, value := getUnversionedPlanValues(repo.Tag, imageRef, &imageDetails)
plan.Values[path] = value
plan.NewVersion = repo.Tag
plan.CurrentVersion = imageRef.Tag()
plan.Config = keelCfg
shouldUpdateRelease = true
}

View File

@ -77,10 +77,23 @@ keel:
config: &hapi_chart.Config{Raw: ""},
},
wantPlan: &UpdatePlan{
Namespace: "default",
Name: "release-1",
Chart: helloWorldChart,
Values: map[string]string{"image.tag": "latest"}},
Namespace: "default",
Name: "release-1",
Chart: helloWorldChart,
Values: map[string]string{"image.tag": "latest"},
CurrentVersion: "1.1.0",
NewVersion: "latest",
Config: &KeelChartConfig{
Policy: types.PolicyTypeForce,
Trigger: types.TriggerTypePoll,
Images: []ImageDetails{
ImageDetails{
RepositoryPath: "image.repository",
TagPath: "image.tag",
},
},
},
},
wantShouldUpdateRelease: true,
wantErr: false,
},

View File

@ -69,7 +69,9 @@ func checkVersionedRelease(newVersion *types.Version, repo *types.Repository, na
if keelCfg.Policy == types.PolicyTypeForce || imageRef.Tag() == "latest" {
path, value := getPlanValues(newVersion, imageRef, &imageDetails)
plan.Values[path] = value
plan.NewVersion = newVersion.String()
plan.CurrentVersion = imageRef.Tag()
plan.Config = keelCfg
shouldUpdateRelease = true
log.WithFields(log.Fields{
@ -125,7 +127,9 @@ func checkVersionedRelease(newVersion *types.Version, repo *types.Repository, na
if shouldUpdate {
path, value := getPlanValues(newVersion, imageRef, &imageDetails)
plan.Values[path] = value
plan.NewVersion = newVersion.String()
plan.CurrentVersion = currentVersion.String()
plan.Config = keelCfg
shouldUpdateRelease = true
log.WithFields(log.Fields{

View File

@ -139,7 +139,21 @@ image:
chart: helloWorldChart,
config: &hapi_chart.Config{Raw: ""},
},
wantPlan: &UpdatePlan{Namespace: "default", Name: "release-1", Chart: helloWorldChart, Values: map[string]string{"image.tag": "1.1.2"}},
wantPlan: &UpdatePlan{
Namespace: "default",
Name: "release-1",
Chart: helloWorldChart,
Values: map[string]string{"image.tag": "1.1.2"},
NewVersion: "1.1.2",
CurrentVersion: "1.1.0",
Config: &KeelChartConfig{
Policy: types.PolicyTypeAll,
Trigger: types.TriggerTypePoll,
Images: []ImageDetails{
ImageDetails{RepositoryPath: "image.repository", TagPath: "image.tag"},
},
},
},
wantShouldUpdateRelease: true,
wantErr: false,
},
@ -181,7 +195,21 @@ image:
chart: helloWorldNonSemverChart,
config: &hapi_chart.Config{Raw: ""},
},
wantPlan: &UpdatePlan{Namespace: "default", Name: "release-1", Chart: helloWorldNonSemverChart, Values: map[string]string{"image.tag": "1.1.0"}},
wantPlan: &UpdatePlan{
Namespace: "default",
Name: "release-1",
Chart: helloWorldNonSemverChart,
Values: map[string]string{"image.tag": "1.1.0"},
NewVersion: "1.1.0",
CurrentVersion: "alpha",
Config: &KeelChartConfig{
Policy: types.PolicyTypeForce,
Trigger: types.TriggerTypePoll,
Images: []ImageDetails{
ImageDetails{RepositoryPath: "image.repository", TagPath: "image.tag"},
},
},
},
wantShouldUpdateRelease: true,
wantErr: false,
},
@ -209,7 +237,21 @@ image:
chart: helloWorldNoTagChart,
config: &hapi_chart.Config{Raw: ""},
},
wantPlan: &UpdatePlan{Namespace: "default", Name: "release-1-no-tag", Chart: helloWorldNoTagChart, Values: map[string]string{"image.repository": "gcr.io/v2-namespace/hello-world:1.1.0"}},
wantPlan: &UpdatePlan{
Namespace: "default",
Name: "release-1-no-tag",
Chart: helloWorldNoTagChart,
Values: map[string]string{"image.repository": "gcr.io/v2-namespace/hello-world:1.1.0"},
NewVersion: "1.1.0",
CurrentVersion: "1.0.0",
Config: &KeelChartConfig{
Policy: types.PolicyTypeMajor,
Trigger: types.TriggerTypePoll,
Images: []ImageDetails{
ImageDetails{RepositoryPath: "image.repository"},
},
},
},
wantShouldUpdateRelease: true,
wantErr: false,
},

View File

@ -0,0 +1,102 @@
package kubernetes
import (
"fmt"
"strconv"
"time"
"github.com/rusenask/keel/cache"
"github.com/rusenask/keel/types"
log "github.com/Sirupsen/logrus"
)
func getIdentifier(namespace, name, version string) string {
return namespace + "/" + name + ":" + version
}
// checkForApprovals - filters out deployments and only passes forward approved ones
func (p *Provider) checkForApprovals(event *types.Event, plans []*UpdatePlan) (approvedPlans []*UpdatePlan) {
approvedPlans = []*UpdatePlan{}
for _, plan := range plans {
approved, err := p.isApproved(event, plan)
if err != nil {
log.WithFields(log.Fields{
"error": err,
"deployment": plan.Deployment.Name,
"namespace": plan.Deployment.Namespace,
}).Error("provider.kubernetes: failed to check approval status for deployment")
continue
}
if approved {
approvedPlans = append(approvedPlans, plan)
}
}
return approvedPlans
}
func (p *Provider) isApproved(event *types.Event, plan *UpdatePlan) (bool, error) {
labels := plan.Deployment.GetLabels()
minApprovalsStr, ok := labels[types.KeelMinimumApprovalsLabel]
if !ok {
// no approvals required - passing
return true, nil
}
minApprovals, err := strconv.Atoi(minApprovalsStr)
if err != nil {
return false, err
}
if minApprovals == 0 {
return true, nil
}
deadline := types.KeelApprovalDeadlineDefault
// deadline
deadlineStr, ok := labels[types.KeelApprovalDeadlineLabel]
if ok {
d, err := strconv.Atoi(deadlineStr)
if err == nil {
deadline = d
}
}
identifier := getIdentifier(plan.Deployment.Namespace, plan.Deployment.Name, plan.NewVersion)
// checking for existing approval
existing, err := p.approvalManager.Get(identifier)
if err != nil {
if err == cache.ErrNotFound {
// creating new one
approval := &types.Approval{
Provider: types.ProviderTypeKubernetes,
Identifier: identifier,
Event: event,
CurrentVersion: plan.CurrentVersion,
NewVersion: plan.NewVersion,
VotesRequired: minApprovals,
VotesReceived: 0,
Rejected: false,
Deadline: time.Now().Add(time.Duration(deadline) * time.Hour),
}
approval.Message = fmt.Sprintf("New image is available for deployment %s/%s (%s).",
plan.Deployment.Namespace,
plan.Deployment.Name,
approval.Delta(),
)
fmt.Println("requesting approval, ns: ", plan.Deployment.Namespace)
return false, p.approvalManager.Create(approval)
}
return false, err
}
return existing.Status() == types.ApprovalStatusApproved, nil
}

View File

@ -0,0 +1,153 @@
package kubernetes
import (
"testing"
"time"
"github.com/rusenask/keel/types"
meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/pkg/api/v1"
"k8s.io/client-go/pkg/apis/extensions/v1beta1"
)
func TestCheckRequestedApproval(t *testing.T) {
fp := &fakeImplementer{}
fp.namespaces = &v1.NamespaceList{
Items: []v1.Namespace{
v1.Namespace{
meta_v1.TypeMeta{},
meta_v1.ObjectMeta{Name: "xxxx"},
v1.NamespaceSpec{},
v1.NamespaceStatus{},
},
},
}
fp.deploymentList = &v1beta1.DeploymentList{
Items: []v1beta1.Deployment{
v1beta1.Deployment{
meta_v1.TypeMeta{},
meta_v1.ObjectMeta{
Name: "dep-1",
Namespace: "xxxx",
Labels: map[string]string{types.KeelPolicyLabel: "all", types.KeelMinimumApprovalsLabel: "1"},
},
v1beta1.DeploymentSpec{
Template: v1.PodTemplateSpec{
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
Image: "gcr.io/v2-namespace/hello-world:1.1.1",
},
},
},
},
},
v1beta1.DeploymentStatus{},
},
},
}
approver := approver()
provider, err := NewProvider(fp, &fakeSender{}, approver)
if err != nil {
t.Fatalf("failed to get provider: %s", err)
}
// creating "new version" event
repo := types.Repository{
Name: "gcr.io/v2-namespace/hello-world",
Tag: "1.1.2",
}
deps, err := provider.processEvent(&types.Event{Repository: repo})
if err != nil {
t.Errorf("failed to get deployments: %s", err)
}
if len(deps) != 0 {
t.Errorf("expected to find 0 updated deployment but found %d", len(deps))
}
// checking approvals
approval, err := provider.approvalManager.Get("xxxx/dep-1:1.1.2")
if err != nil {
t.Fatalf("failed to find approval, err: %s", err)
}
if approval.Provider != types.ProviderTypeKubernetes {
t.Errorf("wrong provider: %s", approval.Provider)
}
}
func TestApprovedCheck(t *testing.T) {
fp := &fakeImplementer{}
fp.namespaces = &v1.NamespaceList{
Items: []v1.Namespace{
v1.Namespace{
meta_v1.TypeMeta{},
meta_v1.ObjectMeta{Name: "xxxx"},
v1.NamespaceSpec{},
v1.NamespaceStatus{},
},
},
}
fp.deploymentList = &v1beta1.DeploymentList{
Items: []v1beta1.Deployment{
v1beta1.Deployment{
meta_v1.TypeMeta{},
meta_v1.ObjectMeta{
Name: "dep-1",
Namespace: "xxxx",
Labels: map[string]string{types.KeelPolicyLabel: "all", types.KeelMinimumApprovalsLabel: "1"},
},
v1beta1.DeploymentSpec{
Template: v1.PodTemplateSpec{
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
Image: "gcr.io/v2-namespace/hello-world:1.1.1",
},
},
},
},
},
v1beta1.DeploymentStatus{},
},
},
}
approver := approver()
provider, err := NewProvider(fp, &fakeSender{}, approver)
if err != nil {
t.Fatalf("failed to get provider: %s", err)
}
// approving event
err = provider.approvalManager.Create(&types.Approval{
Identifier: "xxxx/dep-1:1.1.2",
VotesReceived: 2,
VotesRequired: 2,
Deadline: time.Now().Add(10 * time.Second),
})
if err != nil {
t.Fatalf("failed to create approval: %s", err)
}
appr, _ := provider.approvalManager.Get("xxxx/dep-1:1.1.2")
if appr.Status() != types.ApprovalStatusApproved {
t.Fatalf("approval not approved")
}
// creating "new version" event
repo := types.Repository{
Name: "gcr.io/v2-namespace/hello-world",
Tag: "1.1.2",
}
deps, err := provider.processEvent(&types.Event{Repository: repo})
if err != nil {
t.Errorf("failed to get deployments: %s", err)
}
if len(deps) != 1 {
t.Errorf("expected to find 1 updated deployment but found %d", len(deps))
}
}

View File

@ -5,6 +5,7 @@ import (
meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
core_v1 "k8s.io/client-go/kubernetes/typed/core/v1"
"k8s.io/client-go/pkg/api/v1"
"k8s.io/client-go/pkg/apis/extensions/v1beta1"
"k8s.io/client-go/rest"
@ -21,6 +22,7 @@ type Implementer interface {
Update(deployment *v1beta1.Deployment) error
Secret(namespace, name string) (*v1.Secret, error)
Pods(namespace, labelSelector string) (*v1.PodList, error)
ConfigMaps(namespace string) core_v1.ConfigMapInterface
}
// KubernetesImplementer - default kubernetes client implementer, uses
@ -112,3 +114,8 @@ func (i *KubernetesImplementer) Secret(namespace, name string) (*v1.Secret, erro
func (i *KubernetesImplementer) Pods(namespace, labelSelector string) (*v1.PodList, error) {
return i.client.Pods(namespace).List(meta_v1.ListOptions{LabelSelector: labelSelector})
}
// ConfigMaps - returns an interface to config maps for a specified namespace
func (i *KubernetesImplementer) ConfigMaps(namespace string) core_v1.ConfigMapInterface {
return i.client.ConfigMaps(namespace)
}

View File

@ -11,6 +11,7 @@ import (
"k8s.io/client-go/pkg/api/v1"
"k8s.io/client-go/pkg/apis/extensions/v1beta1"
"github.com/rusenask/keel/approvals"
"github.com/rusenask/keel/extension/notification"
"github.com/rusenask/keel/types"
"github.com/rusenask/keel/util/image"
@ -31,23 +32,36 @@ const forceUpdateImageAnnotation = "keel.sh/update-image"
// forceUpdateResetTag - tag used to reset container to force pull image
const forceUpdateResetTag = "0.0.0"
// UpdatePlan - deployment update plan
type UpdatePlan struct {
// Updated deployment version
Deployment v1beta1.Deployment
// Current (last seen cluster version)
CurrentVersion string
// New version that's already in the deployment
NewVersion string
}
// Provider - kubernetes provider for auto update
type Provider struct {
implementer Implementer
sender notification.Sender
approvalManager approvals.Manager
events chan *types.Event
stop chan struct{}
}
// NewProvider - create new kubernetes based provider
func NewProvider(implementer Implementer, sender notification.Sender) (*Provider, error) {
func NewProvider(implementer Implementer, sender notification.Sender, approvalManager approvals.Manager) (*Provider, error) {
return &Provider{
implementer: implementer,
events: make(chan *types.Event, 100),
stop: make(chan struct{}),
sender: sender,
implementer: implementer,
approvalManager: approvalManager,
events: make(chan *types.Event, 100),
stop: make(chan struct{}),
sender: sender,
}, nil
}
@ -101,7 +115,7 @@ func (p *Provider) TrackedImages() ([]*types.TrackedImage, error) {
"schedule": schedule,
"deployment": deployment.Name,
"namespace": deployment.Namespace,
}).Error("trigger.poll.manager: failed to parse poll schedule, setting default schedule")
}).Error("provider.kubernetes: failed to parse poll schedule, setting default schedule")
schedule = types.KeelPollDefaultSchedule
}
} else {
@ -165,24 +179,30 @@ func (p *Provider) startInternal() error {
}
func (p *Provider) processEvent(event *types.Event) (updated []*v1beta1.Deployment, err error) {
impacted, err := p.impactedDeployments(&event.Repository)
plans, err := p.createUpdatePlans(&event.Repository)
if err != nil {
return nil, err
}
if len(impacted) == 0 {
if len(plans) == 0 {
log.WithFields(log.Fields{
"image": event.Repository.Name,
"tag": event.Repository.Tag,
}).Info("provider.kubernetes: no impacted deployments found for this event")
}).Info("provider.kubernetes: no plans for deployment updates found for this event")
return
}
return p.updateDeployments(impacted)
approvedPlans := p.checkForApprovals(event, plans)
return p.updateDeployments(approvedPlans)
}
func (p *Provider) updateDeployments(deployments []v1beta1.Deployment) (updated []*v1beta1.Deployment, err error) {
for _, deployment := range deployments {
// func (p *Provider) updateDeployments(deployments []v1beta1.Deployment) (updated []*v1beta1.Deployment, err error) {
func (p *Provider) updateDeployments(plans []*UpdatePlan) (updated []*v1beta1.Deployment, err error) {
// for _, deployment := range plans {
for _, plan := range plans {
deployment := plan.Deployment
reset, delta, err := checkForReset(deployment, p.implementer)
if err != nil {
@ -227,7 +247,7 @@ func (p *Provider) updateDeployments(deployments []v1beta1.Deployment) (updated
p.sender.Send(types.EventNotification{
Name: "preparing to update deployment after reset",
Message: fmt.Sprintf("Preparing to update deployment %s/%s (%s)", deployment.Namespace, deployment.Name, strings.Join(getImages(&refresh), ", ")),
Message: fmt.Sprintf("Preparing to update deployment %s/%s %s->%s (%s)", deployment.Namespace, deployment.Name, plan.CurrentVersion, plan.NewVersion, strings.Join(getImages(&refresh), ", ")),
CreatedAt: time.Now(),
Type: types.NotificationPreDeploymentUpdate,
Level: types.LevelDebug,
@ -243,7 +263,7 @@ func (p *Provider) updateDeployments(deployments []v1beta1.Deployment) (updated
p.sender.Send(types.EventNotification{
Name: "update deployment after",
Message: fmt.Sprintf("Deployment %s/%s update failed, error: %s", refresh.Namespace, refresh.Name, err),
Message: fmt.Sprintf("Deployment %s/%s update %s->%s failed, error: %s", refresh.Namespace, refresh.Name, plan.CurrentVersion, plan.NewVersion, err),
CreatedAt: time.Now(),
Type: types.NotificationDeploymentUpdate,
Level: types.LevelError,
@ -253,7 +273,7 @@ func (p *Provider) updateDeployments(deployments []v1beta1.Deployment) (updated
p.sender.Send(types.EventNotification{
Name: "update deployment after reset",
Message: fmt.Sprintf("Successfully updated deployment %s/%s (%s)", refresh.Namespace, refresh.Name, strings.Join(getImages(&refresh), ", ")),
Message: fmt.Sprintf("Successfully updated deployment %s/%s %s->%s (%s)", refresh.Namespace, refresh.Name, plan.CurrentVersion, plan.NewVersion, strings.Join(getImages(&refresh), ", ")),
CreatedAt: time.Now(),
Type: types.NotificationDeploymentUpdate,
Level: types.LevelSuccess,
@ -267,7 +287,7 @@ func (p *Provider) updateDeployments(deployments []v1beta1.Deployment) (updated
p.sender.Send(types.EventNotification{
Name: "preparing to update deployment",
Message: fmt.Sprintf("Preparing to update deployment %s/%s (%s)", deployment.Namespace, deployment.Name, strings.Join(getImages(&deployment), ", ")),
Message: fmt.Sprintf("Preparing to update deployment %s/%s %s->%s (%s)", deployment.Namespace, deployment.Name, plan.CurrentVersion, plan.NewVersion, strings.Join(getImages(&deployment), ", ")),
CreatedAt: time.Now(),
Type: types.NotificationPreDeploymentUpdate,
Level: types.LevelDebug,
@ -283,7 +303,7 @@ func (p *Provider) updateDeployments(deployments []v1beta1.Deployment) (updated
p.sender.Send(types.EventNotification{
Name: "update deployment",
Message: fmt.Sprintf("Deployment %s/%s update failed, error: %s", deployment.Namespace, deployment.Name, err),
Message: fmt.Sprintf("Deployment %s/%s update %s->%s failed, error: %s", deployment.Namespace, deployment.Name, plan.CurrentVersion, plan.NewVersion, err),
CreatedAt: time.Now(),
Type: types.NotificationDeploymentUpdate,
Level: types.LevelError,
@ -294,7 +314,7 @@ func (p *Provider) updateDeployments(deployments []v1beta1.Deployment) (updated
p.sender.Send(types.EventNotification{
Name: "update deployment",
Message: fmt.Sprintf("Successfully updated deployment %s/%s (%s)", deployment.Namespace, deployment.Name, strings.Join(getImages(&deployment), ", ")),
Message: fmt.Sprintf("Successfully updated deployment %s/%s %s->%s (%s)", deployment.Namespace, deployment.Name, plan.CurrentVersion, plan.NewVersion, strings.Join(getImages(&deployment), ", ")),
CreatedAt: time.Now(),
Type: types.NotificationDeploymentUpdate,
Level: types.LevelSuccess,
@ -402,8 +422,9 @@ func (p *Provider) getDeployment(namespace, name string) (*v1beta1.Deployment, e
return p.implementer.Deployment(namespace, name)
}
// gets impacted deployments by changed repository
func (p *Provider) impactedDeployments(repo *types.Repository) ([]v1beta1.Deployment, error) {
// createUpdatePlans - impacted deployments by changed repository
// func (p *Provider) impactedDeployments(repo *types.Repository) ([]v1beta1.Deployment, error) {
func (p *Provider) createUpdatePlans(repo *types.Repository) ([]*UpdatePlan, error) {
deploymentLists, err := p.deployments()
if err != nil {
@ -413,7 +434,8 @@ func (p *Provider) impactedDeployments(repo *types.Repository) ([]v1beta1.Deploy
return nil, err
}
impacted := []v1beta1.Deployment{}
// impacted := []v1beta1.Deployment{}
impacted := []*UpdatePlan{}
for _, deploymentList := range deploymentLists {
for _, deployment := range deploymentList.Items {

View File

@ -2,15 +2,43 @@ package kubernetes
import (
"testing"
"time"
"github.com/rusenask/keel/approvals"
"github.com/rusenask/keel/cache/memory"
"github.com/rusenask/keel/extension/notification"
"github.com/rusenask/keel/types"
"github.com/rusenask/keel/util/codecs"
meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
core_v1 "k8s.io/client-go/kubernetes/typed/core/v1"
"k8s.io/client-go/pkg/api/v1"
"k8s.io/client-go/pkg/apis/extensions/v1beta1"
)
type fakeProvider struct {
submitted []types.Event
images []*types.TrackedImage
}
func (p *fakeProvider) Submit(event types.Event) error {
p.submitted = append(p.submitted, event)
return nil
}
func (p *fakeProvider) TrackedImages() ([]*types.TrackedImage, error) {
return p.images, nil
}
func (p *fakeProvider) List() []string {
return []string{"fakeprovider"}
}
func (p *fakeProvider) Stop() {
return
}
func (p *fakeProvider) GetName() string {
return "fp"
}
type fakeImplementer struct {
namespaces *v1.NamespaceList
deployment *v1beta1.Deployment
@ -49,6 +77,10 @@ func (i *fakeImplementer) Pods(namespace, labelSelector string) (*v1.PodList, er
return i.podList, nil
}
func (i *fakeImplementer) ConfigMaps(namespace string) core_v1.ConfigMapInterface {
return nil
}
type fakeSender struct {
sentEvent types.EventNotification
}
@ -62,6 +94,12 @@ func (s *fakeSender) Send(event types.EventNotification) error {
return nil
}
func approver() *approvals.DefaultManager {
cache := memory.NewMemoryCache(10*time.Minute, 10*time.Minute, 10*time.Minute)
return approvals.New(cache, codecs.DefaultSerializer())
}
func TestGetNamespaces(t *testing.T) {
fi := &fakeImplementer{
namespaces: &v1.NamespaceList{
@ -76,7 +114,7 @@ func TestGetNamespaces(t *testing.T) {
},
}
provider, err := NewProvider(fi, &fakeSender{})
provider, err := NewProvider(fi, &fakeSender{}, approver())
if err != nil {
t.Fatalf("failed to get provider: %s", err)
}
@ -135,7 +173,7 @@ func TestGetDeployments(t *testing.T) {
},
}
provider, err := NewProvider(fp, &fakeSender{})
provider, err := NewProvider(fp, &fakeSender{}, approver())
if err != nil {
t.Fatalf("failed to get provider: %s", err)
}
@ -210,7 +248,7 @@ func TestGetImpacted(t *testing.T) {
},
}
provider, err := NewProvider(fp, &fakeSender{})
provider, err := NewProvider(fp, &fakeSender{}, approver())
if err != nil {
t.Fatalf("failed to get provider: %s", err)
}
@ -221,17 +259,17 @@ func TestGetImpacted(t *testing.T) {
Tag: "1.1.2",
}
deps, err := provider.impactedDeployments(repo)
plans, err := provider.createUpdatePlans(repo)
if err != nil {
t.Errorf("failed to get deployments: %s", err)
}
if len(deps) != 1 {
t.Errorf("expected to find 1 deployment but found %d", len(deps))
if len(plans) != 1 {
t.Errorf("expected to find 1 deployment update plan but found %d", len(plans))
}
found := false
for _, c := range deps[0].Spec.Template.Spec.Containers {
for _, c := range plans[0].Deployment.Spec.Template.Spec.Containers {
containerImageName := versionreg.ReplaceAllString(c.Image, "")
@ -303,7 +341,7 @@ func TestProcessEvent(t *testing.T) {
},
}
provider, err := NewProvider(fp, &fakeSender{})
provider, err := NewProvider(fp, &fakeSender{}, approver())
if err != nil {
t.Fatalf("failed to get provider: %s", err)
}
@ -361,7 +399,7 @@ func TestProcessEventBuildNumber(t *testing.T) {
},
}
provider, err := NewProvider(fp, &fakeSender{})
provider, err := NewProvider(fp, &fakeSender{}, approver())
if err != nil {
t.Fatalf("failed to get provider: %s", err)
}
@ -443,7 +481,7 @@ func TestGetImpactedTwoContainersInSameDeployment(t *testing.T) {
},
}
provider, err := NewProvider(fp, &fakeSender{})
provider, err := NewProvider(fp, &fakeSender{}, approver())
if err != nil {
t.Fatalf("failed to get provider: %s", err)
}
@ -454,17 +492,17 @@ func TestGetImpactedTwoContainersInSameDeployment(t *testing.T) {
Tag: "1.1.2",
}
deps, err := provider.impactedDeployments(repo)
plans, err := provider.createUpdatePlans(repo)
if err != nil {
t.Errorf("failed to get deployments: %s", err)
}
if len(deps) != 1 {
t.Errorf("expected to find 1 deployment but found %d", len(deps))
if len(plans) != 1 {
t.Errorf("expected to find 1 deployment but found %d", len(plans))
}
found := false
for _, c := range deps[0].Spec.Template.Spec.Containers {
for _, c := range plans[0].Deployment.Spec.Template.Spec.Containers {
containerImageName := versionreg.ReplaceAllString(c.Image, "")
@ -540,7 +578,7 @@ func TestGetImpactedTwoSameContainersInSameDeployment(t *testing.T) {
},
}
provider, err := NewProvider(fp, &fakeSender{})
provider, err := NewProvider(fp, &fakeSender{}, approver())
if err != nil {
t.Fatalf("failed to get provider: %s", err)
}
@ -551,17 +589,17 @@ func TestGetImpactedTwoSameContainersInSameDeployment(t *testing.T) {
Tag: "1.1.2",
}
deps, err := provider.impactedDeployments(repo)
plans, err := provider.createUpdatePlans(repo)
if err != nil {
t.Errorf("failed to get deployments: %s", err)
}
if len(deps) != 1 {
t.Errorf("expected to find 1 deployment but found %d", len(deps))
if len(plans) != 1 {
t.Errorf("expected to find 1 deployment but found %d", len(plans))
}
found := false
for _, c := range deps[0].Spec.Template.Spec.Containers {
for _, c := range plans[0].Deployment.Spec.Template.Spec.Containers {
containerImageName := versionreg.ReplaceAllString(c.Image, "")
@ -635,7 +673,7 @@ func TestGetImpactedUntaggedImage(t *testing.T) {
},
}
provider, err := NewProvider(fp, &fakeSender{})
provider, err := NewProvider(fp, &fakeSender{}, approver())
if err != nil {
t.Fatalf("failed to get provider: %s", err)
}
@ -646,17 +684,17 @@ func TestGetImpactedUntaggedImage(t *testing.T) {
Tag: "1.1.2",
}
deps, err := provider.impactedDeployments(repo)
plans, err := provider.createUpdatePlans(repo)
if err != nil {
t.Errorf("failed to get deployments: %s", err)
}
if len(deps) != 1 {
t.Errorf("expected to find 1 deployment but found %d", len(deps))
if len(plans) != 1 {
t.Errorf("expected to find 1 deployment but found %d", len(plans))
}
found := false
for _, c := range deps[0].Spec.Template.Spec.Containers {
for _, c := range plans[0].Deployment.Spec.Template.Spec.Containers {
containerImageName := versionreg.ReplaceAllString(c.Image, "")
@ -731,7 +769,7 @@ func TestGetImpactedUntaggedOneImage(t *testing.T) {
},
}
provider, err := NewProvider(fp, &fakeSender{})
provider, err := NewProvider(fp, &fakeSender{}, approver())
if err != nil {
t.Fatalf("failed to get provider: %s", err)
}
@ -742,22 +780,24 @@ func TestGetImpactedUntaggedOneImage(t *testing.T) {
Tag: "1.1.2",
}
deps, err := provider.impactedDeployments(repo)
plans, err := provider.createUpdatePlans(repo)
if err != nil {
t.Errorf("failed to get deployments: %s", err)
}
if len(deps) != 2 {
t.Errorf("expected to find 2 deployment but found %d", len(deps))
if len(plans) != 2 {
t.Fatalf("expected to find 2 deployment but found %d", len(plans))
}
found := false
for _, c := range deps[0].Spec.Template.Spec.Containers {
for _, plan := range plans {
for _, c := range plan.Deployment.Spec.Template.Spec.Containers {
containerImageName := versionreg.ReplaceAllString(c.Image, "")
containerImageName := versionreg.ReplaceAllString(c.Image, "")
if containerImageName == repo.Name {
found = true
if containerImageName == repo.Name {
found = true
}
}
}
@ -809,7 +849,7 @@ func TestTrackedImages(t *testing.T) {
},
}
provider, err := NewProvider(fp, &fakeSender{})
provider, err := NewProvider(fp, &fakeSender{}, approver())
if err != nil {
t.Fatalf("failed to get provider: %s", err)
}

View File

@ -11,7 +11,10 @@ import (
log "github.com/Sirupsen/logrus"
)
func (p *Provider) checkUnversionedDeployment(policy types.PolicyType, repo *types.Repository, deployment v1beta1.Deployment) (updated v1beta1.Deployment, shouldUpdateDeployment bool, err error) {
// func (p *Provider) checkUnversionedDeployment(policy types.PolicyType, repo *types.Repository, deployment v1beta1.Deployment) (updated v1beta1.Deployment, shouldUpdateDeployment bool, err error) {
func (p *Provider) checkUnversionedDeployment(policy types.PolicyType, repo *types.Repository, deployment v1beta1.Deployment) (updatePlan *UpdatePlan, shouldUpdateDeployment bool, err error) {
updatePlan = &UpdatePlan{}
eventRepoRef, err := image.Parse(repo.Name)
if err != nil {
return
@ -82,6 +85,10 @@ func (p *Provider) checkUnversionedDeployment(policy types.PolicyType, repo *typ
deployment.SetAnnotations(annotations)
updatePlan.CurrentVersion = containerImageRef.Tag()
updatePlan.NewVersion = repo.Tag
updatePlan.Deployment = deployment
log.WithFields(log.Fields{
"parsed_image": containerImageRef.Remote(),
"raw_image_name": c.Image,
@ -92,5 +99,5 @@ func (p *Provider) checkUnversionedDeployment(policy types.PolicyType, repo *typ
}
return deployment, shouldUpdateDeployment, nil
return updatePlan, shouldUpdateDeployment, nil
}

View File

@ -4,6 +4,8 @@ import (
"reflect"
"testing"
"github.com/rusenask/keel/approvals"
"github.com/rusenask/keel/extension/notification"
"github.com/rusenask/keel/types"
meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/pkg/api/v1"
@ -12,9 +14,11 @@ import (
func TestProvider_checkUnversionedDeployment(t *testing.T) {
type fields struct {
implementer Implementer
events chan *types.Event
stop chan struct{}
implementer Implementer
sender notification.Sender
approvalManager approvals.Manager
events chan *types.Event
stop chan struct{}
}
type args struct {
policy types.PolicyType
@ -25,7 +29,7 @@ func TestProvider_checkUnversionedDeployment(t *testing.T) {
name string
fields fields
args args
wantUpdated v1beta1.Deployment
wantUpdatePlan *UpdatePlan
wantShouldUpdateDeployment bool
wantErr bool
}{
@ -56,26 +60,30 @@ func TestProvider_checkUnversionedDeployment(t *testing.T) {
v1beta1.DeploymentStatus{},
},
},
wantUpdated: v1beta1.Deployment{
meta_v1.TypeMeta{},
meta_v1.ObjectMeta{
Name: "dep-1",
Namespace: "xxxx",
Annotations: map[string]string{forceUpdateImageAnnotation: "gcr.io/v2-namespace/hello-world:latest"},
Labels: map[string]string{types.KeelPolicyLabel: "all"},
},
v1beta1.DeploymentSpec{
Template: v1.PodTemplateSpec{
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
Image: "gcr.io/v2-namespace/hello-world:latest",
wantUpdatePlan: &UpdatePlan{
Deployment: v1beta1.Deployment{
meta_v1.TypeMeta{},
meta_v1.ObjectMeta{
Name: "dep-1",
Namespace: "xxxx",
Annotations: map[string]string{forceUpdateImageAnnotation: "gcr.io/v2-namespace/hello-world:latest"},
Labels: map[string]string{types.KeelPolicyLabel: "all"},
},
v1beta1.DeploymentSpec{
Template: v1.PodTemplateSpec{
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
Image: "gcr.io/v2-namespace/hello-world:latest",
},
},
},
},
},
v1beta1.DeploymentStatus{},
},
v1beta1.DeploymentStatus{},
NewVersion: "latest",
CurrentVersion: "latest",
},
wantShouldUpdateDeployment: true,
wantErr: false,
@ -107,26 +115,8 @@ func TestProvider_checkUnversionedDeployment(t *testing.T) {
v1beta1.DeploymentStatus{},
},
},
wantUpdated: v1beta1.Deployment{
meta_v1.TypeMeta{},
meta_v1.ObjectMeta{
Name: "dep-1",
Namespace: "xxxx",
Annotations: map[string]string{},
Labels: map[string]string{types.KeelPolicyLabel: "all"},
},
v1beta1.DeploymentSpec{
Template: v1.PodTemplateSpec{
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
Image: "gcr.io/v2-namespace/goodbye-world:earliest",
},
},
},
},
},
v1beta1.DeploymentStatus{},
wantUpdatePlan: &UpdatePlan{
Deployment: v1beta1.Deployment{},
},
wantShouldUpdateDeployment: false,
wantErr: false,
@ -158,26 +148,30 @@ func TestProvider_checkUnversionedDeployment(t *testing.T) {
v1beta1.DeploymentStatus{},
},
},
wantUpdated: v1beta1.Deployment{
meta_v1.TypeMeta{},
meta_v1.ObjectMeta{
Name: "dep-1",
Namespace: "xxxx",
Annotations: map[string]string{forceUpdateImageAnnotation: "karolisr/keel:0.2.0"},
Labels: map[string]string{types.KeelPolicyLabel: "force"},
},
v1beta1.DeploymentSpec{
Template: v1.PodTemplateSpec{
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
Image: "karolisr/keel:0.2.0",
wantUpdatePlan: &UpdatePlan{
Deployment: v1beta1.Deployment{
meta_v1.TypeMeta{},
meta_v1.ObjectMeta{
Name: "dep-1",
Namespace: "xxxx",
Annotations: map[string]string{forceUpdateImageAnnotation: "karolisr/keel:0.2.0"},
Labels: map[string]string{types.KeelPolicyLabel: "force"},
},
v1beta1.DeploymentSpec{
Template: v1.PodTemplateSpec{
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
Image: "karolisr/keel:0.2.0",
},
},
},
},
},
v1beta1.DeploymentStatus{},
},
v1beta1.DeploymentStatus{},
NewVersion: "0.2.0",
CurrentVersion: "latest",
},
wantShouldUpdateDeployment: true,
wantErr: false,
@ -186,17 +180,19 @@ func TestProvider_checkUnversionedDeployment(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p := &Provider{
implementer: tt.fields.implementer,
events: tt.fields.events,
stop: tt.fields.stop,
implementer: tt.fields.implementer,
sender: tt.fields.sender,
approvalManager: tt.fields.approvalManager,
events: tt.fields.events,
stop: tt.fields.stop,
}
gotUpdated, gotShouldUpdateDeployment, err := p.checkUnversionedDeployment(tt.args.policy, tt.args.repo, tt.args.deployment)
gotUpdatePlan, gotShouldUpdateDeployment, err := p.checkUnversionedDeployment(tt.args.policy, tt.args.repo, tt.args.deployment)
if (err != nil) != tt.wantErr {
t.Errorf("Provider.checkUnversionedDeployment() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(gotUpdated, tt.wantUpdated) {
t.Errorf("Provider.checkUnversionedDeployment() gotUpdated = %v, want %v", gotUpdated, tt.wantUpdated)
if !reflect.DeepEqual(gotUpdatePlan, tt.wantUpdatePlan) {
t.Errorf("Provider.checkUnversionedDeployment() gotUpdatePlan = %v, want %v", gotUpdatePlan, tt.wantUpdatePlan)
}
if gotShouldUpdateDeployment != tt.wantShouldUpdateDeployment {
t.Errorf("Provider.checkUnversionedDeployment() gotShouldUpdateDeployment = %v, want %v", gotShouldUpdateDeployment, tt.wantShouldUpdateDeployment)

View File

@ -14,7 +14,10 @@ import (
log "github.com/Sirupsen/logrus"
)
func (p *Provider) checkVersionedDeployment(newVersion *types.Version, policy types.PolicyType, repo *types.Repository, deployment v1beta1.Deployment) (updated v1beta1.Deployment, shouldUpdateDeployment bool, err error) {
// func (p *Provider) checkVersionedDeployment(newVersion *types.Version, policy types.PolicyType, repo *types.Repository, deployment v1beta1.Deployment) (updated v1beta1.Deployment, shouldUpdateDeployment bool, err error) {
func (p *Provider) checkVersionedDeployment(newVersion *types.Version, policy types.PolicyType, repo *types.Repository, deployment v1beta1.Deployment) (updatePlan *UpdatePlan, shouldUpdateDeployment bool, err error) {
updatePlan = &UpdatePlan{}
eventRepoRef, err := image.Parse(repo.Name)
if err != nil {
return
@ -88,6 +91,10 @@ func (p *Provider) checkVersionedDeployment(newVersion *types.Version, policy ty
"policy": policy,
}).Info("provider.kubernetes: impacted deployment container found")
updatePlan.CurrentVersion = conatinerImageRef.Tag()
updatePlan.NewVersion = newVersion.Original
updatePlan.Deployment = deployment
// success, moving to next container
continue
}
@ -148,6 +155,10 @@ func (p *Provider) checkVersionedDeployment(newVersion *types.Version, policy ty
}
deployment.SetAnnotations(annotations)
updatePlan.CurrentVersion = currentVersion.Original
updatePlan.NewVersion = newVersion.Original
updatePlan.Deployment = deployment
log.WithFields(log.Fields{
"parsed_image": conatinerImageRef.Remote(),
"raw_image_name": c.Image,
@ -158,7 +169,7 @@ func (p *Provider) checkVersionedDeployment(newVersion *types.Version, policy ty
}
}
return deployment, shouldUpdateDeployment, nil
return updatePlan, shouldUpdateDeployment, nil
}
func updateContainer(container v1.Container, ref *image.Reference, version string) v1.Container {

View File

@ -4,10 +4,13 @@ import (
"reflect"
"testing"
"github.com/rusenask/keel/approvals"
"github.com/rusenask/keel/extension/notification"
"github.com/rusenask/keel/types"
"github.com/rusenask/keel/util/version"
meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/pkg/api/v1"
"k8s.io/client-go/pkg/apis/extensions/v1beta1"
)
@ -22,9 +25,11 @@ func unsafeGetVersion(ver string) *types.Version {
func TestProvider_checkVersionedDeployment(t *testing.T) {
type fields struct {
implementer Implementer
events chan *types.Event
stop chan struct{}
implementer Implementer
sender notification.Sender
approvalManager approvals.Manager
events chan *types.Event
stop chan struct{}
}
type args struct {
newVersion *types.Version
@ -36,7 +41,7 @@ func TestProvider_checkVersionedDeployment(t *testing.T) {
name string
fields fields
args args
wantUpdated v1beta1.Deployment
wantUpdatePlan *UpdatePlan
wantShouldUpdateDeployment bool
wantErr bool
}{
@ -68,26 +73,30 @@ func TestProvider_checkVersionedDeployment(t *testing.T) {
v1beta1.DeploymentStatus{},
},
},
wantUpdated: v1beta1.Deployment{
meta_v1.TypeMeta{},
meta_v1.ObjectMeta{
Name: "dep-1",
Namespace: "xxxx",
Annotations: map[string]string{},
Labels: map[string]string{types.KeelPolicyLabel: "all"},
},
v1beta1.DeploymentSpec{
Template: v1.PodTemplateSpec{
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
Image: "gcr.io/v2-namespace/hello-world:1.1.2",
wantUpdatePlan: &UpdatePlan{
Deployment: v1beta1.Deployment{
meta_v1.TypeMeta{},
meta_v1.ObjectMeta{
Name: "dep-1",
Namespace: "xxxx",
Annotations: map[string]string{},
Labels: map[string]string{types.KeelPolicyLabel: "all"},
},
v1beta1.DeploymentSpec{
Template: v1.PodTemplateSpec{
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
Image: "gcr.io/v2-namespace/hello-world:1.1.2",
},
},
},
},
},
v1beta1.DeploymentStatus{},
},
v1beta1.DeploymentStatus{},
NewVersion: "1.1.2",
CurrentVersion: "1.1.1",
},
wantShouldUpdateDeployment: true,
wantErr: false,
@ -120,26 +129,10 @@ func TestProvider_checkVersionedDeployment(t *testing.T) {
v1beta1.DeploymentStatus{},
},
},
wantUpdated: v1beta1.Deployment{
meta_v1.TypeMeta{},
meta_v1.ObjectMeta{
Name: "dep-1",
Namespace: "xxxx",
Annotations: map[string]string{},
Labels: map[string]string{types.KeelPolicyLabel: "all"},
},
v1beta1.DeploymentSpec{
Template: v1.PodTemplateSpec{
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
Image: "gcr.io/v2-namespace/hello-world:1.1.1",
},
},
},
},
},
v1beta1.DeploymentStatus{},
wantUpdatePlan: &UpdatePlan{
Deployment: v1beta1.Deployment{},
NewVersion: "",
CurrentVersion: "",
},
wantShouldUpdateDeployment: false,
wantErr: false,
@ -175,29 +168,33 @@ func TestProvider_checkVersionedDeployment(t *testing.T) {
v1beta1.DeploymentStatus{},
},
},
wantUpdated: v1beta1.Deployment{
meta_v1.TypeMeta{},
meta_v1.ObjectMeta{
Name: "dep-1",
Namespace: "xxxx",
Annotations: map[string]string{},
Labels: map[string]string{types.KeelPolicyLabel: "all"},
},
v1beta1.DeploymentSpec{
Template: v1.PodTemplateSpec{
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
Image: "gcr.io/v2-namespace/hello-world:1.1.2",
},
v1.Container{
Image: "yo-world:1.1.1",
wantUpdatePlan: &UpdatePlan{
Deployment: v1beta1.Deployment{
meta_v1.TypeMeta{},
meta_v1.ObjectMeta{
Name: "dep-1",
Namespace: "xxxx",
Annotations: map[string]string{},
Labels: map[string]string{types.KeelPolicyLabel: "all"},
},
v1beta1.DeploymentSpec{
Template: v1.PodTemplateSpec{
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
Image: "gcr.io/v2-namespace/hello-world:1.1.2",
},
v1.Container{
Image: "yo-world:1.1.1",
},
},
},
},
},
v1beta1.DeploymentStatus{},
},
v1beta1.DeploymentStatus{},
NewVersion: "1.1.2",
CurrentVersion: "1.1.1",
},
wantShouldUpdateDeployment: true,
wantErr: false,
@ -233,29 +230,33 @@ func TestProvider_checkVersionedDeployment(t *testing.T) {
v1beta1.DeploymentStatus{},
},
},
wantUpdated: v1beta1.Deployment{
meta_v1.TypeMeta{},
meta_v1.ObjectMeta{
Name: "dep-1",
Namespace: "xxxx",
Annotations: map[string]string{forceUpdateImageAnnotation: "gcr.io/v2-namespace/hello-world:1.1.2"},
Labels: map[string]string{types.KeelPolicyLabel: "force"},
},
v1beta1.DeploymentSpec{
Template: v1.PodTemplateSpec{
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
Image: "gcr.io/v2-namespace/hello-world:1.1.2",
},
v1.Container{
Image: "yo-world:1.1.1",
wantUpdatePlan: &UpdatePlan{
Deployment: v1beta1.Deployment{
meta_v1.TypeMeta{},
meta_v1.ObjectMeta{
Name: "dep-1",
Namespace: "xxxx",
Annotations: map[string]string{forceUpdateImageAnnotation: "gcr.io/v2-namespace/hello-world:1.1.2"},
Labels: map[string]string{types.KeelPolicyLabel: "force"},
},
v1beta1.DeploymentSpec{
Template: v1.PodTemplateSpec{
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
Image: "gcr.io/v2-namespace/hello-world:1.1.2",
},
v1.Container{
Image: "yo-world:1.1.1",
},
},
},
},
},
v1beta1.DeploymentStatus{},
},
v1beta1.DeploymentStatus{},
NewVersion: "1.1.2",
CurrentVersion: "latest",
},
wantShouldUpdateDeployment: true,
wantErr: false,
@ -264,17 +265,19 @@ func TestProvider_checkVersionedDeployment(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p := &Provider{
implementer: tt.fields.implementer,
events: tt.fields.events,
stop: tt.fields.stop,
implementer: tt.fields.implementer,
sender: tt.fields.sender,
approvalManager: tt.fields.approvalManager,
events: tt.fields.events,
stop: tt.fields.stop,
}
gotUpdated, gotShouldUpdateDeployment, err := p.checkVersionedDeployment(tt.args.newVersion, tt.args.policy, tt.args.repo, tt.args.deployment)
gotUpdatePlan, gotShouldUpdateDeployment, err := p.checkVersionedDeployment(tt.args.newVersion, tt.args.policy, tt.args.repo, tt.args.deployment)
if (err != nil) != tt.wantErr {
t.Errorf("Provider.checkVersionedDeployment() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(gotUpdated, tt.wantUpdated) {
t.Errorf("Provider.checkVersionedDeployment() gotUpdated = %v, want %v", gotUpdated, tt.wantUpdated)
if !reflect.DeepEqual(gotUpdatePlan, tt.wantUpdatePlan) {
t.Errorf("Provider.checkVersionedDeployment() gotUpdatePlan = %v, want %v", gotUpdatePlan, tt.wantUpdatePlan)
}
if gotShouldUpdateDeployment != tt.wantShouldUpdateDeployment {
t.Errorf("Provider.checkVersionedDeployment() gotShouldUpdateDeployment = %v, want %v", gotShouldUpdateDeployment, tt.wantShouldUpdateDeployment)

View File

@ -1,6 +1,9 @@
package provider
import (
"context"
"github.com/rusenask/keel/approvals"
"github.com/rusenask/keel/types"
log "github.com/Sirupsen/logrus"
@ -23,7 +26,7 @@ type Providers interface {
}
// New - new providers registry
func New(providers []Provider) *DefaultProviders {
func New(providers []Provider, approvalsManager approvals.Manager) *DefaultProviders {
pvs := make(map[string]Provider)
for _, p := range providers {
@ -31,14 +34,46 @@ func New(providers []Provider) *DefaultProviders {
log.Infof("provider.defaultProviders: provider '%s' registered", p.GetName())
}
return &DefaultProviders{
providers: pvs,
dp := &DefaultProviders{
providers: pvs,
approvalsManager: approvalsManager,
stopCh: make(chan struct{}),
}
// subscribing to approved events
// TODO: create Start() function for DefaultProviders
go dp.subscribeToApproved()
return dp
}
// DefaultProviders - default providers container
type DefaultProviders struct {
providers map[string]Provider
providers map[string]Provider
approvalsManager approvals.Manager
stopCh chan struct{}
}
func (p *DefaultProviders) subscribeToApproved() {
ctx, cancel := context.WithCancel(context.Background())
approvedCh, err := p.approvalsManager.SubscribeApproved(ctx)
if err != nil {
log.WithFields(log.Fields{
"error": err,
}).Fatal("provider.subscribeToApproved: failed to subscribe for approved reqs")
}
for {
select {
case approval := <-approvedCh:
p.Submit(*approval.Event)
case <-p.stopCh:
cancel()
return
}
}
}
// Submit - submit event to all providers
@ -51,7 +86,7 @@ func (p *DefaultProviders) Submit(event types.Event) error {
"provider": provider.GetName(),
"event": event.Repository,
"trigger": event.TriggerName,
}).Error("provider.defaultProviders: submit event failed")
}).Error("provider.Submit: submit event failed")
}
}

View File

@ -87,7 +87,7 @@ func (g *DefaultGetter) lookupSecrets(image *types.TrackedImage) ([]string, erro
"image": image.Image.Repository(),
"pod_selector": selector,
"secrets": podSecrets,
}).Info("secrets.defaultGetter.lookupSecrets: pod secrets found")
}).Debug("secrets.defaultGetter.lookupSecrets: pod secrets found")
secrets = append(secrets, podSecrets...)
}
@ -99,7 +99,7 @@ func (g *DefaultGetter) lookupSecrets(image *types.TrackedImage) ([]string, erro
"image": image.Image.Repository(),
"pod_selector": selector,
"pods_checked": len(podList.Items),
}).Info("secrets.defaultGetter.lookupSecrets: no secrets for image found")
}).Warn("secrets.defaultGetter.lookupSecrets: no secrets for image found")
}
return secrets, nil
@ -210,7 +210,7 @@ func (g *DefaultGetter) getCredentialsFromSecret(image *types.TrackedImage) (*ty
"provider": image.Provider,
"registry": image.Image.Registry(),
"image": image.Image.Repository(),
}).Info("secrets.defaultGetter: secret looked up successfully")
}).Debug("secrets.defaultGetter: secret looked up successfully")
return credentials, nil
}

View File

@ -0,0 +1,46 @@
package http
import (
"encoding/json"
"fmt"
"net/http"
"github.com/rusenask/keel/types"
)
func (s *TriggerServer) approvalsHandler(resp http.ResponseWriter, req *http.Request) {
// unknown lists all
approvals, err := s.approvalsManager.List()
if err != nil {
fmt.Fprintf(resp, "%s", err)
resp.WriteHeader(http.StatusInternalServerError)
return
}
if len(approvals) == 0 {
approvals = make([]*types.Approval, 0)
}
bts, err := json.Marshal(&approvals)
if err != nil {
fmt.Fprintf(resp, "%s", err)
resp.WriteHeader(http.StatusInternalServerError)
return
}
resp.Write(bts)
}
func (s *TriggerServer) approvalDeleteHandler(resp http.ResponseWriter, req *http.Request) {
identifier := getID(req)
err := s.approvalsManager.Delete(identifier)
if err != nil {
fmt.Fprintf(resp, "%s", err)
resp.WriteHeader(http.StatusInternalServerError)
return
}
resp.WriteHeader(http.StatusOK)
fmt.Fprintf(resp, identifier)
}

View File

@ -0,0 +1,113 @@
package http
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/rusenask/keel/approvals"
"github.com/rusenask/keel/cache/memory"
"github.com/rusenask/keel/provider"
"github.com/rusenask/keel/types"
"github.com/rusenask/keel/util/codecs"
)
func TestListApprovals(t *testing.T) {
fp := &fakeProvider{}
mem := memory.NewMemoryCache(100*time.Second, 100*time.Second, 10*time.Second)
am := approvals.New(mem, codecs.DefaultSerializer())
providers := provider.New([]provider.Provider{fp}, am)
srv := NewTriggerServer(&Opts{Providers: providers, ApprovalManager: am})
srv.registerRoutes(srv.router)
err := am.Create(&types.Approval{
Identifier: "123",
VotesRequired: 5,
NewVersion: "2.0.0",
CurrentVersion: "1.0.0",
})
if err != nil {
t.Fatalf("failed to create approval: %s", err)
}
// listing
req, err := http.NewRequest("GET", "/v1/approvals", nil)
if err != nil {
t.Fatalf("failed to create req: %s", err)
}
rec := httptest.NewRecorder()
srv.router.ServeHTTP(rec, req)
if rec.Code != 200 {
t.Errorf("unexpected status code: %d", rec.Code)
t.Log(rec.Body.String())
}
var approvals []*types.Approval
err = json.Unmarshal(rec.Body.Bytes(), &approvals)
if err != nil {
t.Fatalf("failed to unmarshal response into approvals: %s", err)
}
if len(approvals) != 1 {
t.Fatalf("expected to find 1 approval but found: %d", len(approvals))
}
if approvals[0].VotesRequired != 5 {
t.Errorf("unexpected votes required")
}
if approvals[0].NewVersion != "2.0.0" {
t.Errorf("unexpected new version: %s", approvals[0].NewVersion)
}
if approvals[0].CurrentVersion != "1.0.0" {
t.Errorf("unexpected current version: %s", approvals[0].CurrentVersion)
}
}
func TestDeleteApproval(t *testing.T) {
fp := &fakeProvider{}
mem := memory.NewMemoryCache(100*time.Second, 100*time.Second, 10*time.Second)
am := approvals.New(mem, codecs.DefaultSerializer())
providers := provider.New([]provider.Provider{fp}, am)
srv := NewTriggerServer(&Opts{Providers: providers, ApprovalManager: am})
srv.registerRoutes(srv.router)
err := am.Create(&types.Approval{
Identifier: "12345",
VotesRequired: 5,
NewVersion: "2.0.0",
CurrentVersion: "1.0.0",
})
if err != nil {
t.Fatalf("failed to create approval: %s", err)
}
// listing
req, err := http.NewRequest("DELETE", "/v1/approvals/12345", nil)
if err != nil {
t.Fatalf("failed to create req: %s", err)
}
rec := httptest.NewRecorder()
srv.router.ServeHTTP(rec, req)
if rec.Code != 200 {
t.Errorf("unexpected status code: %d", rec.Code)
t.Log(rec.Body.String())
}
_, err = am.Get("12345")
if err == nil {
t.Errorf("expected approval to be deleted")
}
}

View File

@ -3,9 +3,12 @@ package http
import (
"bytes"
"net/http"
"time"
"github.com/rusenask/keel/approvals"
"github.com/rusenask/keel/cache/memory"
"github.com/rusenask/keel/provider"
// "github.com/rusenask/keel/types"
"github.com/rusenask/keel/util/codecs"
"net/http/httptest"
"testing"
@ -41,7 +44,9 @@ var fakeRequest = `{
func TestDockerhubWebhookHandler(t *testing.T) {
fp := &fakeProvider{}
providers := provider.New([]provider.Provider{fp})
mem := memory.NewMemoryCache(100*time.Millisecond, 100*time.Millisecond, 10*time.Millisecond)
am := approvals.New(mem, codecs.DefaultSerializer())
providers := provider.New([]provider.Provider{fp}, am)
srv := NewTriggerServer(&Opts{Providers: providers})
srv.registerRoutes(srv.router)

View File

@ -10,6 +10,7 @@ import (
"github.com/gorilla/mux"
"github.com/urfave/negroni"
"github.com/rusenask/keel/approvals"
"github.com/rusenask/keel/provider"
"github.com/rusenask/keel/types"
"github.com/rusenask/keel/version"
@ -23,22 +24,26 @@ type Opts struct {
// available providers
Providers provider.Providers
ApprovalManager approvals.Manager
}
// TriggerServer - webhook trigger & healthcheck server
type TriggerServer struct {
providers provider.Providers
port int
server *http.Server
router *mux.Router
providers provider.Providers
approvalsManager approvals.Manager
port int
server *http.Server
router *mux.Router
}
// NewTriggerServer - create new HTTP trigger based server
func NewTriggerServer(opts *Opts) *TriggerServer {
return &TriggerServer{
port: opts.Port,
providers: opts.Providers,
router: mux.NewRouter(),
port: opts.Port,
providers: opts.Providers,
approvalsManager: opts.ApprovalManager,
router: mux.NewRouter(),
}
}
@ -66,7 +71,10 @@ func (s *TriggerServer) Stop() {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
s.server.Shutdown(ctx)
}
func getID(req *http.Request) string {
return mux.Vars(req)["id"]
}
func (s *TriggerServer) registerRoutes(mux *mux.Router) {
@ -74,6 +82,11 @@ func (s *TriggerServer) registerRoutes(mux *mux.Router) {
mux.HandleFunc("/healthz", s.healthHandler).Methods("GET", "OPTIONS")
// version handler
mux.HandleFunc("/version", s.versionHandler).Methods("GET", "OPTIONS")
// approvals
mux.HandleFunc("/v1/approvals", s.approvalsHandler).Methods("GET", "OPTIONS")
mux.HandleFunc("/v1/approvals/{id}", s.approvalDeleteHandler).Methods("DELETE", "OPTIONS")
// native webhooks handler
mux.HandleFunc("/v1/webhooks/native", s.nativeHandler).Methods("POST", "OPTIONS")

View File

@ -3,9 +3,13 @@ package http
import (
"bytes"
"net/http"
"time"
"github.com/rusenask/keel/approvals"
"github.com/rusenask/keel/cache/memory"
"github.com/rusenask/keel/provider"
"github.com/rusenask/keel/types"
"github.com/rusenask/keel/util/codecs"
"net/http/httptest"
"testing"
@ -37,7 +41,9 @@ func TestNativeWebhookHandler(t *testing.T) {
fp := &fakeProvider{}
providers := provider.New([]provider.Provider{fp})
mem := memory.NewMemoryCache(100*time.Millisecond, 100*time.Millisecond, 10*time.Millisecond)
am := approvals.New(mem, codecs.DefaultSerializer())
providers := provider.New([]provider.Provider{fp}, am)
srv := NewTriggerServer(&Opts{Providers: providers})
srv.registerRoutes(srv.router)
@ -64,7 +70,9 @@ func TestNativeWebhookHandler(t *testing.T) {
func TestNativeWebhookHandlerNoRepoName(t *testing.T) {
fp := &fakeProvider{}
providers := provider.New([]provider.Provider{fp})
mem := memory.NewMemoryCache(100*time.Millisecond, 100*time.Millisecond, 10*time.Millisecond)
am := approvals.New(mem, codecs.DefaultSerializer())
providers := provider.New([]provider.Provider{fp}, am)
srv := NewTriggerServer(&Opts{Providers: providers})
srv.registerRoutes(srv.router)

View File

@ -3,8 +3,12 @@ package http
import (
"bytes"
"net/http"
"time"
"github.com/rusenask/keel/approvals"
"github.com/rusenask/keel/cache/memory"
"github.com/rusenask/keel/provider"
"github.com/rusenask/keel/util/codecs"
"net/http/httptest"
"testing"
@ -25,7 +29,9 @@ var fakeQuayWebhook = `{
func TestQuayWebhookHandler(t *testing.T) {
fp := &fakeProvider{}
providers := provider.New([]provider.Provider{fp})
mem := memory.NewMemoryCache(100*time.Millisecond, 100*time.Millisecond, 10*time.Millisecond)
am := approvals.New(mem, codecs.DefaultSerializer())
providers := provider.New([]provider.Provider{fp}, am)
srv := NewTriggerServer(&Opts{Providers: providers})
srv.registerRoutes(srv.router)

View File

@ -2,9 +2,13 @@ package poll
import (
"context"
"time"
"github.com/rusenask/keel/approvals"
"github.com/rusenask/keel/cache/memory"
"github.com/rusenask/keel/provider"
"github.com/rusenask/keel/types"
"github.com/rusenask/keel/util/codecs"
"github.com/rusenask/keel/util/image"
"testing"
@ -39,7 +43,9 @@ func TestCheckDeployment(t *testing.T) {
},
},
}
providers := provider.New([]provider.Provider{fp})
mem := memory.NewMemoryCache(100*time.Millisecond, 100*time.Millisecond, 10*time.Millisecond)
am := approvals.New(mem, codecs.DefaultSerializer())
providers := provider.New([]provider.Provider{fp}, am)
// returning some sha
frc := &fakeRegistryClient{

View File

@ -2,10 +2,14 @@ package poll
import (
"testing"
"time"
"github.com/rusenask/keel/approvals"
"github.com/rusenask/keel/cache/memory"
"github.com/rusenask/keel/provider"
"github.com/rusenask/keel/registry"
"github.com/rusenask/keel/types"
"github.com/rusenask/keel/util/codecs"
"github.com/rusenask/keel/util/image"
)
@ -53,7 +57,9 @@ func (p *fakeProvider) TrackedImages() ([]*types.TrackedImage, error) {
func TestWatchTagJob(t *testing.T) {
fp := &fakeProvider{}
providers := provider.New([]provider.Provider{fp})
mem := memory.NewMemoryCache(100*time.Millisecond, 100*time.Millisecond, 10*time.Millisecond)
am := approvals.New(mem, codecs.DefaultSerializer())
providers := provider.New([]provider.Provider{fp}, am)
frc := &fakeRegistryClient{
digestToReturn: "sha256:0604af35299dd37ff23937d115d103532948b568a9dd8197d14c256a8ab8b0bb",
@ -96,7 +102,9 @@ func TestWatchTagJob(t *testing.T) {
func TestWatchTagJobLatest(t *testing.T) {
fp := &fakeProvider{}
providers := provider.New([]provider.Provider{fp})
mem := memory.NewMemoryCache(100*time.Millisecond, 100*time.Millisecond, 10*time.Millisecond)
am := approvals.New(mem, codecs.DefaultSerializer())
providers := provider.New([]provider.Provider{fp}, am)
frc := &fakeRegistryClient{
digestToReturn: "sha256:0604af35299dd37ff23937d115d103532948b568a9dd8197d14c256a8ab8b0bb",
@ -139,7 +147,9 @@ func TestWatchTagJobLatest(t *testing.T) {
func TestWatchAllTagsJob(t *testing.T) {
fp := &fakeProvider{}
providers := provider.New([]provider.Provider{fp})
mem := memory.NewMemoryCache(100*time.Millisecond, 100*time.Millisecond, 10*time.Millisecond)
am := approvals.New(mem, codecs.DefaultSerializer())
providers := provider.New([]provider.Provider{fp}, am)
frc := &fakeRegistryClient{
tagsToReturn: []string{"1.1.2", "1.1.3", "0.9.1"},
@ -171,7 +181,9 @@ func TestWatchAllTagsJob(t *testing.T) {
func TestWatchAllTagsJobCurrentLatest(t *testing.T) {
fp := &fakeProvider{}
providers := provider.New([]provider.Provider{fp})
mem := memory.NewMemoryCache(100*time.Millisecond, 100*time.Millisecond, 10*time.Millisecond)
am := approvals.New(mem, codecs.DefaultSerializer())
providers := provider.New([]provider.Provider{fp}, am)
frc := &fakeRegistryClient{
tagsToReturn: []string{"1.1.2", "1.1.3", "0.9.1"},

View File

@ -1,12 +1,16 @@
package pubsub
import (
"golang.org/x/net/context"
"sync"
"time"
"golang.org/x/net/context"
"github.com/rusenask/keel/approvals"
"github.com/rusenask/keel/cache/memory"
"github.com/rusenask/keel/provider"
"github.com/rusenask/keel/types"
"github.com/rusenask/keel/util/codecs"
"github.com/rusenask/keel/util/image"
"testing"
@ -62,7 +66,10 @@ func TestCheckDeployment(t *testing.T) {
},
},
}
providers := provider.New([]provider.Provider{fp})
mem := memory.NewMemoryCache(100*time.Millisecond, 100*time.Millisecond, 10*time.Millisecond)
am := approvals.New(mem, codecs.DefaultSerializer())
providers := provider.New([]provider.Provider{fp}, am)
fs := &fakeSubscriber{}
mng := &DefaultManager{

View File

@ -2,11 +2,15 @@ package pubsub
import (
"encoding/json"
"time"
"cloud.google.com/go/pubsub"
"golang.org/x/net/context"
"github.com/rusenask/keel/approvals"
"github.com/rusenask/keel/cache/memory"
"github.com/rusenask/keel/provider"
"github.com/rusenask/keel/util/codecs"
"testing"
)
@ -21,7 +25,9 @@ func fakeDoneFunc(id string, done bool) {
func TestCallback(t *testing.T) {
fp := &fakeProvider{}
providers := provider.New([]provider.Provider{fp})
mem := memory.NewMemoryCache(100*time.Millisecond, 100*time.Millisecond, 10*time.Millisecond)
am := approvals.New(mem, codecs.DefaultSerializer())
providers := provider.New([]provider.Provider{fp}, am)
sub := &PubsubSubscriber{disableAck: true, providers: providers}
dataMsg := &Message{Action: "INSERT", Tag: "gcr.io/v2-namespace/hello-world:1.1.1"}
@ -46,7 +52,9 @@ func TestCallback(t *testing.T) {
func TestCallbackTagNotSemver(t *testing.T) {
fp := &fakeProvider{}
providers := provider.New([]provider.Provider{fp})
mem := memory.NewMemoryCache(100*time.Millisecond, 100*time.Millisecond, 10*time.Millisecond)
am := approvals.New(mem, codecs.DefaultSerializer())
providers := provider.New([]provider.Provider{fp}, am)
sub := &PubsubSubscriber{disableAck: true, providers: providers}
dataMsg := &Message{Action: "INSERT", Tag: "gcr.io/stemnapp/alpine-website:latest"}
@ -72,7 +80,9 @@ func TestCallbackTagNotSemver(t *testing.T) {
func TestCallbackNoTag(t *testing.T) {
fp := &fakeProvider{}
providers := provider.New([]provider.Provider{fp})
mem := memory.NewMemoryCache(100*time.Millisecond, 100*time.Millisecond, 10*time.Millisecond)
am := approvals.New(mem, codecs.DefaultSerializer())
providers := provider.New([]provider.Provider{fp}, am)
sub := &PubsubSubscriber{disableAck: true, providers: providers}
dataMsg := &Message{Action: "INSERT", Tag: "gcr.io/stemnapp/alpine-website"}
@ -92,5 +102,4 @@ func TestCallbackNoTag(t *testing.T) {
if fp.submitted[0].Repository.Tag != "latest" {
t.Errorf("expected repo tag %s but got %s", "latest", fp.submitted[0].Repository.Tag)
}
}

View File

@ -13,6 +13,8 @@ var (
"PostProviderSubmitNotification": PostProviderSubmitNotification,
"NotificationPreDeploymentUpdate": NotificationPreDeploymentUpdate,
"NotificationDeploymentUpdate": NotificationDeploymentUpdate,
"NotificationPreReleaseUpdate": NotificationPreReleaseUpdate,
"NotificationReleaseUpdate": NotificationReleaseUpdate,
}
_NotificationValueToName = map[Notification]string{
@ -20,6 +22,8 @@ var (
PostProviderSubmitNotification: "PostProviderSubmitNotification",
NotificationPreDeploymentUpdate: "NotificationPreDeploymentUpdate",
NotificationDeploymentUpdate: "NotificationDeploymentUpdate",
NotificationPreReleaseUpdate: "NotificationPreReleaseUpdate",
NotificationReleaseUpdate: "NotificationReleaseUpdate",
}
)
@ -31,6 +35,8 @@ func init() {
interface{}(PostProviderSubmitNotification).(fmt.Stringer).String(): PostProviderSubmitNotification,
interface{}(NotificationPreDeploymentUpdate).(fmt.Stringer).String(): NotificationPreDeploymentUpdate,
interface{}(NotificationDeploymentUpdate).(fmt.Stringer).String(): NotificationDeploymentUpdate,
interface{}(NotificationPreReleaseUpdate).(fmt.Stringer).String(): NotificationPreReleaseUpdate,
interface{}(NotificationReleaseUpdate).(fmt.Stringer).String(): NotificationReleaseUpdate,
}
}
}

View File

@ -0,0 +1,59 @@
// generated by jsonenums -type=ProviderType; DO NOT EDIT
package types
import (
"encoding/json"
"fmt"
)
var (
_ProviderTypeNameToValue = map[string]ProviderType{
"ProviderTypeUnknown": ProviderTypeUnknown,
"ProviderTypeKubernetes": ProviderTypeKubernetes,
"ProviderTypeHelm": ProviderTypeHelm,
}
_ProviderTypeValueToName = map[ProviderType]string{
ProviderTypeUnknown: "ProviderTypeUnknown",
ProviderTypeKubernetes: "ProviderTypeKubernetes",
ProviderTypeHelm: "ProviderTypeHelm",
}
)
func init() {
var v ProviderType
if _, ok := interface{}(v).(fmt.Stringer); ok {
_ProviderTypeNameToValue = map[string]ProviderType{
interface{}(ProviderTypeUnknown).(fmt.Stringer).String(): ProviderTypeUnknown,
interface{}(ProviderTypeKubernetes).(fmt.Stringer).String(): ProviderTypeKubernetes,
interface{}(ProviderTypeHelm).(fmt.Stringer).String(): ProviderTypeHelm,
}
}
}
// MarshalJSON is generated so ProviderType satisfies json.Marshaler.
func (r ProviderType) MarshalJSON() ([]byte, error) {
if s, ok := interface{}(r).(fmt.Stringer); ok {
return json.Marshal(s.String())
}
s, ok := _ProviderTypeValueToName[r]
if !ok {
return nil, fmt.Errorf("invalid ProviderType: %d", r)
}
return json.Marshal(s)
}
// UnmarshalJSON is generated so ProviderType satisfies json.Unmarshaler.
func (r *ProviderType) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return fmt.Errorf("ProviderType should be a string, got %s", data)
}
v, ok := _ProviderTypeNameToValue[s]
if !ok {
return fmt.Errorf("invalid ProviderType %q", s)
}
*r = v
return nil
}

View File

@ -1,7 +1,9 @@
// Package types holds most of the types used across Keel
//go:generate jsonenums -type=Notification
//go:generate jsonenums -type=Level
//go:generate jsonenums -type=PolicyType
//go:generate jsonenums -type=TriggerType
//go:generate jsonenums -type=ProviderType
package types
import (
@ -30,6 +32,15 @@ const KeelPollDefaultSchedule = "@every 1m"
// KeelDigestAnnotation - digest annotation
const KeelDigestAnnotation = "keel.sh/digest"
// KeelMinimumApprovalsLabel - min approvals
const KeelMinimumApprovalsLabel = "keel.sh/approvals"
// KeelApprovalDeadlineLabel - approval deadline
const KeelApprovalDeadlineLabel = "keel.sh/approvalDeadline"
// KeelApprovalDeadlineDefault - default deadline in hours
const KeelApprovalDeadlineDefault = 24
// Repository - represents main docker repository fields that
// keel cares about
type Repository struct {
@ -230,3 +241,119 @@ func (l Level) Color() string {
return "#9E9E9E"
}
}
// ProviderType - provider type used to differentiate different providers
// when used with plugins
type ProviderType int
// Known provider types
const (
ProviderTypeUnknown ProviderType = iota
ProviderTypeKubernetes
ProviderTypeHelm
)
func (t ProviderType) String() string {
switch t {
case ProviderTypeUnknown:
return "unknown"
case ProviderTypeKubernetes:
return "kubernetes"
case ProviderTypeHelm:
return "helm"
default:
return ""
}
}
// Approval used to store and track updates
type Approval struct {
// Provider name - Kubernetes/Helm
Provider ProviderType `json:"provider,omitempty"`
// Identifier is used to inform user about specific
// Helm release or k8s deployment
// ie: k8s <namespace>/<deployment name>
// helm: <namespace>/<release name>
Identifier string `json:"identifier,omitempty"`
// Event that triggered evaluation
Event *Event `json:"event,omitempty"`
Message string `json:"message,omitempty"`
CurrentVersion string `json:"currentVersion,omitempty"`
NewVersion string `json:"newVersion,omitempty"`
// Requirements for the update such as number of votes
// and deadline
VotesRequired int `json:"votesRequired,omitempty"`
VotesReceived int `json:"votesReceived,omitempty"`
// Voters is a list of voter
// IDs for audit
Voters []string `json:"voters,omitempty"`
// Explicitly rejected approval
// can be set directly by user
// so even if deadline is not reached approval
// could be turned down
Rejected bool `json:"rejected,omitempty"`
// Deadline for this request
Deadline time.Time `json:"deadline,omitempty"`
// When this approval was created
CreatedAt time.Time `json:"createdAt,omitempty"`
// WHen this approval was updated
UpdatedAt time.Time `json:"updatedAt,omitempty"`
}
// ApprovalStatus - approval status type used in approvals
// to determine whether it was rejected/approved or still pending
type ApprovalStatus int
// Available approval status types
const (
ApprovalStatusUnknown ApprovalStatus = iota
ApprovalStatusPending
ApprovalStatusApproved
ApprovalStatusRejected
)
func (s ApprovalStatus) String() string {
switch s {
case ApprovalStatusPending:
return "pending"
case ApprovalStatusApproved:
return "approved"
case ApprovalStatusRejected:
return "rejected"
default:
return "unknown"
}
}
// Status - returns current approval status
func (a *Approval) Status() ApprovalStatus {
if a.Rejected {
return ApprovalStatusRejected
}
if a.VotesReceived >= a.VotesRequired {
return ApprovalStatusApproved
}
return ApprovalStatusPending
}
// Expired - checks if approval is already expired
func (a *Approval) Expired() bool {
return a.Deadline.Before(time.Now())
}
// Delta of what's changed
// ie: webhookrelay/webhook-demo:0.15.0 -> webhookrelay/webhook-demo:0.16.0
func (a *Approval) Delta() string {
return fmt.Sprintf("%s -> %s", a.CurrentVersion, a.NewVersion)
}

View File

@ -2,6 +2,7 @@ package types
import (
"testing"
"time"
)
func TestParsePolicy(t *testing.T) {
@ -104,3 +105,24 @@ func TestVersion_String(t *testing.T) {
})
}
}
func TestExpired(t *testing.T) {
aprv := Approval{
Deadline: time.Now().Add(-5 * time.Second),
}
if !aprv.Expired() {
t.Errorf("expected approval to be expired")
}
}
func TestNotExpired(t *testing.T) {
aprv := Approval{
Deadline: time.Now().Add(5 * time.Second),
}
if aprv.Expired() {
t.Errorf("expected approval to be not expired")
}
}

84
util/codecs/codecs.go Normal file
View File

@ -0,0 +1,84 @@
package codecs
import (
"bytes"
"encoding/gob"
"encoding/json"
"sync"
)
var bufferPool = sync.Pool{New: allocBuffer}
func allocBuffer() interface{} {
return &bytes.Buffer{}
}
func getBuffer() *bytes.Buffer {
return bufferPool.Get().(*bytes.Buffer)
}
func releaseBuffer(v *bytes.Buffer) {
v.Reset()
v.Grow(0)
bufferPool.Put(v)
}
// Serializer - generic serializer interface
type Serializer interface {
Encode(source interface{}) ([]byte, error)
Decode(data []byte, target interface{}) error
}
// DefaultSerializer - returns default serializer
func DefaultSerializer() Serializer {
return &GobSerializer{}
}
// GobSerializer - gob based serializer
type GobSerializer struct{}
// Encode - encodes source into bytes using Gob encoder
func (s *GobSerializer) Encode(source interface{}) ([]byte, error) {
buf := getBuffer()
defer releaseBuffer(buf)
enc := gob.NewEncoder(buf)
err := enc.Encode(source)
if err != nil {
return nil, err
}
return buf.Bytes(), nil
}
// Decode - decodes given bytes into target struct
func (s *GobSerializer) Decode(data []byte, target interface{}) error {
buf := bytes.NewBuffer(data)
dec := gob.NewDecoder(buf)
return dec.Decode(target)
}
// JSONSerializer - JSON based serializer
type JSONSerializer struct{}
// Encode - encodes source into bytes using JSON encoder
func (s *JSONSerializer) Encode(source interface{}) ([]byte, error) {
buf := getBuffer()
defer releaseBuffer(buf)
enc := json.NewEncoder(buf)
err := enc.Encode(source)
if err != nil {
return nil, err
}
return buf.Bytes(), nil
}
// Decode - decodes given bytes into target struct
func (s *JSONSerializer) Decode(data []byte, target interface{}) error {
buf := bytes.NewBuffer(data)
dec := json.NewDecoder(buf)
return dec.Decode(target)
}
// Type - shows serializer type
func (s *JSONSerializer) Type() string {
return "JSON"
}

View File

@ -1,6 +1,7 @@
package testing
import (
core_v1 "k8s.io/client-go/kubernetes/typed/core/v1"
"k8s.io/client-go/pkg/api/v1"
"k8s.io/client-go/pkg/apis/extensions/v1beta1"
)
@ -55,3 +56,9 @@ func (i *FakeK8sImplementer) Secret(namespace, name string) (*v1.Secret, error)
func (i *FakeK8sImplementer) Pods(namespace, labelSelector string) (*v1.PodList, error) {
return i.AvailablePods, nil
}
// ConfigMaps - returns nothing (not implemented)
func (i *FakeK8sImplementer) ConfigMaps(namespace string) core_v1.ConfigMapInterface {
panic("not implemented")
return nil
}

View File

@ -1,3 +1,7 @@
# 1.0.3
* Replace example files with testable examples
# 1.0.2
* bug: quote non-string values in text formatter (#583)

View File

@ -247,6 +247,7 @@ Note: Syslog hook also support connecting to local syslog (Ex. "/dev/log" or "/v
| [Airbrake](https://github.com/gemnasium/logrus-airbrake-hook) | Send errors to the Airbrake API V3. Uses the official [`gobrake`](https://github.com/airbrake/gobrake) behind the scenes. |
| [Amazon Kinesis](https://github.com/evalphobia/logrus_kinesis) | Hook for logging to [Amazon Kinesis](https://aws.amazon.com/kinesis/) |
| [Amqp-Hook](https://github.com/vladoatanasov/logrus_amqp) | Hook for logging to Amqp broker (Like RabbitMQ) |
| [AzureTableHook](https://github.com/kpfaulkner/azuretablehook/) | Hook for logging to Azure Table Storage|
| [Bugsnag](https://github.com/Shopify/logrus-bugsnag/blob/master/bugsnag.go) | Send errors to the Bugsnag exception tracking service. |
| [DeferPanic](https://github.com/deferpanic/dp-logrus) | Hook for logging to DeferPanic |
| [Discordrus](https://github.com/kz/discordrus) | Hook for logging to [Discord](https://discordapp.com/) |
@ -260,7 +261,7 @@ Note: Syslog hook also support connecting to local syslog (Ex. "/dev/log" or "/v
| [InfluxDB](https://github.com/Abramovic/logrus_influxdb) | Hook for logging to influxdb |
| [Influxus](http://github.com/vlad-doru/influxus) | Hook for concurrently logging to [InfluxDB](http://influxdata.com/) |
| [Journalhook](https://github.com/wercker/journalhook) | Hook for logging to `systemd-journald` |
| [KafkaLogrus](https://github.com/goibibo/KafkaLogrus) | Hook for logging to kafka |
| [KafkaLogrus](https://github.com/tracer0tong/kafkalogrus) | Hook for logging to Kafka |
| [LFShook](https://github.com/rifflock/lfshook) | Hook for logging to the local filesystem |
| [Logentries](https://github.com/jcftang/logentriesrus) | Hook for logging to [Logentries](https://logentries.com/) |
| [Logentrus](https://github.com/puddingfactory/logentrus) | Hook for logging to [Logentries](https://logentries.com/) |
@ -285,6 +286,7 @@ Note: Syslog hook also support connecting to local syslog (Ex. "/dev/log" or "/v
| [Sumorus](https://github.com/doublefree/sumorus) | Hook for logging to [SumoLogic](https://www.sumologic.com/)|
| [Syslog](https://github.com/sirupsen/logrus/blob/master/hooks/syslog/syslog.go) | Send errors to remote syslog server. Uses standard library `log/syslog` behind the scenes. |
| [Syslog TLS](https://github.com/shinji62/logrus-syslog-ng) | Send errors to remote syslog server with TLS support. |
| [Telegram](https://github.com/rossmcdonald/telegram_hook) | Hook for logging errors to [Telegram](https://telegram.org/) |
| [TraceView](https://github.com/evalphobia/logrus_appneta) | Hook for logging to [AppNeta TraceView](https://www.appneta.com/products/traceview/) |
| [Typetalk](https://github.com/dragon3/logrus-typetalk-hook) | Hook for logging to [Typetalk](https://www.typetalk.in/) |
| [logz.io](https://github.com/ripcurld00d/logrus-logzio-hook) | Hook for logging to [logz.io](https://logz.io), a Log as a Service using Logstash |

View File

@ -94,7 +94,10 @@ func (entry Entry) log(level Level, msg string) {
entry.Level = level
entry.Message = msg
if err := entry.Logger.Hooks.Fire(level, &entry); err != nil {
entry.Logger.mu.Lock()
err := entry.Logger.Hooks.Fire(level, &entry)
entry.Logger.mu.Unlock()
if err != nil {
entry.Logger.mu.Lock()
fmt.Fprintf(os.Stderr, "Failed to fire hook: %v\n", err)
entry.Logger.mu.Unlock()

View File

@ -1,59 +0,0 @@
package main
import (
"github.com/sirupsen/logrus"
// "os"
)
var log = logrus.New()
func init() {
log.Formatter = new(logrus.JSONFormatter)
log.Formatter = new(logrus.TextFormatter) // default
// file, err := os.OpenFile("logrus.log", os.O_CREATE|os.O_WRONLY, 0666)
// if err == nil {
// log.Out = file
// } else {
// log.Info("Failed to log to file, using default stderr")
// }
log.Level = logrus.DebugLevel
}
func main() {
defer func() {
err := recover()
if err != nil {
log.WithFields(logrus.Fields{
"omg": true,
"err": err,
"number": 100,
}).Fatal("The ice breaks!")
}
}()
log.WithFields(logrus.Fields{
"animal": "walrus",
"number": 8,
}).Debug("Started observing beach")
log.WithFields(logrus.Fields{
"animal": "walrus",
"size": 10,
}).Info("A group of walrus emerges from the ocean")
log.WithFields(logrus.Fields{
"omg": true,
"number": 122,
}).Warn("The group's number increased tremendously!")
log.WithFields(logrus.Fields{
"temperature": -4,
}).Debug("Temperature changes")
log.WithFields(logrus.Fields{
"animal": "orca",
"size": 9009,
}).Panic("It's over 9000!")
}

View File

@ -1,30 +0,0 @@
package main
import (
"github.com/sirupsen/logrus"
"gopkg.in/gemnasium/logrus-airbrake-hook.v2"
)
var log = logrus.New()
func init() {
log.Formatter = new(logrus.TextFormatter) // default
log.Hooks.Add(airbrake.NewHook(123, "xyz", "development"))
}
func main() {
log.WithFields(logrus.Fields{
"animal": "walrus",
"size": 10,
}).Info("A group of walrus emerges from the ocean")
log.WithFields(logrus.Fields{
"omg": true,
"number": 122,
}).Warn("The group's number increased tremendously!")
log.WithFields(logrus.Fields{
"omg": true,
"number": 100,
}).Fatal("The ice breaks!")
}

View File

@ -1,6 +1,7 @@
package logrus
import (
"sync"
"testing"
"github.com/stretchr/testify/assert"
@ -120,3 +121,24 @@ func TestErrorHookShouldFireOnError(t *testing.T) {
assert.Equal(t, hook.Fired, true)
})
}
func TestAddHookRace(t *testing.T) {
var wg sync.WaitGroup
wg.Add(2)
hook := new(ErrorHook)
LogAndAssertJSON(t, func(log *Logger) {
go func() {
defer wg.Done()
log.AddHook(hook)
}()
go func() {
defer wg.Done()
log.Error("test")
}()
wg.Wait()
}, func(fields Fields) {
// the line may have been logged
// before the hook was added, so we can't
// actually assert on the hook
})
}

View File

@ -315,3 +315,9 @@ func (logger *Logger) level() Level {
func (logger *Logger) SetLevel(level Level) {
atomic.StoreUint32((*uint32)(&logger.Level), uint32(level))
}
func (logger *Logger) AddHook(hook Hook) {
logger.mu.Lock()
defer logger.mu.Unlock()
logger.Hooks.Add(hook)
}

View File

@ -15,7 +15,7 @@ The name mux stands for "HTTP request multiplexer". Like the standard `http.Serv
* It implements the `http.Handler` interface so it is compatible with the standard `http.ServeMux`.
* Requests can be matched based on URL host, path, path prefix, schemes, header and query values, HTTP methods or using custom matchers.
* URL hosts and paths can have variables with an optional regular expression.
* URL hosts, paths and query values can have variables with an optional regular expression.
* Registered URLs can be built, or "reversed", which helps maintaining references to resources.
* Routes can be used as subrouters: nested routes are only tested if the parent route matches. This is useful to define groups of routes that share common conditions like a host, a path prefix or other repeated attributes. As a bonus, this optimizes request matching.
@ -24,9 +24,9 @@ The name mux stands for "HTTP request multiplexer". Like the standard `http.Serv
* [Install](#install)
* [Examples](#examples)
* [Matching Routes](#matching-routes)
* [Listing Routes](#listing-routes)
* [Static Files](#static-files)
* [Registered URLs](#registered-urls)
* [Walking Routes](#walking-routes)
* [Full Example](#full-example)
---
@ -168,7 +168,6 @@ s.HandleFunc("/{key}/", ProductHandler)
// "/products/{key}/details"
s.HandleFunc("/{key}/details", ProductDetailsHandler)
```
### Listing Routes
Routes on a mux can be listed using the Router.Walk method—useful for generating documentation:
@ -191,9 +190,9 @@ func handler(w http.ResponseWriter, r *http.Request) {
func main() {
r := mux.NewRouter()
r.HandleFunc("/", handler)
r.Methods("POST").HandleFunc("/products", handler)
r.Methods("GET").HandleFunc("/articles", handler)
r.Methods("GET", "PUT").HandleFunc("/articles/{id}", handler)
r.HandleFunc("/products", handler).Methods("POST")
r.HandleFunc("/articles", handler).Methods("GET")
r.HandleFunc("/articles/{id}", handler).Methods("GET", "PUT")
r.Walk(func(route *mux.Route, router *mux.Router, ancestors []*mux.Route) error {
t, err := route.GetPathTemplate()
if err != nil {
@ -269,19 +268,21 @@ url, err := r.Get("article").URL("category", "technology", "id", "42")
"/articles/technology/42"
```
This also works for host variables:
This also works for host and query value variables:
```go
r := mux.NewRouter()
r.Host("{subdomain}.domain.com").
Path("/articles/{category}/{id:[0-9]+}").
Queries("filter", "{filter}").
HandlerFunc(ArticleHandler).
Name("article")
// url.String() will be "http://news.domain.com/articles/technology/42"
// url.String() will be "http://news.domain.com/articles/technology/42?filter=gorilla"
url, err := r.Get("article").URL("subdomain", "news",
"category", "technology",
"id", "42")
"id", "42",
"filter", "gorilla")
```
All variables defined in the route are required, and their values must conform to the corresponding patterns. These requirements guarantee that a generated URL will always match a registered route -- the only exception is for explicitly defined "build-only" routes which never match.
@ -319,6 +320,37 @@ url, err := r.Get("article").URL("subdomain", "news",
"id", "42")
```
### Walking Routes
The `Walk` function on `mux.Router` can be used to visit all of the routes that are registered on a router. For example,
the following prints all of the registered routes:
```go
r := mux.NewRouter()
r.HandleFunc("/", handler)
r.HandleFunc("/products", handler).Methods("POST")
r.HandleFunc("/articles", handler).Methods("GET")
r.HandleFunc("/articles/{id}", handler).Methods("GET", "PUT")
r.Walk(func(route *mux.Route, router *mux.Router, ancestors []*mux.Route) error {
t, err := route.GetPathTemplate()
if err != nil {
return err
}
// p will contain a regular expression that is compatible with regular expressions in Perl, Python, and other languages.
// For example, the regular expression for path '/articles/{id}' will be '^/articles/(?P<v0>[^/]+)$'.
p, err := route.GetPathRegexp()
if err != nil {
return err
}
m, err := route.GetMethods()
if err != nil {
return err
}
fmt.Println(strings.Join(m, ","), t, p)
return nil
})
```
## Full Example
Here's a complete, runnable example of a small `mux` based server:

12
vendor/github.com/gorilla/mux/doc.go generated vendored
View File

@ -12,8 +12,8 @@ or other conditions. The main features are:
* Requests can be matched based on URL host, path, path prefix, schemes,
header and query values, HTTP methods or using custom matchers.
* URL hosts and paths can have variables with an optional regular
expression.
* URL hosts, paths and query values can have variables with an optional
regular expression.
* Registered URLs can be built, or "reversed", which helps maintaining
references to resources.
* Routes can be used as subrouters: nested routes are only tested if the
@ -188,18 +188,20 @@ key/value pairs for the route variables. For the previous route, we would do:
"/articles/technology/42"
This also works for host variables:
This also works for host and query value variables:
r := mux.NewRouter()
r.Host("{subdomain}.domain.com").
Path("/articles/{category}/{id:[0-9]+}").
Queries("filter", "{filter}").
HandlerFunc(ArticleHandler).
Name("article")
// url.String() will be "http://news.domain.com/articles/technology/42"
// url.String() will be "http://news.domain.com/articles/technology/42?filter=gorilla"
url, err := r.Get("article").URL("subdomain", "news",
"category", "technology",
"id", "42")
"id", "42",
"filter", "gorilla")
All variables defined in the route are required, and their values must
conform to the corresponding patterns. These requirements guarantee that a

48
vendor/github.com/gorilla/mux/mux.go generated vendored
View File

@ -13,6 +13,10 @@ import (
"strings"
)
var (
ErrMethodMismatch = errors.New("method is not allowed")
)
// NewRouter returns a new router instance.
func NewRouter() *Router {
return &Router{namedRoutes: make(map[string]*Route), KeepContext: false}
@ -39,6 +43,10 @@ func NewRouter() *Router {
type Router struct {
// Configurable Handler to be used when no route matches.
NotFoundHandler http.Handler
// Configurable Handler to be used when the request method does not match the route.
MethodNotAllowedHandler http.Handler
// Parent route, if this is a subrouter.
parent parentRoute
// Routes to be matched, in order.
@ -65,6 +73,11 @@ func (r *Router) Match(req *http.Request, match *RouteMatch) bool {
}
}
if match.MatchErr == ErrMethodMismatch && r.MethodNotAllowedHandler != nil {
match.Handler = r.MethodNotAllowedHandler
return true
}
// Closest match for a router (includes sub-routers)
if r.NotFoundHandler != nil {
match.Handler = r.NotFoundHandler
@ -105,9 +118,15 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
req = setVars(req, match.Vars)
req = setCurrentRoute(req, match.Route)
}
if handler == nil && match.MatchErr == ErrMethodMismatch {
handler = methodNotAllowedHandler()
}
if handler == nil {
handler = http.NotFoundHandler()
}
if !r.KeepContext {
defer contextClear(req)
}
@ -176,6 +195,13 @@ func (r *Router) UseEncodedPath() *Router {
// parentRoute
// ----------------------------------------------------------------------------
func (r *Router) getBuildScheme() string {
if r.parent != nil {
return r.parent.getBuildScheme()
}
return ""
}
// getNamedRoutes returns the map where named routes are registered.
func (r *Router) getNamedRoutes() map[string]*Route {
if r.namedRoutes == nil {
@ -299,10 +325,6 @@ type WalkFunc func(route *Route, router *Router, ancestors []*Route) error
func (r *Router) walk(walkFn WalkFunc, ancestors []*Route) error {
for _, t := range r.routes {
if t.regexp == nil || t.regexp.path == nil || t.regexp.path.template == "" {
continue
}
err := walkFn(t, r, ancestors)
if err == SkipRouter {
continue
@ -312,10 +334,12 @@ func (r *Router) walk(walkFn WalkFunc, ancestors []*Route) error {
}
for _, sr := range t.matchers {
if h, ok := sr.(*Router); ok {
ancestors = append(ancestors, t)
err := h.walk(walkFn, ancestors)
if err != nil {
return err
}
ancestors = ancestors[:len(ancestors)-1]
}
}
if h, ok := t.handler.(*Router); ok {
@ -339,6 +363,11 @@ type RouteMatch struct {
Route *Route
Handler http.Handler
Vars map[string]string
// MatchErr is set to appropriate matching error
// It is set to ErrMethodMismatch if there is a mismatch in
// the request method and route method
MatchErr error
}
type contextKey int
@ -458,7 +487,7 @@ func mapFromPairsToString(pairs ...string) (map[string]string, error) {
return m, nil
}
// mapFromPairsToRegex converts variadic string paramers to a
// mapFromPairsToRegex converts variadic string parameters to a
// string to regex map.
func mapFromPairsToRegex(pairs ...string) (map[string]*regexp.Regexp, error) {
length, err := checkPairs(pairs...)
@ -540,3 +569,12 @@ func matchMapWithRegex(toCheck map[string]*regexp.Regexp, toMatch map[string][]s
}
return true
}
// methodNotAllowed replies to the request with an HTTP status code 405.
func methodNotAllowed(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusMethodNotAllowed)
}
// methodNotAllowedHandler returns a simple request handler
// that replies to each request with a status code 405.
func methodNotAllowedHandler() http.Handler { return http.HandlerFunc(methodNotAllowed) }

View File

@ -11,6 +11,7 @@ import (
"fmt"
"net/http"
"net/url"
"reflect"
"strings"
"testing"
)
@ -35,6 +36,7 @@ type routeTest struct {
scheme string // the expected scheme of the built URL
host string // the expected host of the built URL
path string // the expected path of the built URL
query string // the expected query string of the built URL
pathTemplate string // the expected path template of the route
hostTemplate string // the expected host template of the route
methods []string // the expected route methods
@ -743,6 +745,7 @@ func TestQueries(t *testing.T) {
vars: map[string]string{},
host: "",
path: "",
query: "foo=bar&baz=ding",
shouldMatch: true,
},
{
@ -752,6 +755,7 @@ func TestQueries(t *testing.T) {
vars: map[string]string{},
host: "",
path: "",
query: "foo=bar&baz=ding",
pathTemplate: `/api`,
hostTemplate: `www.example.com`,
shouldMatch: true,
@ -763,6 +767,7 @@ func TestQueries(t *testing.T) {
vars: map[string]string{},
host: "",
path: "",
query: "foo=bar&baz=ding",
pathTemplate: `/api`,
hostTemplate: `www.example.com`,
shouldMatch: true,
@ -783,6 +788,7 @@ func TestQueries(t *testing.T) {
vars: map[string]string{"v1": "bar"},
host: "",
path: "",
query: "foo=bar",
shouldMatch: true,
},
{
@ -792,6 +798,7 @@ func TestQueries(t *testing.T) {
vars: map[string]string{"v1": "bar", "v2": "ding"},
host: "",
path: "",
query: "foo=bar&baz=ding",
shouldMatch: true,
},
{
@ -801,6 +808,7 @@ func TestQueries(t *testing.T) {
vars: map[string]string{"v1": "10"},
host: "",
path: "",
query: "foo=10",
shouldMatch: true,
},
{
@ -819,6 +827,7 @@ func TestQueries(t *testing.T) {
vars: map[string]string{"v1": "1"},
host: "",
path: "",
query: "foo=1",
shouldMatch: true,
},
{
@ -828,6 +837,7 @@ func TestQueries(t *testing.T) {
vars: map[string]string{"v1": "1"},
host: "",
path: "",
query: "foo=1",
shouldMatch: true,
},
{
@ -846,6 +856,7 @@ func TestQueries(t *testing.T) {
vars: map[string]string{"v1": "1a"},
host: "",
path: "",
query: "foo=1a",
shouldMatch: true,
},
{
@ -864,6 +875,7 @@ func TestQueries(t *testing.T) {
vars: map[string]string{"v-1": "bar"},
host: "",
path: "",
query: "foo=bar",
shouldMatch: true,
},
{
@ -873,6 +885,7 @@ func TestQueries(t *testing.T) {
vars: map[string]string{"v-1": "bar", "v-2": "ding"},
host: "",
path: "",
query: "foo=bar&baz=ding",
shouldMatch: true,
},
{
@ -882,6 +895,7 @@ func TestQueries(t *testing.T) {
vars: map[string]string{"v-1": "10"},
host: "",
path: "",
query: "foo=10",
shouldMatch: true,
},
{
@ -891,6 +905,7 @@ func TestQueries(t *testing.T) {
vars: map[string]string{"v-1": "1a"},
host: "",
path: "",
query: "foo=1a",
shouldMatch: true,
},
{
@ -900,6 +915,7 @@ func TestQueries(t *testing.T) {
vars: map[string]string{},
host: "",
path: "",
query: "foo=",
shouldMatch: true,
},
{
@ -918,6 +934,7 @@ func TestQueries(t *testing.T) {
vars: map[string]string{},
host: "",
path: "",
query: "foo=",
shouldMatch: true,
},
{
@ -945,6 +962,7 @@ func TestQueries(t *testing.T) {
vars: map[string]string{"foo": ""},
host: "",
path: "",
query: "foo=",
shouldMatch: true,
},
{
@ -956,6 +974,16 @@ func TestQueries(t *testing.T) {
path: "",
shouldMatch: false,
},
{
title: "Queries route with pattern, match, escaped value",
route: new(Route).Queries("foo", "{v1}"),
request: newRequest("GET", "http://localhost?foo=%25bar%26%20%2F%3D%3F"),
vars: map[string]string{"v1": "%bar& /=?"},
host: "",
path: "",
query: "foo=%25bar%26+%2F%3D%3F",
shouldMatch: true,
},
}
for _, test := range tests {
@ -1187,6 +1215,28 @@ func TestSubRouter(t *testing.T) {
pathTemplate: `/{category}`,
shouldMatch: true,
},
{
title: "Build with scheme on parent router",
route: new(Route).Schemes("ftp").Host("google.com").Subrouter().Path("/"),
request: newRequest("GET", "ftp://google.com/"),
scheme: "ftp",
host: "google.com",
path: "/",
pathTemplate: `/`,
hostTemplate: `google.com`,
shouldMatch: true,
},
{
title: "Prefer scheme on child route when building URLs",
route: new(Route).Schemes("https", "ftp").Host("google.com").Subrouter().Schemes("ftp").Path("/"),
request: newRequest("GET", "ftp://google.com/"),
scheme: "ftp",
host: "google.com",
path: "/",
pathTemplate: `/`,
hostTemplate: `google.com`,
shouldMatch: true,
},
}
for _, test := range tests {
@ -1382,11 +1432,55 @@ func TestWalkNested(t *testing.T) {
l2 := l1.PathPrefix("/l").Subrouter()
l2.Path("/a")
paths := []string{"/g", "/g/o", "/g/o/r", "/g/o/r/i", "/g/o/r/i/l", "/g/o/r/i/l/l", "/g/o/r/i/l/l/a"}
testCases := []struct {
path string
ancestors []*Route
}{
{"/g", []*Route{}},
{"/g/o", []*Route{g.parent.(*Route)}},
{"/g/o/r", []*Route{g.parent.(*Route), o.parent.(*Route)}},
{"/g/o/r/i", []*Route{g.parent.(*Route), o.parent.(*Route), r.parent.(*Route)}},
{"/g/o/r/i/l", []*Route{g.parent.(*Route), o.parent.(*Route), r.parent.(*Route), i.parent.(*Route)}},
{"/g/o/r/i/l/l", []*Route{g.parent.(*Route), o.parent.(*Route), r.parent.(*Route), i.parent.(*Route), l1.parent.(*Route)}},
{"/g/o/r/i/l/l/a", []*Route{g.parent.(*Route), o.parent.(*Route), r.parent.(*Route), i.parent.(*Route), l1.parent.(*Route), l2.parent.(*Route)}},
}
idx := 0
err := router.Walk(func(route *Route, router *Router, ancestors []*Route) error {
path := testCases[idx].path
tpl := route.regexp.path.template
if tpl != path {
t.Errorf(`Expected %s got %s`, path, tpl)
}
currWantAncestors := testCases[idx].ancestors
if !reflect.DeepEqual(currWantAncestors, ancestors) {
t.Errorf(`Expected %+v got %+v`, currWantAncestors, ancestors)
}
idx++
return nil
})
if err != nil {
panic(err)
}
if idx != len(testCases) {
t.Errorf("Expected %d routes, found %d", len(testCases), idx)
}
}
func TestWalkSubrouters(t *testing.T) {
router := NewRouter()
g := router.Path("/g").Subrouter()
o := g.PathPrefix("/o").Subrouter()
o.Methods("GET")
o.Methods("PUT")
// all 4 routes should be matched, but final 2 routes do not have path templates
paths := []string{"/g", "/g/o", "", ""}
idx := 0
err := router.Walk(func(route *Route, router *Router, ancestors []*Route) error {
path := paths[idx]
tpl := route.regexp.path.template
tpl, _ := route.GetPathTemplate()
if tpl != path {
t.Errorf(`Expected %s got %s`, path, tpl)
}
@ -1492,6 +1586,7 @@ func testRoute(t *testing.T, test routeTest) {
route := test.route
vars := test.vars
shouldMatch := test.shouldMatch
query := test.query
shouldRedirect := test.shouldRedirect
uri := url.URL{
Scheme: test.scheme,
@ -1561,6 +1656,13 @@ func testRoute(t *testing.T, test routeTest) {
return
}
}
if query != "" {
u, _ := route.URL(mapToPairs(match.Vars)...)
if query != u.RawQuery {
t.Errorf("(%v) URL query not equal: expected %v, got %v", test.title, query, u.RawQuery)
return
}
}
if shouldRedirect && match.Handler == nil {
t.Errorf("(%v) Did not redirect", test.title)
return
@ -1769,3 +1871,42 @@ func newRequest(method, url string) *http.Request {
}
return req
}
func TestNoMatchMethodErrorHandler(t *testing.T) {
func1 := func(w http.ResponseWriter, r *http.Request) {}
r := NewRouter()
r.HandleFunc("/", func1).Methods("GET", "POST")
req, _ := http.NewRequest("PUT", "http://localhost/", nil)
match := new(RouteMatch)
matched := r.Match(req, match)
if matched {
t.Error("Should not have matched route for methods")
}
if match.MatchErr != ErrMethodMismatch {
t.Error("Should get ErrMethodMismatch error")
}
resp := NewRecorder()
r.ServeHTTP(resp, req)
if resp.Code != 405 {
t.Errorf("Expecting code %v", 405)
}
// Add matching route
r.HandleFunc("/", func1).Methods("PUT")
match = new(RouteMatch)
matched = r.Match(req, match)
if !matched {
t.Error("Should have matched route for methods")
}
if match.MatchErr != nil {
t.Error("Should not have any matching error. Found:", match.MatchErr)
}
}

View File

@ -121,12 +121,7 @@ func TestRouteMatchers(t *testing.T) {
var routeMatch RouteMatch
matched := router.Match(request, &routeMatch)
if matched != shouldMatch {
// Need better messages. :)
if matched {
t.Errorf("Should match.")
} else {
t.Errorf("Should not match.")
}
t.Errorf("Expected: %v\nGot: %v\nRequest: %v %v", shouldMatch, matched, request.Method, url)
}
if matched {
@ -188,7 +183,6 @@ func TestRouteMatchers(t *testing.T) {
match(true)
// 2nd route --------------------------------------------------------------
// Everything match.
reset2()
match(true)

View File

@ -35,7 +35,7 @@ func newRouteRegexp(tpl string, matchHost, matchPrefix, matchQuery, strictSlash,
// Now let's parse it.
defaultPattern := "[^/]+"
if matchQuery {
defaultPattern = "[^?&]*"
defaultPattern = ".*"
} else if matchHost {
defaultPattern = "[^.]+"
matchPrefix = false
@ -178,6 +178,9 @@ func (r *routeRegexp) url(values map[string]string) (string, error) {
if !ok {
return "", fmt.Errorf("mux: missing route variable %q", v)
}
if r.matchQuery {
value = url.QueryEscape(value)
}
urlValues[k] = value
}
rv := fmt.Sprintf(r.reverse, urlValues...)

View File

@ -52,12 +52,27 @@ func (r *Route) Match(req *http.Request, match *RouteMatch) bool {
if r.buildOnly || r.err != nil {
return false
}
var matchErr error
// Match everything.
for _, m := range r.matchers {
if matched := m.Match(req, match); !matched {
if _, ok := m.(methodMatcher); ok {
matchErr = ErrMethodMismatch
continue
}
matchErr = nil
return false
}
}
if matchErr != nil {
match.MatchErr = matchErr
return false
}
match.MatchErr = nil
// Yay, we have a match. Let's collect some info about it.
if match.Route == nil {
match.Route = r
@ -68,6 +83,7 @@ func (r *Route) Match(req *http.Request, match *RouteMatch) bool {
if match.Vars == nil {
match.Vars = make(map[string]string)
}
// Set variables.
if r.regexp != nil {
r.regexp.setMatch(req, match, r)
@ -482,13 +498,14 @@ func (r *Route) URL(pairs ...string) (*url.URL, error) {
return nil, err
}
var scheme, host, path string
queries := make([]string, 0, len(r.regexp.queries))
if r.regexp.host != nil {
if host, err = r.regexp.host.url(values); err != nil {
return nil, err
}
scheme = "http"
if r.buildScheme != "" {
scheme = r.buildScheme
if s := r.getBuildScheme(); s != "" {
scheme = s
}
}
if r.regexp.path != nil {
@ -496,10 +513,18 @@ func (r *Route) URL(pairs ...string) (*url.URL, error) {
return nil, err
}
}
for _, q := range r.regexp.queries {
var query string
if query, err = q.url(values); err != nil {
return nil, err
}
queries = append(queries, query)
}
return &url.URL{
Scheme: scheme,
Host: host,
Path: path,
Scheme: scheme,
Host: host,
Path: path,
RawQuery: strings.Join(queries, "&"),
}, nil
}
@ -525,8 +550,8 @@ func (r *Route) URLHost(pairs ...string) (*url.URL, error) {
Scheme: "http",
Host: host,
}
if r.buildScheme != "" {
u.Scheme = r.buildScheme
if s := r.getBuildScheme(); s != "" {
u.Scheme = s
}
return u, nil
}
@ -640,11 +665,22 @@ func (r *Route) buildVars(m map[string]string) map[string]string {
// parentRoute allows routes to know about parent host and path definitions.
type parentRoute interface {
getBuildScheme() string
getNamedRoutes() map[string]*Route
getRegexpGroup() *routeRegexpGroup
buildVars(map[string]string) map[string]string
}
func (r *Route) getBuildScheme() string {
if r.buildScheme != "" {
return r.buildScheme
}
if r.parent != nil {
return r.parent.getBuildScheme()
}
return ""
}
// getNamedRoutes returns the map where named routes are registered.
func (r *Route) getNamedRoutes() map[string]*Route {
if r.parent == nil {

201
vendor/github.com/rusenask/k8s-kv/LICENSE generated vendored Normal file
View File

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2017 Karolis Rusenas
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.

94
vendor/github.com/rusenask/k8s-kv/README.md generated vendored Normal file
View File

@ -0,0 +1,94 @@
# Kubernetes backed KV
[![GoDoc](https://godoc.org/github.com/rusenask/k8s-kv/kv?status.svg)](https://godoc.org/github.com/rusenask/k8s-kv/kv)
Use Kubernetes config maps as key/value store!
When to use k8s-kv:
* You have a simple application that has a need to store some configuration and you can't be bothered to set up EBS like volumes or use some fancy external KV store.
* You have a stateless application that suddenly got to store state and you are not into converting
it into full stateless app that will use a proper database.
When __not to__ use k8s-kv:
* You have a read/write heavy multi-node application (k8s-kv doesn't have cross-app locking).
* You want to store bigger values than 1MB. Even though k8s-kv uses compression for the data stored in bucket - it's wise to not try the limits. It's there because of the limit in Etcd. In this case use something else.
## Basics
Package API:
```
// Pyt key/value pair into the store
Put(key string, value []byte) error
// Get value of the specified key
Get(key string) (value []byte, err error)
// Delete key/value pair from the store
Delete(key string) error
// List all key/value pairs under specified prefix
List(prefix string) (data map[string][]byte, err error)
// Delete config map (results in deleted data)
Teardown() error
```
## Caveats
* Don't be silly, you can't put a lot of stuff here.
## Example
Usage example:
1. Get minikube or your favourite k8s environment running.
2. In your app you will probably want to use this: https://github.com/kubernetes/client-go/tree/master/examples/in-cluster-client-configuration
3. Get ConfigMaps interface and supply it to this lib:
```
package main
import (
"fmt"
"github.com/rusenask/k8s-kv/kv"
"k8s.io/client-go/kubernetes"
core_v1 "k8s.io/client-go/kubernetes/typed/core/v1"
"k8s.io/client-go/tools/clientcmd"
)
// get ConfigMapInterface to access config maps in "default" namespace
func getImplementer() (implementer core_v1.ConfigMapInterface) {
cfg, err := clientcmd.BuildConfigFromFlags("", ".kubeconfig") // in your app you could replace it with in-cluster-config
if err != nil {
panic(err)
}
client, err := kubernetes.NewForConfig(cfg)
if err != nil {
panic(err)
}
return client.ConfigMaps("default")
}
func main() {
impl := getImplementer()
// getting acces to k8s-kv. "my-app" will become a label
// for this config map, this way it's easier to manage configs
// "bucket1" will be config map's name and represent one entry in config maps list
kvdb, err := kv.New(impl, "my-app", "bucket1")
if err != nil {
panic(err)
}
// insert a key "foo" with value "hello kubernetes world"
kvdb.Put("foo", []byte("hello kubernetes world"))
// get value of key "foo"
stored, _ := kvdb.Get("foo")
fmt.Println(string(stored))
}
```

40
vendor/github.com/rusenask/k8s-kv/examples/main.go generated vendored Normal file
View File

@ -0,0 +1,40 @@
package main
import (
"fmt"
"github.com/rusenask/k8s-kv/kv"
"k8s.io/client-go/kubernetes"
core_v1 "k8s.io/client-go/kubernetes/typed/core/v1"
"k8s.io/client-go/tools/clientcmd"
)
func getImplementer() (implementer core_v1.ConfigMapInterface) {
cfg, err := clientcmd.BuildConfigFromFlags("", ".kubeconfig") // in your app you could replace it with in-cluster-config
if err != nil {
panic(err)
}
client, err := kubernetes.NewForConfig(cfg)
if err != nil {
panic(err)
}
return client.ConfigMaps("default")
}
func main() {
impl := getImplementer()
kvdb, err := kv.New(impl, "my-app", "bucket1")
if err != nil {
panic(err)
}
kvdb.Put("foo", []byte("hello kubernetes world"))
stored, _ := kvdb.Get("foo")
fmt.Println(string(stored))
}

16
vendor/github.com/rusenask/k8s-kv/kv/doc.go generated vendored Normal file
View File

@ -0,0 +1,16 @@
/*
Package kv implements a low-level key/value store backed by Kubernetes config maps.
It supports main operations expected from key/value store such as Put, Get, Delete and List.
Operations are protected by an internal mutex and therefore can be safely used inside a single
node application.
Basics
There are only few things worth to know: key/value database is created based on bucket name so in order
to have multiple configMaps - use different bucket names. Teardown() function will remove configMap entry
completely destroying all entries.
Caveats
Since k8s-kv is based on configMaps which are in turn based on Etcd key/value store - all values have a limitation
of 1MB so each bucket in k8s-kv is limited to that size. To overcome it - create more buckets.
If you have multi-node application that is frequently reading/writing to the same buckets - be aware of race
conditions as it doesn't provide any cross-node locking capabilities.
*/
package kv

311
vendor/github.com/rusenask/k8s-kv/kv/kv.go generated vendored Normal file
View File

@ -0,0 +1,311 @@
package kv
import (
"bytes"
"compress/gzip"
"encoding/base64"
"encoding/gob"
"errors"
"io/ioutil"
"strings"
"sync"
apierrors "k8s.io/apimachinery/pkg/api/errors"
meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/pkg/api/v1"
)
func init() {
gob.Register(&internalMap{})
}
type internalMap struct {
Data map[string][]byte
}
// errors
var (
ErrNotFound = errors.New("not found")
)
var b64 = base64.StdEncoding
// KVDB generic kv package interface
type KVDB interface {
Put(key string, value []byte) error
Get(key string) (value []byte, err error)
Delete(key string) error
List(prefix string) (data map[string][]byte, err error)
Teardown() error
}
// KV provides access to key/value store operations such as Put, Get, Delete, List.
// Entry in ConfigMap is created based on bucket name and total size is limited to 1MB per bucket.
// Operations are protected by an internal mutex so it's safe to use in a single node application.
type KV struct {
app string
bucket string
implementer ConfigMapInterface
mu *sync.RWMutex
serializer Serializer
}
// ConfigMapInterface implements a subset of Kubernetes original ConfigMapInterface to provide
// required operations for k8s-kv. Main purpose of this interface is to enable easier testing.
type ConfigMapInterface interface {
Get(name string, options meta_v1.GetOptions) (*v1.ConfigMap, error)
Create(cfgMap *v1.ConfigMap) (*v1.ConfigMap, error)
Update(cfgMap *v1.ConfigMap) (*v1.ConfigMap, error)
Delete(name string, options *meta_v1.DeleteOptions) error
}
// New creates a new instance of KV. Requires prepared ConfigMapInterface (provided by go-client), app and bucket names.
// App name is used as a label to make it easier to distinguish different k8s-kv instances created by separate (or the same)
// application. Bucket name is used to give a name to config map.
func New(implementer ConfigMapInterface, app, bucket string) (*KV, error) {
kv := &KV{
implementer: implementer,
app: app,
bucket: bucket,
mu: &sync.RWMutex{},
serializer: DefaultSerializer(),
}
_, err := kv.getMap()
if err != nil {
return nil, err
}
return kv, nil
}
// Teardown deletes configMap for this bucket. All bucket's data is lost.
func (k *KV) Teardown() error {
return k.implementer.Delete(k.bucket, &meta_v1.DeleteOptions{})
}
func (k *KV) getMap() (*v1.ConfigMap, error) {
cfgMap, err := k.implementer.Get(k.bucket, meta_v1.GetOptions{})
if err != nil {
// creating
if apierrors.IsNotFound(err) {
return k.newConfigMapsObject()
}
return nil, err
}
if cfgMap.Data == nil {
cfgMap.Data = make(map[string]string)
}
// it's there, nothing to do
return cfgMap, nil
}
func encodeInternalMap(serializer Serializer, data map[string][]byte) (string, error) {
var im internalMap
im.Data = data
bts, err := serializer.Encode(&im)
if err != nil {
return "", err
}
var buf bytes.Buffer
w, err := gzip.NewWriterLevel(&buf, gzip.BestCompression)
if err != nil {
return "", err
}
if _, err = w.Write(bts); err != nil {
return "", err
}
w.Close()
return b64.EncodeToString(buf.Bytes()), nil
}
func decodeInternalMap(serializer Serializer, data string) (map[string][]byte, error) {
if data == "" {
empty := make(map[string][]byte)
return empty, nil
}
b, err := b64.DecodeString(data)
if err != nil {
return nil, err
}
r, err := gzip.NewReader(bytes.NewReader(b))
if err != nil {
return nil, err
}
decompressed, err := ioutil.ReadAll(r)
if err != nil {
return nil, err
}
var im internalMap
err = serializer.Decode(decompressed, &im)
return im.Data, err
}
const dataKey = "data"
func (k *KV) newConfigMapsObject() (*v1.ConfigMap, error) {
var lbs labels
lbs.init()
// apply labels
lbs.set("BUCKET", k.bucket)
lbs.set("APP", k.app)
lbs.set("OWNER", "K8S-KV")
// create and return configmap object
cfgMap := &v1.ConfigMap{
ObjectMeta: meta_v1.ObjectMeta{
Name: k.bucket,
Labels: lbs.toMap(),
},
Data: map[string]string{
dataKey: "",
},
}
cm, err := k.implementer.Create(cfgMap)
if err != nil {
return nil, err
}
return cm, nil
}
func (k *KV) saveInternalMap(cfgMap *v1.ConfigMap, im map[string][]byte) error {
encoded, err := encodeInternalMap(k.serializer, im)
if err != nil {
return err
}
cfgMap.Data[dataKey] = encoded
return k.saveMap(cfgMap)
}
func (k *KV) getInternalMap() (*v1.ConfigMap, map[string][]byte, error) {
cfgMap, err := k.getMap()
if err != nil {
return nil, nil, err
}
im, err := decodeInternalMap(k.serializer, cfgMap.Data[dataKey])
if err != nil {
return nil, nil, err
}
return cfgMap, im, nil
}
func (k *KV) saveMap(cfgMap *v1.ConfigMap) error {
_, err := k.implementer.Update(cfgMap)
return err
}
// Put saves key/value pair into a bucket. Value can be any []byte value (ie: encoded JSON/GOB)
func (k *KV) Put(key string, value []byte) error {
k.mu.Lock()
defer k.mu.Unlock()
cfgMap, im, err := k.getInternalMap()
if err != nil {
return err
}
im[key] = value
return k.saveInternalMap(cfgMap, im)
}
// Get retrieves value from the key/value store bucket or returns ErrNotFound error if it was not found.
func (k *KV) Get(key string) (value []byte, err error) {
k.mu.RLock()
defer k.mu.RUnlock()
_, im, err := k.getInternalMap()
if err != nil {
return
}
val, ok := im[key]
if !ok {
return []byte(""), ErrNotFound
}
return val, nil
}
// Delete removes entry from the KV store bucket.
func (k *KV) Delete(key string) error {
k.mu.Lock()
defer k.mu.Unlock()
cfgMap, im, err := k.getInternalMap()
if err != nil {
return err
}
delete(im, key)
return k.saveInternalMap(cfgMap, im)
}
// List retrieves all entries that match specific prefix
func (k *KV) List(prefix string) (data map[string][]byte, err error) {
k.mu.RLock()
defer k.mu.RUnlock()
_, im, err := k.getInternalMap()
if err != nil {
return
}
data = make(map[string][]byte)
for key, val := range im {
if strings.HasPrefix(key, prefix) {
data[key] = val
}
}
return
}
// labels is a map of key value pairs to be included as metadata in a configmap object.
type labels map[string]string
func (lbs *labels) init() { *lbs = labels(make(map[string]string)) }
func (lbs labels) get(key string) string { return lbs[key] }
func (lbs labels) set(key, val string) { lbs[key] = val }
func (lbs labels) keys() (ls []string) {
for key := range lbs {
ls = append(ls, key)
}
return
}
func (lbs labels) match(set labels) bool {
for _, key := range set.keys() {
if lbs.get(key) != set.get(key) {
return false
}
}
return true
}
func (lbs labels) toMap() map[string]string { return lbs }
func (lbs *labels) fromMap(kvs map[string]string) {
for k, v := range kvs {
lbs.set(k, v)
}
}

155
vendor/github.com/rusenask/k8s-kv/kv/kv_test.go generated vendored Normal file
View File

@ -0,0 +1,155 @@
package kv
import (
"fmt"
"testing"
meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/pkg/api/v1"
)
type fakeImplementer struct {
getcfgMap *v1.ConfigMap
createdMap *v1.ConfigMap
updatedMap *v1.ConfigMap
deletedName string
deletedOptions *meta_v1.DeleteOptions
}
func (i *fakeImplementer) Get(name string, options meta_v1.GetOptions) (*v1.ConfigMap, error) {
return i.getcfgMap, nil
}
func (i *fakeImplementer) Create(cfgMap *v1.ConfigMap) (*v1.ConfigMap, error) {
i.createdMap = cfgMap
return i.createdMap, nil
}
func (i *fakeImplementer) Update(cfgMap *v1.ConfigMap) (*v1.ConfigMap, error) {
i.updatedMap = cfgMap
return i.updatedMap, nil
}
func (i *fakeImplementer) Delete(name string, options *meta_v1.DeleteOptions) error {
i.deletedName = name
i.deletedOptions = options
return nil
}
func TestGetMap(t *testing.T) {
fi := &fakeImplementer{
getcfgMap: &v1.ConfigMap{
Data: map[string]string{
"foo": "bar",
},
},
}
kv, err := New(fi, "app", "b1")
if err != nil {
t.Fatalf("failed to get kv: %s", err)
}
cfgMap, err := kv.getMap()
if err != nil {
t.Fatalf("failed to get map: %s", err)
}
if cfgMap.Data["foo"] != "bar" {
t.Errorf("cfgMap.Data is missing expected key")
}
}
func TestGet(t *testing.T) {
im := map[string][]byte{
"foo": []byte("bar"),
}
fi := &fakeImplementer{
getcfgMap: &v1.ConfigMap{
Data: map[string]string{},
},
}
kv, err := New(fi, "app", "b1")
if err != nil {
t.Fatalf("failed to get kv: %s", err)
}
cfgMap, _ := kv.getMap()
kv.saveInternalMap(cfgMap, im)
val, err := kv.Get("foo")
if err != nil {
t.Fatalf("failed to get key: %s", err)
}
if string(val) != "bar" {
t.Errorf("expected 'bar' but got: %s", string(val))
}
}
func TestUpdate(t *testing.T) {
im := map[string][]byte{
"a": []byte("a-val"),
"b": []byte("b-val"),
"c": []byte("c-val"),
"d": []byte("d-val"),
}
fi := &fakeImplementer{
getcfgMap: &v1.ConfigMap{
Data: map[string]string{},
},
}
kv, err := New(fi, "app", "b1")
if err != nil {
t.Fatalf("failed to get kv: %s", err)
}
cfgMap, _ := kv.getMap()
kv.saveInternalMap(cfgMap, im)
err = kv.Put("b", []byte("updated"))
if err != nil {
t.Fatalf("failed to get key: %s", err)
}
updatedIm, err := decodeInternalMap(kv.serializer, fi.updatedMap.Data[dataKey])
if err != nil {
t.Fatalf("failed to decode internal map: %s", err)
}
if string(updatedIm["b"]) != "updated" {
t.Errorf("b value was not updated")
}
}
func TestEncodeInternal(t *testing.T) {
serializer := DefaultSerializer()
im := make(map[string][]byte)
for i := 0; i < 100; i++ {
im[fmt.Sprintf("foo-%d", i)] = []byte(fmt.Sprintf("some important data here %d", i))
}
encoded, err := encodeInternalMap(serializer, im)
if err != nil {
t.Fatalf("failed to encode map: %s", err)
}
decoded, err := decodeInternalMap(serializer, encoded)
if err != nil {
t.Fatalf("failed to decode map: %s", err)
}
if string(decoded["foo-1"]) != "some important data here 1" {
t.Errorf("expected to find 'some important data here 1' but got: %s", string(decoded["foo-1"]))
}
}

56
vendor/github.com/rusenask/k8s-kv/kv/serialize.go generated vendored Normal file
View File

@ -0,0 +1,56 @@
package kv
import (
"bytes"
"encoding/gob"
"sync"
)
var bufferPool = sync.Pool{New: allocBuffer}
func allocBuffer() interface{} {
return &bytes.Buffer{}
}
func getBuffer() *bytes.Buffer {
return bufferPool.Get().(*bytes.Buffer)
}
func releaseBuffer(v *bytes.Buffer) {
v.Reset()
v.Grow(0)
bufferPool.Put(v)
}
// Serializer - generic serializer interface
type Serializer interface {
Encode(source interface{}) ([]byte, error)
Decode(data []byte, target interface{}) error
}
// DefaultSerializer - returns default serializer
func DefaultSerializer() Serializer {
return &GobSerializer{}
}
// GobSerializer - gob based serializer
type GobSerializer struct{}
// Encode - encodes source into bytes using Gob encoder
func (s *GobSerializer) Encode(source interface{}) ([]byte, error) {
buf := getBuffer()
defer releaseBuffer(buf)
enc := gob.NewEncoder(buf)
err := enc.Encode(source)
if err != nil {
return nil, err
}
return buf.Bytes(), nil
}
// Decode - decodes given bytes into target struct
func (s *GobSerializer) Decode(data []byte, target interface{}) error {
buf := bytes.NewBuffer(data)
dec := gob.NewDecoder(buf)
return dec.Decode(target)
}

View File

@ -0,0 +1,222 @@
package tests
import (
"fmt"
"testing"
"github.com/rusenask/k8s-kv/kv"
"k8s.io/client-go/kubernetes"
core_v1 "k8s.io/client-go/kubernetes/typed/core/v1"
"k8s.io/client-go/tools/clientcmd"
)
const clusterConfig = ".kubeconfig"
const testingNamespace = "default"
func getImplementer(t *testing.T) (implementer core_v1.ConfigMapInterface) {
cfg, err := clientcmd.BuildConfigFromFlags("", clusterConfig)
if err != nil {
t.Fatalf("failed to get config: %s", err)
}
client, err := kubernetes.NewForConfig(cfg)
if err != nil {
t.Fatalf("failed to create client: %s", err)
}
return client.ConfigMaps(testingNamespace)
}
func TestPut(t *testing.T) {
impl := getImplementer(t)
kv, err := kv.New(impl, "test", "testput")
if err != nil {
t.Fatalf("failed to create kv: %s", err)
}
defer kv.Teardown()
err = kv.Put("key", []byte("val"))
if err != nil {
t.Errorf("failed to put: %s", err)
}
}
func TestPutDirectoryKeys(t *testing.T) {
impl := getImplementer(t)
kv, err := kv.New(impl, "test", "testputdirectorykeys")
if err != nil {
t.Fatalf("failed to create kv: %s", err)
}
defer kv.Teardown()
err = kv.Put("/somedir/key-here", []byte("val"))
if err != nil {
t.Errorf("failed to put: %s", err)
}
val, err := kv.Get("/somedir/key-here")
if err != nil {
t.Errorf("failed to get key: %s", err)
}
if string(val) != "val" {
t.Errorf("unexpected return: %s", string(val))
}
}
func TestGet(t *testing.T) {
impl := getImplementer(t)
kv, err := kv.New(impl, "test", "testget")
if err != nil {
t.Fatalf("failed to create kv: %s", err)
}
defer kv.Teardown()
err = kv.Put("foo", []byte("bar"))
if err != nil {
t.Errorf("failed to put: %s", err)
}
// getting it back
val, err := kv.Get("foo")
if err != nil {
t.Errorf("failed to get: %s", err)
}
if string(val) != "bar" {
t.Errorf("expected 'bar' but got: '%s'", string(val))
}
}
func TestDelete(t *testing.T) {
impl := getImplementer(t)
kvdb, err := kv.New(impl, "test", "testdelete")
if err != nil {
t.Fatalf("failed to create kv: %s", err)
}
defer kvdb.Teardown()
err = kvdb.Put("foo", []byte("bar"))
if err != nil {
t.Errorf("failed to put: %s", err)
}
// getting it back
val, err := kvdb.Get("foo")
if err != nil {
t.Errorf("failed to get: %s", err)
}
if string(val) != "bar" {
t.Errorf("expected 'bar' but got: '%s'", string(val))
}
// deleting it
err = kvdb.Delete("foo")
if err != nil {
t.Errorf("got error while deleting: %s", err)
}
_, err = kvdb.Get("foo")
if err != kv.ErrNotFound {
t.Errorf("expected to get an error on deleted key")
}
}
func TestList(t *testing.T) {
count := 3
impl := getImplementer(t)
kv, err := kv.New(impl, "test", "testlist")
if err != nil {
t.Fatalf("failed to create kv: %s", err)
}
defer kv.Teardown()
for i := 0; i < count; i++ {
err = kv.Put(fmt.Sprint(i), []byte(fmt.Sprintf("bar-%d", i)))
if err != nil {
t.Errorf("failed to put: %s", err)
}
}
items, err := kv.List("")
if err != nil {
t.Fatalf("failed to list items, error: %s", err)
}
if len(items) != count {
t.Errorf("expected %d items, got: %d", count, len(items))
}
if string(items["0"]) != "bar-0" {
t.Errorf("unexpected value on '0': %s", items["0"])
}
if string(items["1"]) != "bar-1" {
t.Errorf("unexpected value on '1': %s", items["1"])
}
if string(items["2"]) != "bar-2" {
t.Errorf("unexpected value on '2': %s", items["2"])
}
}
func TestListPrefix(t *testing.T) {
impl := getImplementer(t)
kv, err := kv.New(impl, "test", "testlistprefix")
if err != nil {
t.Fatalf("failed to create kv: %s", err)
}
defer kv.Teardown()
err = kv.Put("aaa", []byte("aaa"))
if err != nil {
t.Errorf("failed to put key, error: %s", err)
}
err = kv.Put("aaaaa", []byte("aaa"))
if err != nil {
t.Errorf("failed to put key, error: %s", err)
}
err = kv.Put("aaaaaaa", []byte("aaa"))
if err != nil {
t.Errorf("failed to put key, error: %s", err)
}
err = kv.Put("bbb", []byte("bbb"))
if err != nil {
t.Errorf("failed to put key, error: %s", err)
}
err = kv.Put("bbbbb", []byte("bbb"))
if err != nil {
t.Errorf("failed to put key, error: %s", err)
}
err = kv.Put("bbbbbbb", []byte("bbb"))
if err != nil {
t.Errorf("failed to put key, error: %s", err)
}
items, err := kv.List("aaa")
if err != nil {
t.Fatalf("failed to list items, error: %s", err)
}
if len(items) != 3 {
t.Errorf("expected %d items, got: %d", 3, len(items))
}
if string(items["aaa"]) != "aaa" {
t.Errorf("unexpected value on 'aaa': %s", items["aaa"])
}
if string(items["aaaaa"]) != "aaa" {
t.Errorf("unexpected value on 'aaaaa': %s", items["aaaaa"])
}
if string(items["aaaaaaa"]) != "aaa" {
t.Errorf("unexpected value on 'aaaaaaa': %s", items["aaaaaaa"])
}
}

View File

@ -0,0 +1,69 @@
package logrus_test
import (
"github.com/sirupsen/logrus"
"os"
)
func Example_basic() {
var log = logrus.New()
log.Formatter = new(logrus.JSONFormatter)
log.Formatter = new(logrus.TextFormatter) //default
log.Formatter.(*logrus.TextFormatter).DisableTimestamp = true // remove timestamp from test output
log.Level = logrus.DebugLevel
log.Out = os.Stdout
// file, err := os.OpenFile("logrus.log", os.O_CREATE|os.O_WRONLY, 0666)
// if err == nil {
// log.Out = file
// } else {
// log.Info("Failed to log to file, using default stderr")
// }
defer func() {
err := recover()
if err != nil {
entry := err.(*logrus.Entry)
log.WithFields(logrus.Fields{
"omg": true,
"err_animal": entry.Data["animal"],
"err_size": entry.Data["size"],
"err_level": entry.Level,
"err_message": entry.Message,
"number": 100,
}).Error("The ice breaks!") // or use Fatal() to force the process to exit with a nonzero code
}
}()
log.WithFields(logrus.Fields{
"animal": "walrus",
"number": 8,
}).Debug("Started observing beach")
log.WithFields(logrus.Fields{
"animal": "walrus",
"size": 10,
}).Info("A group of walrus emerges from the ocean")
log.WithFields(logrus.Fields{
"omg": true,
"number": 122,
}).Warn("The group's number increased tremendously!")
log.WithFields(logrus.Fields{
"temperature": -4,
}).Debug("Temperature changes")
log.WithFields(logrus.Fields{
"animal": "orca",
"size": 9009,
}).Panic("It's over 9000!")
// Output:
// level=debug msg="Started observing beach" animal=walrus number=8
// level=info msg="A group of walrus emerges from the ocean" animal=walrus size=10
// level=warning msg="The group's number increased tremendously!" number=122 omg=true
// level=debug msg="Temperature changes" temperature=-4
// level=panic msg="It's over 9000!" animal=orca size=9009
// level=error msg="The ice breaks!" err_animal=orca err_level=panic err_message="It's over 9000!" err_size=9009 number=100 omg=true
}

35
vendor/github.com/sirupsen/logrus/example_hook_test.go generated vendored Normal file
View File

@ -0,0 +1,35 @@
package logrus_test
import (
"github.com/sirupsen/logrus"
"gopkg.in/gemnasium/logrus-airbrake-hook.v2"
"os"
)
func Example_hook() {
var log = logrus.New()
log.Formatter = new(logrus.TextFormatter) // default
log.Formatter.(*logrus.TextFormatter).DisableTimestamp = true // remove timestamp from test output
log.Hooks.Add(airbrake.NewHook(123, "xyz", "development"))
log.Out = os.Stdout
log.WithFields(logrus.Fields{
"animal": "walrus",
"size": 10,
}).Info("A group of walrus emerges from the ocean")
log.WithFields(logrus.Fields{
"omg": true,
"number": 122,
}).Warn("The group's number increased tremendously!")
log.WithFields(logrus.Fields{
"omg": true,
"number": 100,
}).Error("The ice breaks!")
// Output:
// level=info msg="A group of walrus emerges from the ocean" animal=walrus size=10
// level=warning msg="The group's number increased tremendously!" number=122 omg=true
// level=error msg="The ice breaks!" number=100 omg=true
}

View File

@ -167,9 +167,8 @@ func core(out *[64]byte, in *[16]byte, k *[32]byte) {
}
// XORKeyStream crypts bytes from in to out using the given key and counters.
// In and out may be the same slice but otherwise should not overlap. Counter
// contains the raw ChaCha20 counter bytes (i.e. block counter followed by
// nonce).
// In and out must overlap entirely or not at all. Counter contains the raw
// ChaCha20 counter bytes (i.e. block counter followed by nonce).
func XORKeyStream(out, in []byte, counter *[16]byte, key *[32]byte) {
var block [64]byte
var counterCopy [16]byte

58
vendor/golang.org/x/crypto/nacl/auth/auth.go generated vendored Normal file
View File

@ -0,0 +1,58 @@
// Copyright 2017 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
/*
Package auth authenticates a message using a secret key.
The Sum function, viewed as a function of the message for a uniform random
key, is designed to meet the standard notion of unforgeability. This means
that an attacker cannot find authenticators for any messages not authenticated
by the sender, even if the attacker has adaptively influenced the messages
authenticated by the sender. For a formal definition see, e.g., Section 2.4
of Bellare, Kilian, and Rogaway, "The security of the cipher block chaining
message authentication code," Journal of Computer and System Sciences 61 (2000),
362399; http://www-cse.ucsd.edu/~mihir/papers/cbc.html.
auth does not make any promises regarding "strong" unforgeability; perhaps
one valid authenticator can be converted into another valid authenticator for
the same message. NaCl also does not make any promises regarding "truncated
unforgeability."
This package is interoperable with NaCl: https://nacl.cr.yp.to/auth.html.
*/
package auth
import (
"crypto/hmac"
"crypto/sha512"
)
const (
// Size is the size, in bytes, of an authenticated digest.
Size = 32
// KeySize is the size, in bytes, of an authentication key.
KeySize = 32
)
// Sum generates an authenticator for m using a secret key and returns the
// 32-byte digest.
func Sum(m []byte, key *[KeySize]byte) *[Size]byte {
mac := hmac.New(sha512.New, key[:])
mac.Write(m)
out := new([KeySize]byte)
copy(out[:], mac.Sum(nil)[:Size])
return out
}
// Verify checks that digest is a valid authenticator of message m under the
// given secret key. Verify does not leak timing information.
func Verify(digest []byte, m []byte, key *[32]byte) bool {
if len(digest) != Size {
return false
}
mac := hmac.New(sha512.New, key[:])
mac.Write(m)
expectedMAC := mac.Sum(nil) // first 256 bits of 512-bit sum
return hmac.Equal(digest, expectedMAC[:Size])
}

172
vendor/golang.org/x/crypto/nacl/auth/auth_test.go generated vendored Normal file
View File

@ -0,0 +1,172 @@
// Copyright 2017 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package auth
import (
"bytes"
rand "crypto/rand"
mrand "math/rand"
"testing"
)
// Test cases are from RFC 4231, and match those present in the tests directory
// of the download here: https://nacl.cr.yp.to/install.html
var testCases = []struct {
key [32]byte
msg []byte
out [32]byte
}{
{
key: [32]byte{
0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b,
0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b,
0x0b, 0x0b, 0x0b, 0x0b,
},
msg: []byte("Hi There"),
out: [32]byte{
0x87, 0xaa, 0x7c, 0xde, 0xa5, 0xef, 0x61, 0x9d,
0x4f, 0xf0, 0xb4, 0x24, 0x1a, 0x1d, 0x6c, 0xb0,
0x23, 0x79, 0xf4, 0xe2, 0xce, 0x4e, 0xc2, 0x78,
0x7a, 0xd0, 0xb3, 0x05, 0x45, 0xe1, 0x7c, 0xde,
},
},
{
key: [32]byte{'J', 'e', 'f', 'e'},
msg: []byte("what do ya want for nothing?"),
out: [32]byte{
0x16, 0x4b, 0x7a, 0x7b, 0xfc, 0xf8, 0x19, 0xe2,
0xe3, 0x95, 0xfb, 0xe7, 0x3b, 0x56, 0xe0, 0xa3,
0x87, 0xbd, 0x64, 0x22, 0x2e, 0x83, 0x1f, 0xd6,
0x10, 0x27, 0x0c, 0xd7, 0xea, 0x25, 0x05, 0x54,
},
},
{
key: [32]byte{
0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa,
0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa,
0xaa, 0xaa, 0xaa, 0xaa,
},
msg: []byte{ // 50 bytes of 0xdd
0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd,
0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd,
0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd,
0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd,
0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd,
0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd,
0xdd, 0xdd,
},
out: [32]byte{
0xfa, 0x73, 0xb0, 0x08, 0x9d, 0x56, 0xa2, 0x84,
0xef, 0xb0, 0xf0, 0x75, 0x6c, 0x89, 0x0b, 0xe9,
0xb1, 0xb5, 0xdb, 0xdd, 0x8e, 0xe8, 0x1a, 0x36,
0x55, 0xf8, 0x3e, 0x33, 0xb2, 0x27, 0x9d, 0x39,
},
},
{
key: [32]byte{
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10,
0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18,
0x19,
},
msg: []byte{
0xcd, 0xcd, 0xcd, 0xcd, 0xcd, 0xcd, 0xcd, 0xcd,
0xcd, 0xcd, 0xcd, 0xcd, 0xcd, 0xcd, 0xcd, 0xcd,
0xcd, 0xcd, 0xcd, 0xcd, 0xcd, 0xcd, 0xcd, 0xcd,
0xcd, 0xcd, 0xcd, 0xcd, 0xcd, 0xcd, 0xcd, 0xcd,
0xcd, 0xcd, 0xcd, 0xcd, 0xcd, 0xcd, 0xcd, 0xcd,
0xcd, 0xcd, 0xcd, 0xcd, 0xcd, 0xcd, 0xcd, 0xcd,
0xcd, 0xcd,
},
out: [32]byte{
0xb0, 0xba, 0x46, 0x56, 0x37, 0x45, 0x8c, 0x69,
0x90, 0xe5, 0xa8, 0xc5, 0xf6, 0x1d, 0x4a, 0xf7,
0xe5, 0x76, 0xd9, 0x7f, 0xf9, 0x4b, 0x87, 0x2d,
0xe7, 0x6f, 0x80, 0x50, 0x36, 0x1e, 0xe3, 0xdb,
},
},
}
func TestSum(t *testing.T) {
for i, test := range testCases {
tag := Sum(test.msg, &test.key)
if !bytes.Equal(tag[:], test.out[:]) {
t.Errorf("#%d: Sum: got\n%x\nwant\n%x", i, tag, test.out)
}
}
}
func TestVerify(t *testing.T) {
wrongMsg := []byte("unknown msg")
for i, test := range testCases {
if !Verify(test.out[:], test.msg, &test.key) {
t.Errorf("#%d: Verify(%x, %q, %x) failed", i, test.out, test.msg, test.key)
}
if Verify(test.out[:], wrongMsg, &test.key) {
t.Errorf("#%d: Verify(%x, %q, %x) unexpectedly passed", i, test.out, wrongMsg, test.key)
}
}
}
func TestStress(t *testing.T) {
if testing.Short() {
t.Skip("exhaustiveness test")
}
var key [32]byte
msg := make([]byte, 10000)
prng := mrand.New(mrand.NewSource(0))
// copied from tests/auth5.c in nacl
for i := 0; i < 10000; i++ {
if _, err := rand.Read(key[:]); err != nil {
t.Fatal(err)
}
if _, err := rand.Read(msg[:i]); err != nil {
t.Fatal(err)
}
tag := Sum(msg[:i], &key)
if !Verify(tag[:], msg[:i], &key) {
t.Errorf("#%d: unexpected failure from Verify", i)
}
if i > 0 {
msgIndex := prng.Intn(i)
oldMsgByte := msg[msgIndex]
msg[msgIndex] += byte(1 + prng.Intn(255))
if Verify(tag[:], msg[:i], &key) {
t.Errorf("#%d: unexpected success from Verify after corrupting message", i)
}
msg[msgIndex] = oldMsgByte
tag[prng.Intn(len(tag))] += byte(1 + prng.Intn(255))
if Verify(tag[:], msg[:i], &key) {
t.Errorf("#%d: unexpected success from Verify after corrupting authenticator", i)
}
}
}
}
func BenchmarkAuth(b *testing.B) {
var key [32]byte
if _, err := rand.Read(key[:]); err != nil {
b.Fatal(err)
}
buf := make([]byte, 1024)
if _, err := rand.Read(buf[:]); err != nil {
b.Fatal(err)
}
b.SetBytes(int64(len(buf)))
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
tag := Sum(buf, &key)
if Verify(tag[:], buf, &key) == false {
b.Fatal("unexpected failure from Verify")
}
}
}

36
vendor/golang.org/x/crypto/nacl/auth/example_test.go generated vendored Normal file
View File

@ -0,0 +1,36 @@
// Copyright 2017 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package auth_test
import (
"encoding/hex"
"fmt"
"golang.org/x/crypto/nacl/auth"
)
func Example() {
// Load your secret key from a safe place and reuse it across multiple
// Sum calls. (Obviously don't use this example key for anything
// real.) If you want to convert a passphrase to a key, use a suitable
// package like bcrypt or scrypt.
secretKeyBytes, err := hex.DecodeString("6368616e676520746869732070617373776f726420746f206120736563726574")
if err != nil {
panic(err)
}
var secretKey [32]byte
copy(secretKey[:], secretKeyBytes)
mac := auth.Sum([]byte("hello world"), &secretKey)
fmt.Printf("%x\n", *mac)
result := auth.Verify(mac[:], []byte("hello world"), &secretKey)
fmt.Println(result)
badResult := auth.Verify(mac[:], []byte("different message"), &secretKey)
fmt.Println(badResult)
// Output: eca5a521f3d77b63f567fb0cb6f5f2d200641bc8dada42f60c5f881260c30317
// true
// false
}

View File

@ -3,7 +3,7 @@
// license that can be found in the LICENSE file.
/*
Package box authenticates and encrypts messages using public-key cryptography.
Package box authenticates and encrypts small messages using public-key cryptography.
Box uses Curve25519, XSalsa20 and Poly1305 to encrypt and authenticate
messages. The length of messages is not hidden.
@ -13,6 +13,23 @@ example, by using nonce 1 for the first message, nonce 2 for the second
message, etc. Nonces are long enough that randomly generated nonces have
negligible risk of collision.
Messages should be small because:
1. The whole message needs to be held in memory to be processed.
2. Using large messages pressures implementations on small machines to decrypt
and process plaintext before authenticating it. This is very dangerous, and
this API does not allow it, but a protocol that uses excessive message sizes
might present some implementations with no other choice.
3. Fixed overheads will be sufficiently amortised by messages as small as 8KB.
4. Performance may be improved by working with messages that fit into data caches.
Thus large amounts of data should be chunked so that each message is small.
(Each message still needs a unique nonce.) If in doubt, 16KB is a reasonable
chunk size.
This package is interoperable with NaCl: https://nacl.cr.yp.to/box.html.
*/
package box // import "golang.org/x/crypto/nacl/box"
@ -56,7 +73,7 @@ func Precompute(sharedKey, peersPublicKey, privateKey *[32]byte) {
}
// Seal appends an encrypted and authenticated copy of message to out, which
// will be Overhead bytes longer than the original and must not overlap. The
// will be Overhead bytes longer than the original and must not overlap it. The
// nonce must be unique for each distinct message for a given pair of keys.
func Seal(out, message []byte, nonce *[24]byte, peersPublicKey, privateKey *[32]byte) []byte {
var sharedKey [32]byte

View File

@ -13,6 +13,23 @@ example, by using nonce 1 for the first message, nonce 2 for the second
message, etc. Nonces are long enough that randomly generated nonces have
negligible risk of collision.
Messages should be small because:
1. The whole message needs to be held in memory to be processed.
2. Using large messages pressures implementations on small machines to decrypt
and process plaintext before authenticating it. This is very dangerous, and
this API does not allow it, but a protocol that uses excessive message sizes
might present some implementations with no other choice.
3. Fixed overheads will be sufficiently amortised by messages as small as 8KB.
4. Performance may be improved by working with messages that fit into data caches.
Thus large amounts of data should be chunked so that each message is small.
(Each message still needs a unique nonce.) If in doubt, 16KB is a reasonable
chunk size.
This package is interoperable with NaCl: https://nacl.cr.yp.to/secretbox.html.
*/
package secretbox // import "golang.org/x/crypto/nacl/secretbox"

View File

@ -12,7 +12,10 @@ import (
)
func TestKeyExpiry(t *testing.T) {
kring, _ := ReadKeyRing(readerFromHex(expiringKeyHex))
kring, err := ReadKeyRing(readerFromHex(expiringKeyHex))
if err != nil {
t.Fatal(err)
}
entity := kring[0]
const timeFormat = "2006-01-02"
@ -104,7 +107,10 @@ func TestGoodCrossSignature(t *testing.T) {
// TestExternallyRevokableKey attempts to load and parse a key with a third party revocation permission.
func TestExternallyRevocableKey(t *testing.T) {
kring, _ := ReadKeyRing(readerFromHex(subkeyUsageHex))
kring, err := ReadKeyRing(readerFromHex(subkeyUsageHex))
if err != nil {
t.Fatal(err)
}
// The 0xA42704B92866382A key can be revoked by 0xBE3893CB843D0FE70C
// according to this signature that appears within the key:
@ -125,7 +131,10 @@ func TestExternallyRevocableKey(t *testing.T) {
}
func TestKeyRevocation(t *testing.T) {
kring, _ := ReadKeyRing(readerFromHex(revokedKeyHex))
kring, err := ReadKeyRing(readerFromHex(revokedKeyHex))
if err != nil {
t.Fatal(err)
}
// revokedKeyHex contains these keys:
// pub 1024R/9A34F7C0 2014-03-25 [revoked: 2014-03-25]
@ -145,7 +154,10 @@ func TestKeyRevocation(t *testing.T) {
}
func TestSubkeyRevocation(t *testing.T) {
kring, _ := ReadKeyRing(readerFromHex(revokedSubkeyHex))
kring, err := ReadKeyRing(readerFromHex(revokedSubkeyHex))
if err != nil {
t.Fatal(err)
}
// revokedSubkeyHex contains these keys:
// pub 1024R/4EF7E4BECCDE97F0 2014-03-25
@ -178,7 +190,10 @@ func TestSubkeyRevocation(t *testing.T) {
}
func TestKeyUsage(t *testing.T) {
kring, _ := ReadKeyRing(readerFromHex(subkeyUsageHex))
kring, err := ReadKeyRing(readerFromHex(subkeyUsageHex))
if err != nil {
t.Fatal(err)
}
// subkeyUsageHex contains these keys:
// pub 1024R/2866382A created: 2014-04-01 expires: never usage: SC

View File

@ -13,7 +13,7 @@ package salsa
func salsa2020XORKeyStream(out, in *byte, n uint64, nonce, key *byte)
// XORKeyStream crypts bytes from in to out using the given key and counters.
// In and out may be the same slice but otherwise should not overlap. Counter
// In and out must overlap entirely or not at all. Counter
// contains the raw salsa20 counter bytes (both nonce and block counter).
func XORKeyStream(out, in []byte, counter *[16]byte, key *[32]byte) {
if len(in) == 0 {

View File

@ -203,7 +203,7 @@ func core(out *[64]byte, in *[16]byte, k *[32]byte, c *[16]byte) {
}
// XORKeyStream crypts bytes from in to out using the given key and counters.
// In and out may be the same slice but otherwise should not overlap. Counter
// In and out must overlap entirely or not at all. Counter
// contains the raw salsa20 counter bytes (both nonce and block counter).
func XORKeyStream(out, in []byte, counter *[16]byte, key *[32]byte) {
var block [64]byte

View File

@ -27,8 +27,8 @@ import (
"golang.org/x/crypto/salsa20/salsa"
)
// XORKeyStream crypts bytes from in to out using the given key and nonce. In
// and out may be the same slice but otherwise should not overlap. Nonce must
// XORKeyStream crypts bytes from in to out using the given key and nonce.
// In and out must overlap entirely or not at all. Nonce must
// be either 8 or 24 bytes long.
func XORKeyStream(out, in []byte, nonce []byte, key *[32]byte) {
if len(out) < len(in) {

View File

@ -42,9 +42,8 @@ type state struct {
storage [maxRate]byte
// Specific to SHA-3 and SHAKE.
fixedOutput bool // whether this is a fixed-output-length instance
outputLen int // the default output size in bytes
state spongeDirection // whether the sponge is absorbing or squeezing
outputLen int // the default output size in bytes
state spongeDirection // whether the sponge is absorbing or squeezing
}
// BlockSize returns the rate of sponge underlying this hash function.

View File

@ -19,8 +19,8 @@ import (
"golang.org/x/crypto/ssh"
)
// startAgent executes ssh-agent, and returns a Agent interface to it.
func startAgent(t *testing.T) (client Agent, socket string, cleanup func()) {
// startOpenSSHAgent executes ssh-agent, and returns a Agent interface to it.
func startOpenSSHAgent(t *testing.T) (client Agent, socket string, cleanup func()) {
if testing.Short() {
// ssh-agent is not always available, and the key
// types supported vary by platform.
@ -79,16 +79,32 @@ func startAgent(t *testing.T) (client Agent, socket string, cleanup func()) {
}
}
func testAgent(t *testing.T, key interface{}, cert *ssh.Certificate, lifetimeSecs uint32) {
agent, _, cleanup := startAgent(t)
// startKeyringAgent uses Keyring to simulate a ssh-agent Server and returns a client.
func startKeyringAgent(t *testing.T) (client Agent, cleanup func()) {
c1, c2, err := netPipe()
if err != nil {
t.Fatalf("netPipe: %v", err)
}
go ServeAgent(NewKeyring(), c2)
return NewClient(c1), func() {
c1.Close()
c2.Close()
}
}
func testOpenSSHAgent(t *testing.T, key interface{}, cert *ssh.Certificate, lifetimeSecs uint32) {
agent, _, cleanup := startOpenSSHAgent(t)
defer cleanup()
testAgentInterface(t, agent, key, cert, lifetimeSecs)
}
func testKeyring(t *testing.T, key interface{}, cert *ssh.Certificate, lifetimeSecs uint32) {
a := NewKeyring()
testAgentInterface(t, a, key, cert, lifetimeSecs)
func testKeyringAgent(t *testing.T, key interface{}, cert *ssh.Certificate, lifetimeSecs uint32) {
agent, cleanup := startKeyringAgent(t)
defer cleanup()
testAgentInterface(t, agent, key, cert, lifetimeSecs)
}
func testAgentInterface(t *testing.T, agent Agent, key interface{}, cert *ssh.Certificate, lifetimeSecs uint32) {
@ -159,8 +175,8 @@ func testAgentInterface(t *testing.T, agent Agent, key interface{}, cert *ssh.Ce
func TestAgent(t *testing.T) {
for _, keyType := range []string{"rsa", "dsa", "ecdsa", "ed25519"} {
testAgent(t, testPrivateKeys[keyType], nil, 0)
testKeyring(t, testPrivateKeys[keyType], nil, 1)
testOpenSSHAgent(t, testPrivateKeys[keyType], nil, 0)
testKeyringAgent(t, testPrivateKeys[keyType], nil, 0)
}
}
@ -172,8 +188,8 @@ func TestCert(t *testing.T) {
}
cert.SignCert(rand.Reader, testSigners["ecdsa"])
testAgent(t, testPrivateKeys["rsa"], cert, 0)
testKeyring(t, testPrivateKeys["rsa"], cert, 1)
testOpenSSHAgent(t, testPrivateKeys["rsa"], cert, 0)
testKeyringAgent(t, testPrivateKeys["rsa"], cert, 0)
}
// netPipe is analogous to net.Pipe, but it uses a real net.Conn, and
@ -203,7 +219,7 @@ func netPipe() (net.Conn, net.Conn, error) {
}
func TestAuth(t *testing.T) {
agent, _, cleanup := startAgent(t)
agent, _, cleanup := startOpenSSHAgent(t)
defer cleanup()
a, b, err := netPipe()
@ -247,8 +263,14 @@ func TestAuth(t *testing.T) {
conn.Close()
}
func TestLockClient(t *testing.T) {
agent, _, cleanup := startAgent(t)
func TestLockOpenSSHAgent(t *testing.T) {
agent, _, cleanup := startOpenSSHAgent(t)
defer cleanup()
testLockAgent(agent, t)
}
func TestLockKeyringAgent(t *testing.T) {
agent, cleanup := startKeyringAgent(t)
defer cleanup()
testLockAgent(agent, t)
}
@ -308,10 +330,19 @@ func testLockAgent(agent Agent, t *testing.T) {
}
}
func TestAgentLifetime(t *testing.T) {
agent, _, cleanup := startAgent(t)
func testOpenSSHAgentLifetime(t *testing.T) {
agent, _, cleanup := startOpenSSHAgent(t)
defer cleanup()
testAgentLifetime(t, agent)
}
func testKeyringAgentLifetime(t *testing.T) {
agent, cleanup := startKeyringAgent(t)
defer cleanup()
testAgentLifetime(t, agent)
}
func testAgentLifetime(t *testing.T, agent Agent) {
for _, keyType := range []string{"rsa", "dsa", "ecdsa"} {
// Add private keys to the agent.
err := agent.Add(AddedKey{

View File

@ -106,7 +106,7 @@ func (s *server) processRequest(data []byte) (interface{}, error) {
return nil, s.agent.Lock(req.Passphrase)
case agentUnlock:
var req agentLockMsg
var req agentUnlockMsg
if err := ssh.Unmarshal(data, &req); err != nil {
return nil, err
}

View File

@ -43,7 +43,7 @@ func TestSetupForwardAgent(t *testing.T) {
defer a.Close()
defer b.Close()
_, socket, cleanup := startAgent(t)
_, socket, cleanup := startOpenSSHAgent(t)
defer cleanup()
serverConf := ssh.ServerConfig{

View File

@ -367,6 +367,17 @@ func (r *dsaPublicKey) Type() string {
return "ssh-dss"
}
func checkDSAParams(param *dsa.Parameters) error {
// SSH specifies FIPS 186-2, which only provided a single size
// (1024 bits) DSA key. FIPS 186-3 allows for larger key
// sizes, which would confuse SSH.
if l := param.P.BitLen(); l != 1024 {
return fmt.Errorf("ssh: unsupported DSA key size %d", l)
}
return nil
}
// parseDSA parses an DSA key according to RFC 4253, section 6.6.
func parseDSA(in []byte) (out PublicKey, rest []byte, err error) {
var w struct {
@ -377,13 +388,18 @@ func parseDSA(in []byte) (out PublicKey, rest []byte, err error) {
return nil, nil, err
}
param := dsa.Parameters{
P: w.P,
Q: w.Q,
G: w.G,
}
if err := checkDSAParams(&param); err != nil {
return nil, nil, err
}
key := &dsaPublicKey{
Parameters: dsa.Parameters{
P: w.P,
Q: w.Q,
G: w.G,
},
Y: w.Y,
Parameters: param,
Y: w.Y,
}
return key, w.Rest, nil
}
@ -630,19 +646,28 @@ func (k *ecdsaPublicKey) CryptoPublicKey() crypto.PublicKey {
}
// NewSignerFromKey takes an *rsa.PrivateKey, *dsa.PrivateKey,
// *ecdsa.PrivateKey or any other crypto.Signer and returns a corresponding
// Signer instance. ECDSA keys must use P-256, P-384 or P-521.
// *ecdsa.PrivateKey or any other crypto.Signer and returns a
// corresponding Signer instance. ECDSA keys must use P-256, P-384 or
// P-521. DSA keys must use parameter size L1024N160.
func NewSignerFromKey(key interface{}) (Signer, error) {
switch key := key.(type) {
case crypto.Signer:
return NewSignerFromSigner(key)
case *dsa.PrivateKey:
return &dsaPrivateKey{key}, nil
return newDSAPrivateKey(key)
default:
return nil, fmt.Errorf("ssh: unsupported key type %T", key)
}
}
func newDSAPrivateKey(key *dsa.PrivateKey) (Signer, error) {
if err := checkDSAParams(&key.PublicKey.Parameters); err != nil {
return nil, err
}
return &dsaPrivateKey{key}, nil
}
type wrappedSigner struct {
signer crypto.Signer
pubKey PublicKey

View File

@ -67,7 +67,7 @@ type ServerConfig struct {
PasswordCallback func(conn ConnMetadata, password []byte) (*Permissions, error)
// PublicKeyCallback, if non-nil, is called when a client
// offers a public key for authentication. It must return true
// offers a public key for authentication. It must return a nil error
// if the given public key can be used to authenticate the
// given user. For example, see CertChecker.Authenticate. A
// call to this function does not guarantee that the key

View File

@ -19,6 +19,8 @@ package terminal // import "golang.org/x/crypto/ssh/terminal"
import (
"syscall"
"unsafe"
"golang.org/x/sys/unix"
)
// State contains the state of a terminal.
@ -50,6 +52,8 @@ func MakeRaw(fd int) (*State, error) {
newState.Lflag &^= syscall.ECHO | syscall.ECHONL | syscall.ICANON | syscall.ISIG | syscall.IEXTEN
newState.Cflag &^= syscall.CSIZE | syscall.PARENB
newState.Cflag |= syscall.CS8
newState.Cc[unix.VMIN] = 1
newState.Cc[unix.VTIME] = 0
if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), ioctlWriteTermios, uintptr(unsafe.Pointer(&newState)), 0, 0, 0); err != 0 {
return nil, err
}

View File

@ -55,7 +55,7 @@ func NewCipher(cipherFunc func([]byte) (cipher.Block, error), key []byte) (c *Ci
}
// Encrypt encrypts a sector of plaintext and puts the result into ciphertext.
// Plaintext and ciphertext may be the same slice but should not overlap.
// Plaintext and ciphertext must overlap entirely or not at all.
// Sectors must be a multiple of 16 bytes and less than 2²⁴ bytes.
func (c *Cipher) Encrypt(ciphertext, plaintext []byte, sectorNum uint64) {
if len(ciphertext) < len(plaintext) {
@ -86,7 +86,7 @@ func (c *Cipher) Encrypt(ciphertext, plaintext []byte, sectorNum uint64) {
}
// Decrypt decrypts a sector of ciphertext and puts the result into plaintext.
// Plaintext and ciphertext may be the same slice but should not overlap.
// Plaintext and ciphertext must overlap entirely or not at all.
// Sectors must be a multiple of 16 bytes and less than 2²⁴ bytes.
func (c *Cipher) Decrypt(plaintext, ciphertext []byte, sectorNum uint64) {
if len(plaintext) < len(ciphertext) {

View File

@ -215,6 +215,9 @@ type AuthProvider struct {
// audiences: bookstore_android.apps.googleusercontent.com,
// bookstore_web.apps.googleusercontent.com
Audiences string `protobuf:"bytes,4,opt,name=audiences" json:"audiences,omitempty"`
// Redirect URL if JWT token is required but no present or is expired.
// Implement authorizationUrl of securityDefinitions in OpenAPI spec.
AuthorizationUrl string `protobuf:"bytes,5,opt,name=authorization_url,json=authorizationUrl" json:"authorization_url,omitempty"`
}
func (m *AuthProvider) Reset() { *m = AuthProvider{} }
@ -250,6 +253,13 @@ func (m *AuthProvider) GetAudiences() string {
return ""
}
func (m *AuthProvider) GetAuthorizationUrl() string {
if m != nil {
return m.AuthorizationUrl
}
return ""
}
// OAuth scopes are a way to define data and permissions on data. For example,
// there are scopes defined for "Read-only access to Google Calendar" and
// "Access to Cloud Platform". Users can consent to a scope for an application,
@ -349,33 +359,35 @@ func init() {
func init() { proto.RegisterFile("google/api/auth.proto", fileDescriptor0) }
var fileDescriptor0 = []byte{
// 437 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x64, 0x52, 0xcd, 0x6e, 0xd3, 0x40,
0x10, 0x96, 0x9d, 0xa6, 0x8d, 0x27, 0x55, 0x0a, 0x2b, 0x51, 0x99, 0x52, 0x20, 0xf2, 0x29, 0x5c,
0x1c, 0xa9, 0x45, 0x08, 0x09, 0x09, 0xd4, 0x22, 0x84, 0x7a, 0x22, 0x32, 0x42, 0x48, 0x5c, 0xac,
0x65, 0x3d, 0x38, 0x4b, 0xdd, 0x1d, 0xb3, 0x3f, 0xcd, 0x8d, 0x87, 0xe1, 0xc9, 0x78, 0x94, 0xca,
0x6b, 0x37, 0x71, 0xd2, 0xe3, 0x7c, 0x3f, 0x33, 0xf3, 0xcd, 0x2e, 0x3c, 0x29, 0x89, 0xca, 0x0a,
0xe7, 0xbc, 0x96, 0x73, 0xee, 0xec, 0x32, 0xad, 0x35, 0x59, 0x62, 0xd0, 0xc2, 0x29, 0xaf, 0xe5,
0xc9, 0x69, 0x5f, 0xa2, 0x14, 0x59, 0x6e, 0x25, 0x29, 0xd3, 0x2a, 0x93, 0xbf, 0x30, 0xb9, 0x70,
0x76, 0x89, 0xca, 0x4a, 0xe1, 0x09, 0xf6, 0x1a, 0x86, 0xda, 0x55, 0x68, 0xe2, 0xc1, 0x74, 0x30,
0x1b, 0x9f, 0xbd, 0x48, 0x37, 0xbd, 0xd2, 0x6d, 0x69, 0xe6, 0x2a, 0xcc, 0x5a, 0x31, 0x7b, 0x03,
0x51, 0xad, 0xe9, 0x56, 0x16, 0xa8, 0x4d, 0xbc, 0xe7, 0x9d, 0xf1, 0xae, 0x73, 0xd1, 0x09, 0xb2,
0x8d, 0x34, 0xf9, 0x1f, 0x00, 0x7b, 0xd8, 0x95, 0x9d, 0xc0, 0xc8, 0x60, 0x85, 0xc2, 0x92, 0x8e,
0x83, 0x69, 0x30, 0x8b, 0xb2, 0x75, 0xcd, 0xce, 0x61, 0x48, 0x4d, 0xd6, 0x38, 0x9c, 0x06, 0xb3,
0xf1, 0xd9, 0xf3, 0xfe, 0x98, 0x2f, 0x4d, 0xaf, 0x0c, 0xff, 0x38, 0xa9, 0xf1, 0x06, 0x95, 0x35,
0x59, 0xab, 0x65, 0x6f, 0x21, 0xe6, 0x55, 0x45, 0xab, 0x7c, 0x25, 0xed, 0x92, 0x9c, 0xcd, 0x85,
0xc6, 0xa2, 0x19, 0xca, 0xab, 0x78, 0x38, 0x0d, 0x66, 0xa3, 0xec, 0xd8, 0xf3, 0xdf, 0x5b, 0xfa,
0xe3, 0x9a, 0x65, 0x1f, 0xe0, 0x50, 0xf7, 0x1a, 0xc6, 0x07, 0x3e, 0xdc, 0xb3, 0xdd, 0x70, 0xbd,
0xa1, 0xd9, 0x96, 0x21, 0x21, 0x38, 0xec, 0xa7, 0x67, 0x13, 0x08, 0x65, 0xd1, 0xa5, 0x0a, 0x65,
0xc1, 0x8e, 0x61, 0x5f, 0x1a, 0xe3, 0x50, 0xfb, 0x40, 0x51, 0xd6, 0x55, 0xec, 0x29, 0x8c, 0x7e,
0xaf, 0xae, 0x4d, 0xee, 0xb4, 0x8c, 0x07, 0x9e, 0x39, 0x68, 0xea, 0x6f, 0x5a, 0xb2, 0x53, 0x88,
0xb8, 0x2b, 0x24, 0x2a, 0x81, 0xcd, 0xb5, 0x1b, 0x6e, 0x03, 0x24, 0xef, 0xe1, 0xf1, 0x83, 0x3b,
0xb0, 0x57, 0xf0, 0x48, 0x70, 0x45, 0x4a, 0x0a, 0x5e, 0xe5, 0x46, 0x50, 0x8d, 0xa6, 0xdb, 0xe1,
0x68, 0x8d, 0x7f, 0xf5, 0x70, 0xb2, 0x80, 0xa3, 0x1d, 0x3b, 0x7b, 0x09, 0xe3, 0xfb, 0x37, 0xcb,
0xd7, 0xcb, 0xc3, 0x3d, 0x74, 0x55, 0x6c, 0x6f, 0x14, 0xee, 0x6c, 0x74, 0x79, 0x0d, 0x13, 0x41,
0x37, 0xbd, 0x93, 0x5d, 0x46, 0xdd, 0x49, 0x2c, 0x2d, 0x82, 0x1f, 0x9f, 0x3a, 0xa2, 0xa4, 0x8a,
0xab, 0x32, 0x25, 0x5d, 0xce, 0x4b, 0x54, 0xfe, 0x83, 0xce, 0x5b, 0x8a, 0xd7, 0xd2, 0xf8, 0x1f,
0x6c, 0x50, 0xdf, 0x4a, 0x81, 0x82, 0xd4, 0x2f, 0x59, 0xbe, 0xdb, 0xaa, 0xfe, 0x85, 0x7b, 0x9f,
0x2f, 0x16, 0x57, 0x3f, 0xf7, 0xbd, 0xf1, 0xfc, 0x2e, 0x00, 0x00, 0xff, 0xff, 0xb9, 0x6d, 0xc6,
0x5e, 0x1c, 0x03, 0x00, 0x00,
// 465 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x64, 0x52, 0x5f, 0x6b, 0x13, 0x4f,
0x14, 0x65, 0x93, 0xa6, 0xcd, 0xde, 0x94, 0xb4, 0x1d, 0xf8, 0x95, 0xfd, 0xd5, 0xaa, 0x21, 0x4f,
0x11, 0x61, 0x03, 0xad, 0x88, 0x20, 0x28, 0xad, 0x88, 0xf4, 0xc9, 0x30, 0x52, 0x04, 0x5f, 0x96,
0x71, 0x76, 0xdc, 0x8c, 0x9d, 0xce, 0x5d, 0xe7, 0x4f, 0x03, 0x3e, 0xf8, 0x49, 0x7c, 0xf2, 0x93,
0xf9, 0x51, 0x64, 0x67, 0xb7, 0xc9, 0x6e, 0xfa, 0x78, 0xef, 0x39, 0xe7, 0xde, 0x7b, 0xce, 0x0c,
0xfc, 0x57, 0x20, 0x16, 0x4a, 0xcc, 0x59, 0x29, 0xe7, 0xcc, 0xbb, 0x65, 0x5a, 0x1a, 0x74, 0x48,
0xa0, 0x6e, 0xa7, 0xac, 0x94, 0x27, 0xa7, 0x6d, 0x8a, 0xd6, 0xe8, 0x98, 0x93, 0xa8, 0x6d, 0xcd,
0x9c, 0xfe, 0x82, 0xf1, 0x85, 0x77, 0x4b, 0xa1, 0x9d, 0xe4, 0x01, 0x20, 0x2f, 0x60, 0x60, 0xbc,
0x12, 0x36, 0xe9, 0x4f, 0xfa, 0xb3, 0xd1, 0xd9, 0x93, 0x74, 0x33, 0x2b, 0xed, 0x52, 0xa9, 0x57,
0x82, 0xd6, 0x64, 0xf2, 0x12, 0xe2, 0xd2, 0xe0, 0x9d, 0xcc, 0x85, 0xb1, 0xc9, 0x4e, 0x50, 0x26,
0xdb, 0xca, 0x45, 0x43, 0xa0, 0x1b, 0xea, 0xf4, 0x6f, 0x04, 0xe4, 0xe1, 0x54, 0x72, 0x02, 0x43,
0x2b, 0x94, 0xe0, 0x0e, 0x4d, 0x12, 0x4d, 0xa2, 0x59, 0x4c, 0xd7, 0x35, 0x39, 0x87, 0x01, 0x56,
0x5e, 0x93, 0xde, 0x24, 0x9a, 0x8d, 0xce, 0x1e, 0xb7, 0xd7, 0x7c, 0xac, 0x66, 0x51, 0xf1, 0xc3,
0x4b, 0x23, 0x6e, 0x85, 0x76, 0x96, 0xd6, 0x5c, 0xf2, 0x0a, 0x12, 0xa6, 0x14, 0xae, 0xb2, 0x95,
0x74, 0x4b, 0xf4, 0x2e, 0xe3, 0x46, 0xe4, 0xd5, 0x52, 0xa6, 0x92, 0xc1, 0x24, 0x9a, 0x0d, 0xe9,
0x71, 0xc0, 0x3f, 0xd7, 0xf0, 0xbb, 0x35, 0x4a, 0xde, 0xc2, 0xbe, 0x69, 0x0d, 0x4c, 0xf6, 0x82,
0xb9, 0x47, 0xdb, 0xe6, 0x5a, 0x4b, 0x69, 0x47, 0x30, 0xfd, 0x1d, 0xc1, 0x7e, 0xdb, 0x3e, 0x19,
0x43, 0x4f, 0xe6, 0x8d, 0xad, 0x9e, 0xcc, 0xc9, 0x31, 0xec, 0x4a, 0x6b, 0xbd, 0x30, 0xc1, 0x51,
0x4c, 0x9b, 0x8a, 0xfc, 0x0f, 0xc3, 0xef, 0xab, 0x1b, 0x9b, 0x79, 0x23, 0x93, 0x7e, 0x40, 0xf6,
0xaa, 0xfa, 0xda, 0x48, 0x72, 0x0a, 0x31, 0xf3, 0xb9, 0x14, 0x9a, 0x8b, 0x2a, 0xee, 0x0a, 0xdb,
0x34, 0xc8, 0x73, 0x38, 0xaa, 0x4c, 0xa3, 0x91, 0x3f, 0x43, 0xa4, 0x99, 0x37, 0xb5, 0xcb, 0x98,
0x1e, 0x76, 0x80, 0x6b, 0xa3, 0xa6, 0x6f, 0xe0, 0xe8, 0x41, 0x6a, 0xe4, 0x19, 0x1c, 0x72, 0xa6,
0x51, 0x4b, 0xce, 0x54, 0x66, 0x39, 0x96, 0xc2, 0x36, 0x07, 0x1f, 0xac, 0xfb, 0x9f, 0x42, 0x7b,
0xba, 0x80, 0x83, 0x2d, 0x39, 0x79, 0x0a, 0xa3, 0xfb, 0x17, 0xce, 0xd6, 0x4e, 0xe1, 0xbe, 0x75,
0x95, 0x77, 0xcf, 0xef, 0x6d, 0x9d, 0x7f, 0x79, 0x03, 0x63, 0x8e, 0xb7, 0xad, 0x80, 0x2f, 0xe3,
0x26, 0x3f, 0x87, 0x8b, 0xe8, 0xcb, 0xfb, 0x06, 0x28, 0x50, 0x31, 0x5d, 0xa4, 0x68, 0x8a, 0x79,
0x21, 0x74, 0xf8, 0xce, 0xf3, 0x1a, 0x62, 0xa5, 0xb4, 0xe1, 0xbf, 0x5b, 0x61, 0xee, 0x24, 0x17,
0x1c, 0xf5, 0x37, 0x59, 0xbc, 0xee, 0x54, 0x7f, 0x7a, 0x3b, 0x1f, 0x2e, 0x16, 0x57, 0x5f, 0x77,
0x83, 0xf0, 0xfc, 0x5f, 0x00, 0x00, 0x00, 0xff, 0xff, 0xe5, 0xa3, 0x9d, 0xc6, 0x4a, 0x03, 0x00,
0x00,
}

View File

@ -75,6 +75,8 @@ type CheckResponse struct {
CheckErrors []*CheckError `protobuf:"bytes,2,rep,name=check_errors,json=checkErrors" json:"check_errors,omitempty"`
// The actual config id used to process the request.
ServiceConfigId string `protobuf:"bytes,5,opt,name=service_config_id,json=serviceConfigId" json:"service_config_id,omitempty"`
// Feedback data returned from the server during processing a Check request.
CheckInfo *CheckResponse_CheckInfo `protobuf:"bytes,6,opt,name=check_info,json=checkInfo" json:"check_info,omitempty"`
}
func (m *CheckResponse) Reset() { *m = CheckResponse{} }
@ -103,6 +105,49 @@ func (m *CheckResponse) GetServiceConfigId() string {
return ""
}
func (m *CheckResponse) GetCheckInfo() *CheckResponse_CheckInfo {
if m != nil {
return m.CheckInfo
}
return nil
}
type CheckResponse_CheckInfo struct {
// Consumer info of this check.
ConsumerInfo *CheckResponse_ConsumerInfo `protobuf:"bytes,2,opt,name=consumer_info,json=consumerInfo" json:"consumer_info,omitempty"`
}
func (m *CheckResponse_CheckInfo) Reset() { *m = CheckResponse_CheckInfo{} }
func (m *CheckResponse_CheckInfo) String() string { return proto.CompactTextString(m) }
func (*CheckResponse_CheckInfo) ProtoMessage() {}
func (*CheckResponse_CheckInfo) Descriptor() ([]byte, []int) { return fileDescriptor5, []int{1, 0} }
func (m *CheckResponse_CheckInfo) GetConsumerInfo() *CheckResponse_ConsumerInfo {
if m != nil {
return m.ConsumerInfo
}
return nil
}
// `ConsumerInfo` provides information about the consumer project.
type CheckResponse_ConsumerInfo struct {
// The Google cloud project number, e.g. 1234567890. A value of 0 indicates
// no project number is found.
ProjectNumber int64 `protobuf:"varint,1,opt,name=project_number,json=projectNumber" json:"project_number,omitempty"`
}
func (m *CheckResponse_ConsumerInfo) Reset() { *m = CheckResponse_ConsumerInfo{} }
func (m *CheckResponse_ConsumerInfo) String() string { return proto.CompactTextString(m) }
func (*CheckResponse_ConsumerInfo) ProtoMessage() {}
func (*CheckResponse_ConsumerInfo) Descriptor() ([]byte, []int) { return fileDescriptor5, []int{1, 1} }
func (m *CheckResponse_ConsumerInfo) GetProjectNumber() int64 {
if m != nil {
return m.ProjectNumber
}
return 0
}
// Request message for the Report method.
type ReportRequest struct {
// The service name as specified in its service configuration. For example,
@ -168,8 +213,9 @@ type ReportResponse struct {
// `Operations` in the request succeeded. Each
// `Operation` that failed processing has a corresponding item
// in this list.
// 3. A failed RPC status indicates a complete failure where none of the
// `Operations` in the request succeeded.
// 3. A failed RPC status indicates a general non-deterministic failure.
// When this happens, it's impossible to know which of the
// 'Operations' in the request succeeded or failed.
ReportErrors []*ReportResponse_ReportError `protobuf:"bytes,1,rep,name=report_errors,json=reportErrors" json:"report_errors,omitempty"`
// The actual config id used to process the request.
ServiceConfigId string `protobuf:"bytes,2,opt,name=service_config_id,json=serviceConfigId" json:"service_config_id,omitempty"`
@ -224,6 +270,8 @@ func (m *ReportResponse_ReportError) GetStatus() *google_rpc.Status {
func init() {
proto.RegisterType((*CheckRequest)(nil), "google.api.servicecontrol.v1.CheckRequest")
proto.RegisterType((*CheckResponse)(nil), "google.api.servicecontrol.v1.CheckResponse")
proto.RegisterType((*CheckResponse_CheckInfo)(nil), "google.api.servicecontrol.v1.CheckResponse.CheckInfo")
proto.RegisterType((*CheckResponse_ConsumerInfo)(nil), "google.api.servicecontrol.v1.CheckResponse.ConsumerInfo")
proto.RegisterType((*ReportRequest)(nil), "google.api.servicecontrol.v1.ReportRequest")
proto.RegisterType((*ReportResponse)(nil), "google.api.servicecontrol.v1.ReportResponse")
proto.RegisterType((*ReportResponse_ReportError)(nil), "google.api.servicecontrol.v1.ReportResponse.ReportError")
@ -245,21 +293,25 @@ type ServiceControllerClient interface {
// operation is executed.
//
// If feasible, the client should cache the check results and reuse them for
// up to 60s. In case of server errors, the client may rely on the cached
// 60 seconds. In case of server errors, the client can rely on the cached
// results for longer time.
//
// NOTE: the `CheckRequest` has the size limit of 64KB.
//
// This method requires the `servicemanagement.services.check` permission
// on the specified service. For more information, see
// [Google Cloud IAM](https://cloud.google.com/iam).
Check(ctx context.Context, in *CheckRequest, opts ...grpc.CallOption) (*CheckResponse, error)
// Reports operations to Google Service Control. It should be called
// after the operation is completed.
// Reports operation results to Google Service Control, such as logs and
// metrics. It should be called after an operation is completed.
//
// If feasible, the client should aggregate reporting data for up to 5s to
// reduce API traffic. Limiting aggregation to 5s is to reduce data loss
// during client crashes. Clients should carefully choose the aggregation
// window to avoid data loss risk more than 0.01% for business and
// compliance reasons.
// If feasible, the client should aggregate reporting data for up to 5
// seconds to reduce API traffic. Limiting aggregation to 5 seconds is to
// reduce data loss during client crashes. Clients should carefully choose
// the aggregation time window to avoid data loss risk more than 0.01%
// for business and compliance reasons.
//
// NOTE: the `ReportRequest` has the size limit of 1MB.
//
// This method requires the `servicemanagement.services.report` permission
// on the specified service. For more information, see
@ -301,21 +353,25 @@ type ServiceControllerServer interface {
// operation is executed.
//
// If feasible, the client should cache the check results and reuse them for
// up to 60s. In case of server errors, the client may rely on the cached
// 60 seconds. In case of server errors, the client can rely on the cached
// results for longer time.
//
// NOTE: the `CheckRequest` has the size limit of 64KB.
//
// This method requires the `servicemanagement.services.check` permission
// on the specified service. For more information, see
// [Google Cloud IAM](https://cloud.google.com/iam).
Check(context.Context, *CheckRequest) (*CheckResponse, error)
// Reports operations to Google Service Control. It should be called
// after the operation is completed.
// Reports operation results to Google Service Control, such as logs and
// metrics. It should be called after an operation is completed.
//
// If feasible, the client should aggregate reporting data for up to 5s to
// reduce API traffic. Limiting aggregation to 5s is to reduce data loss
// during client crashes. Clients should carefully choose the aggregation
// window to avoid data loss risk more than 0.01% for business and
// compliance reasons.
// If feasible, the client should aggregate reporting data for up to 5
// seconds to reduce API traffic. Limiting aggregation to 5 seconds is to
// reduce data loss during client crashes. Clients should carefully choose
// the aggregation time window to avoid data loss risk more than 0.01%
// for business and compliance reasons.
//
// NOTE: the `ReportRequest` has the size limit of 1MB.
//
// This method requires the `servicemanagement.services.report` permission
// on the specified service. For more information, see
@ -385,39 +441,44 @@ func init() {
}
var fileDescriptor5 = []byte{
// 537 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x94, 0xcf, 0x6e, 0xd3, 0x40,
0x10, 0xc6, 0xb5, 0xee, 0x1f, 0xa9, 0xe3, 0x04, 0xd4, 0x3d, 0x40, 0x64, 0xf5, 0x90, 0xfa, 0x40,
0x23, 0x37, 0xd8, 0x6a, 0x10, 0x12, 0x0a, 0x27, 0x1a, 0x55, 0x55, 0x41, 0x82, 0xca, 0xb9, 0x21,
0x50, 0xb4, 0x38, 0x8b, 0xb1, 0x48, 0xbc, 0xcb, 0xae, 0x9b, 0x0b, 0xe2, 0xc2, 0x03, 0x70, 0x28,
0x6f, 0x80, 0x90, 0x38, 0xf0, 0x04, 0x3c, 0x07, 0xaf, 0xc0, 0x43, 0xc0, 0x0d, 0x79, 0x77, 0xed,
0x3a, 0xc2, 0x58, 0xee, 0x2d, 0x9e, 0x9d, 0xf9, 0xe6, 0xb7, 0xdf, 0x4c, 0x16, 0xee, 0xc7, 0x8c,
0xc5, 0x0b, 0x1a, 0x10, 0x9e, 0x04, 0x92, 0x8a, 0x55, 0x12, 0xd1, 0x88, 0xa5, 0x99, 0x60, 0x8b,
0x60, 0x75, 0x54, 0x44, 0x66, 0x26, 0xb4, 0xa0, 0xc2, 0xe7, 0x82, 0x65, 0x0c, 0xef, 0xe9, 0x32,
0x9f, 0xf0, 0xc4, 0x5f, 0x2f, 0xf3, 0x57, 0x47, 0xce, 0x5e, 0x45, 0x94, 0xa4, 0x29, 0xcb, 0x48,
0x96, 0xb0, 0x54, 0xea, 0x5a, 0xc7, 0x6f, 0x6c, 0x19, 0xbd, 0xa1, 0xd1, 0xdb, 0x19, 0x15, 0x82,
0x99, 0x5e, 0xce, 0xb0, 0x31, 0x9f, 0x71, 0x2a, 0x94, 0xbc, 0xc9, 0xbe, 0x6d, 0xb2, 0x05, 0x8f,
0x02, 0x99, 0x91, 0xec, 0xc2, 0xb4, 0x75, 0xbf, 0x22, 0xe8, 0x4c, 0x72, 0xf1, 0x90, 0xbe, 0xbb,
0xa0, 0x32, 0xc3, 0xfb, 0xd0, 0x29, 0xee, 0x97, 0x92, 0x25, 0xed, 0xa1, 0x3e, 0x1a, 0xec, 0x84,
0xb6, 0x89, 0x3d, 0x25, 0x4b, 0x8a, 0x4f, 0x60, 0xa7, 0xd4, 0xef, 0x59, 0x7d, 0x34, 0xb0, 0x47,
0x07, 0x7e, 0xd3, 0xd5, 0xfd, 0x67, 0x45, 0x7a, 0x78, 0x55, 0x89, 0x3d, 0xd8, 0xad, 0x38, 0xf9,
0x3a, 0x89, 0x67, 0xc9, 0xbc, 0xb7, 0xa9, 0xda, 0xdd, 0x34, 0x07, 0x13, 0x15, 0x3f, 0x9b, 0xbb,
0xdf, 0x11, 0x74, 0x0d, 0xa6, 0xe4, 0x2c, 0x95, 0x34, 0xe7, 0x2c, 0xa5, 0xf2, 0x42, 0xc3, 0x59,
0xc6, 0xce, 0xe6, 0xf8, 0x09, 0x74, 0x2a, 0xbe, 0xc9, 0x9e, 0xd5, 0xdf, 0x18, 0xd8, 0xa3, 0x41,
0x33, 0xaa, 0xea, 0x72, 0x92, 0x17, 0x84, 0x76, 0x54, 0xfe, 0x96, 0xf5, 0xb4, 0x5b, 0xf5, 0xb4,
0xdf, 0x10, 0x74, 0x43, 0xca, 0x99, 0xc8, 0xae, 0xe1, 0xea, 0x29, 0x40, 0x09, 0x5f, 0xb0, 0xb6,
0xb6, 0xb5, 0x52, 0x5a, 0x4f, 0xba, 0x51, 0x4f, 0xfa, 0x07, 0xc1, 0x8d, 0x82, 0xd4, 0x18, 0xfb,
0x12, 0xba, 0x42, 0x45, 0x0a, 0xdb, 0x90, 0x42, 0x79, 0xd0, 0x8c, 0xb2, 0x2e, 0x62, 0x3e, 0xb5,
0x8d, 0x1d, 0x71, 0xf5, 0xf1, 0x1f, 0x3a, 0xab, 0x96, 0xce, 0x79, 0x01, 0x76, 0x45, 0xa8, 0xcd,
0xc8, 0x3d, 0xd8, 0xd6, 0xeb, 0x6d, 0xf6, 0x12, 0x17, 0xd4, 0x82, 0x47, 0xfe, 0x54, 0x9d, 0x84,
0x26, 0x63, 0xf4, 0xc3, 0x82, 0xdd, 0x69, 0xd9, 0xd1, 0xfc, 0x93, 0xf1, 0x27, 0x04, 0x5b, 0x6a,
0x07, 0xb0, 0xd7, 0x62, 0x51, 0xcc, 0x7c, 0x9d, 0xc3, 0x56, 0xb9, 0xda, 0x1c, 0x77, 0xf8, 0xf1,
0xe7, 0xaf, 0xcf, 0xd6, 0x1d, 0x77, 0xbf, 0xf2, 0x98, 0xc8, 0xe0, 0x7d, 0x75, 0x41, 0x3e, 0x8c,
0xd5, 0xee, 0x8d, 0x91, 0x87, 0x2f, 0x11, 0x6c, 0x6b, 0x17, 0xf0, 0x61, 0xbb, 0x19, 0x68, 0xa4,
0xe1, 0x75, 0x06, 0xe6, 0xde, 0x55, 0x4c, 0x07, 0xae, 0xdb, 0xc4, 0xa4, 0x07, 0x39, 0x46, 0xde,
0xf1, 0x25, 0x82, 0x7e, 0xc4, 0x96, 0x8d, 0x2d, 0x8e, 0x6f, 0xfd, 0xe3, 0xee, 0x79, 0xfe, 0xe6,
0x9c, 0xa3, 0xe7, 0x8f, 0x4d, 0x5d, 0xcc, 0x16, 0x24, 0x8d, 0x7d, 0x26, 0xe2, 0x20, 0xa6, 0xa9,
0x7a, 0x91, 0x02, 0x7d, 0x44, 0x78, 0x22, 0xeb, 0xdf, 0xb6, 0x87, 0xeb, 0x91, 0xdf, 0x08, 0x7d,
0xb1, 0x36, 0x4f, 0x1f, 0x4d, 0x27, 0xaf, 0xb6, 0x95, 0xc0, 0xbd, 0xbf, 0x01, 0x00, 0x00, 0xff,
0xff, 0x6c, 0x58, 0x92, 0x07, 0xbe, 0x05, 0x00, 0x00,
// 619 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x54, 0xc1, 0x6e, 0xd3, 0x4c,
0x10, 0xd6, 0x3a, 0x6d, 0xa4, 0x4c, 0x9c, 0xfe, 0xea, 0x1e, 0x7e, 0x22, 0xab, 0x87, 0xd4, 0x12,
0x34, 0x4a, 0x8b, 0xad, 0x16, 0x55, 0x42, 0xe1, 0x44, 0xa3, 0xaa, 0x0a, 0x48, 0xa5, 0x72, 0x38,
0x21, 0xaa, 0xc8, 0xdd, 0x6c, 0x8c, 0x4b, 0xb2, 0x6b, 0xd6, 0x4e, 0x2e, 0x88, 0x0b, 0x0f, 0xc0,
0xa1, 0xbc, 0x01, 0xaa, 0xc4, 0x33, 0xf0, 0x1c, 0xbc, 0x02, 0x0f, 0x01, 0x37, 0x94, 0xdd, 0xb5,
0xeb, 0x08, 0x63, 0x92, 0x9b, 0xf7, 0xdb, 0x99, 0xf9, 0xbe, 0x9d, 0xf9, 0x3c, 0x70, 0x1c, 0x70,
0x1e, 0x4c, 0xa8, 0xeb, 0x47, 0xa1, 0x1b, 0x53, 0x31, 0x0f, 0x09, 0x25, 0x9c, 0x25, 0x82, 0x4f,
0xdc, 0xf9, 0x61, 0x8a, 0x0c, 0x35, 0x34, 0xa1, 0xc2, 0x89, 0x04, 0x4f, 0x38, 0xde, 0x51, 0x69,
0x8e, 0x1f, 0x85, 0xce, 0x72, 0x9a, 0x33, 0x3f, 0xb4, 0x76, 0x72, 0x45, 0x7d, 0xc6, 0x78, 0xe2,
0x27, 0x21, 0x67, 0xb1, 0xca, 0xb5, 0x9c, 0x52, 0x4a, 0xf2, 0x86, 0x92, 0xb7, 0x43, 0x2a, 0x04,
0xd7, 0x5c, 0xd6, 0x41, 0x69, 0x3c, 0x8f, 0xa8, 0x90, 0xe5, 0x75, 0xf4, 0x3d, 0x1d, 0x2d, 0x22,
0xe2, 0xc6, 0x89, 0x9f, 0xcc, 0x34, 0xad, 0x7d, 0x8b, 0xc0, 0xec, 0x2d, 0x8a, 0x7b, 0xf4, 0xdd,
0x8c, 0xc6, 0x09, 0xde, 0x05, 0x33, 0x7d, 0x1f, 0xf3, 0xa7, 0xb4, 0x89, 0x5a, 0xa8, 0x5d, 0xf3,
0xea, 0x1a, 0x3b, 0xf7, 0xa7, 0x14, 0x9f, 0x42, 0x2d, 0xab, 0xdf, 0x34, 0x5a, 0xa8, 0x5d, 0x3f,
0xda, 0x73, 0xca, 0x9e, 0xee, 0xbc, 0x48, 0xc3, 0xbd, 0xbb, 0x4c, 0xdc, 0x81, 0xed, 0x5c, 0x27,
0xc7, 0x61, 0x30, 0x0c, 0x47, 0xcd, 0x0d, 0x49, 0xf7, 0x9f, 0xbe, 0xe8, 0x49, 0xbc, 0x3f, 0xb2,
0x6f, 0x2b, 0xd0, 0xd0, 0x32, 0xe3, 0x88, 0xb3, 0x98, 0x2e, 0x74, 0x66, 0xa5, 0x16, 0x89, 0x5a,
0x67, 0x86, 0xf5, 0x47, 0xf8, 0x39, 0x98, 0xb9, 0xbe, 0xc5, 0x4d, 0xa3, 0x55, 0x69, 0xd7, 0x8f,
0xda, 0xe5, 0x52, 0x25, 0xcb, 0xe9, 0x22, 0xc1, 0xab, 0x93, 0xec, 0x3b, 0x2e, 0x56, 0xbb, 0x59,
0xa8, 0x16, 0xbf, 0x04, 0x50, 0xc4, 0x21, 0x1b, 0xf3, 0x66, 0x55, 0x76, 0xe8, 0x78, 0x05, 0xda,
0xf4, 0x71, 0xea, 0xd4, 0x67, 0x63, 0xee, 0xd5, 0x48, 0xfa, 0x69, 0x5d, 0x43, 0x2d, 0xc3, 0xf1,
0x25, 0x34, 0x08, 0x67, 0xf1, 0x6c, 0x4a, 0x85, 0x62, 0x51, 0x73, 0x78, 0xbc, 0x16, 0x8b, 0x2e,
0x20, 0x89, 0x4c, 0x92, 0x3b, 0x59, 0xc7, 0x60, 0xe6, 0x6f, 0xf1, 0x7d, 0xd8, 0x8a, 0x04, 0xbf,
0xa6, 0x24, 0x19, 0xb2, 0xd9, 0xf4, 0x8a, 0x0a, 0xd9, 0xef, 0x8a, 0xd7, 0xd0, 0xe8, 0xb9, 0x04,
0xed, 0xaf, 0x08, 0x1a, 0x1e, 0x8d, 0xb8, 0x48, 0xd6, 0xb0, 0xd3, 0x19, 0x40, 0x36, 0xb5, 0x74,
0x48, 0x2b, 0xfb, 0x29, 0x97, 0x5a, 0x3c, 0xa2, 0x4a, 0xb1, 0xa1, 0x7e, 0x21, 0xd8, 0x4a, 0x95,
0x6a, 0x47, 0x5d, 0x42, 0x43, 0x48, 0x24, 0xf5, 0x0b, 0x92, 0x52, 0xfe, 0xd1, 0xd2, 0xe5, 0x22,
0xfa, 0xa8, 0xfc, 0x63, 0x8a, 0xbb, 0xc3, 0x5f, 0xd4, 0x19, 0x85, 0xea, 0xac, 0xd7, 0x50, 0xcf,
0x15, 0x5a, 0xc5, 0xeb, 0x1d, 0xa8, 0xaa, 0xff, 0x5a, 0x1b, 0x01, 0xa7, 0xaa, 0x45, 0x44, 0x9c,
0x81, 0xbc, 0xf1, 0x74, 0xc4, 0xd1, 0x37, 0x03, 0xb6, 0x07, 0x19, 0xa3, 0x5e, 0x61, 0xf8, 0x13,
0x82, 0x4d, 0xe9, 0x0f, 0xdc, 0x59, 0xc9, 0x44, 0x72, 0xbe, 0xd6, 0xfe, 0x1a, 0x86, 0xb3, 0x0f,
0x3e, 0x7e, 0xff, 0xf1, 0xd9, 0x78, 0x60, 0xef, 0xe6, 0xb6, 0x68, 0xec, 0xbe, 0xcf, 0x1b, 0xe4,
0x43, 0x57, 0x1a, 0xbe, 0x8b, 0x3a, 0xf8, 0x06, 0x41, 0x55, 0x75, 0x01, 0xef, 0xaf, 0x36, 0x03,
0x25, 0xe9, 0x60, 0x9d, 0x81, 0xd9, 0x0f, 0xa5, 0xa6, 0x3d, 0xdb, 0x2e, 0xd3, 0xa4, 0x06, 0xd9,
0x45, 0x9d, 0x93, 0x1b, 0x04, 0x2d, 0xc2, 0xa7, 0xa5, 0x14, 0x27, 0xff, 0xff, 0xd1, 0xdd, 0x8b,
0xc5, 0xb2, 0xbd, 0x40, 0xaf, 0x9e, 0xe9, 0xbc, 0x80, 0x4f, 0x7c, 0x16, 0x38, 0x5c, 0x04, 0x6e,
0x40, 0x99, 0x5c, 0xc5, 0xae, 0xba, 0xf2, 0xa3, 0x30, 0x2e, 0x5e, 0xea, 0x4f, 0x96, 0x91, 0x9f,
0x08, 0x7d, 0x31, 0x36, 0xce, 0x9e, 0x0e, 0x7a, 0x57, 0x55, 0x59, 0xe0, 0xd1, 0xef, 0x00, 0x00,
0x00, 0xff, 0xff, 0x5e, 0x28, 0x7b, 0xe6, 0xb7, 0x06, 0x00, 0x00,
}

View File

@ -215,7 +215,7 @@ func (m *ConverseConfig) GetConverseState() *ConverseState {
// Specifies how to process the `audio_in` data that will be provided in
// subsequent requests. For recommended settings, see the Google Assistant SDK
// [best practices](https://developers.google.com/assistant/best-practices).
// [best practices](https://developers.google.com/assistant/sdk/develop/grpc/best-practices/audio).
type AudioInConfig struct {
// *Required* Encoding of audio data sent in all `audio_in` messages.
Encoding AudioInConfig_Encoding `protobuf:"varint,1,opt,name=encoding,enum=google.assistant.embedded.v1alpha1.AudioInConfig_Encoding" json:"encoding,omitempty"`

View File

@ -1091,76 +1091,78 @@ var _Bigtable_serviceDesc = grpc.ServiceDesc{
func init() { proto.RegisterFile("google/bigtable/v2/bigtable.proto", fileDescriptor0) }
var fileDescriptor0 = []byte{
// 1135 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xac, 0x57, 0x4f, 0x6f, 0x1b, 0x45,
0x14, 0x67, 0xec, 0xda, 0xf1, 0xbe, 0x24, 0x4d, 0x32, 0x84, 0xc6, 0x35, 0x09, 0xb8, 0x4b, 0x0b,
0x8e, 0x4b, 0xd7, 0x55, 0x50, 0x0f, 0x75, 0x95, 0x02, 0x0e, 0x49, 0x83, 0xc0, 0x55, 0x35, 0x96,
0x40, 0x42, 0x48, 0xd6, 0x78, 0x3d, 0x76, 0x96, 0xec, 0xbf, 0xee, 0x8c, 0x63, 0x5c, 0xc4, 0x85,
0x03, 0x1f, 0x00, 0xce, 0x88, 0x13, 0x82, 0x0b, 0x1c, 0xb9, 0x72, 0xe0, 0x23, 0x70, 0xe0, 0x0b,
0xf4, 0x13, 0xf0, 0x09, 0xd0, 0xcc, 0xce, 0xda, 0x4e, 0x62, 0xb7, 0x9b, 0xaa, 0xb7, 0x9d, 0xf7,
0xde, 0xef, 0xcd, 0xef, 0xfd, 0x1d, 0x1b, 0xae, 0xf5, 0x83, 0xa0, 0xef, 0xb2, 0x5a, 0xc7, 0xe9,
0x0b, 0xda, 0x71, 0x59, 0xed, 0x64, 0x67, 0xfc, 0x6d, 0x85, 0x51, 0x20, 0x02, 0x8c, 0x63, 0x13,
0x6b, 0x2c, 0x3e, 0xd9, 0x29, 0x6d, 0x6a, 0x18, 0x0d, 0x9d, 0x1a, 0xf5, 0xfd, 0x40, 0x50, 0xe1,
0x04, 0x3e, 0x8f, 0x11, 0xa5, 0xad, 0x19, 0x4e, 0xbb, 0x54, 0x50, 0xad, 0x7e, 0x43, 0xab, 0xd5,
0xa9, 0x33, 0xe8, 0xd5, 0x86, 0x11, 0x0d, 0x43, 0x16, 0x25, 0xf0, 0x0d, 0xad, 0x8f, 0x42, 0xbb,
0xc6, 0x05, 0x15, 0x03, 0xad, 0x30, 0xff, 0x44, 0xb0, 0x42, 0x18, 0xed, 0x92, 0x60, 0xc8, 0x09,
0x7b, 0x3c, 0x60, 0x5c, 0xe0, 0x2d, 0x00, 0x75, 0x47, 0xdb, 0xa7, 0x1e, 0x2b, 0xa2, 0x32, 0xaa,
0x18, 0xc4, 0x50, 0x92, 0x87, 0xd4, 0x63, 0xd8, 0x82, 0x4b, 0x51, 0x30, 0xe4, 0xc5, 0x4c, 0x19,
0x55, 0x16, 0x77, 0x4a, 0xd6, 0xf9, 0x58, 0x2c, 0x12, 0x0c, 0x5b, 0x4c, 0x10, 0x65, 0x87, 0xef,
0x40, 0xbe, 0xe7, 0xb8, 0x82, 0x45, 0xc5, 0xac, 0x42, 0x6c, 0xcd, 0x41, 0x1c, 0x28, 0x23, 0xa2,
0x8d, 0x25, 0x0b, 0x09, 0x6f, 0xbb, 0x8e, 0xe7, 0x88, 0xe2, 0xa5, 0x32, 0xaa, 0x64, 0x89, 0x21,
0x25, 0x9f, 0x4a, 0x81, 0xf9, 0x5f, 0x16, 0x56, 0x27, 0xc4, 0x79, 0x18, 0xf8, 0x9c, 0xe1, 0x03,
0xc8, 0xdb, 0x47, 0x03, 0xff, 0x98, 0x17, 0x51, 0x39, 0x5b, 0x59, 0xdc, 0xb1, 0x66, 0x5e, 0x75,
0x06, 0x65, 0xed, 0x31, 0xd7, 0xdd, 0x93, 0x30, 0xa2, 0xd1, 0xb8, 0x06, 0xeb, 0x2e, 0xe5, 0xa2,
0xcd, 0x6d, 0xea, 0xfb, 0xac, 0xdb, 0x8e, 0x82, 0x61, 0xfb, 0x98, 0x8d, 0x54, 0xc8, 0x4b, 0x64,
0x4d, 0xea, 0x5a, 0xb1, 0x8a, 0x04, 0xc3, 0x4f, 0xd8, 0xa8, 0xf4, 0x34, 0x03, 0xc6, 0xd8, 0x0d,
0xde, 0x80, 0x85, 0x04, 0x81, 0x14, 0x22, 0x1f, 0x29, 0x33, 0xbc, 0x0b, 0x8b, 0x3d, 0xea, 0x39,
0xee, 0x28, 0x4e, 0x6d, 0x9c, 0xc1, 0xcd, 0x84, 0x64, 0x52, 0x3c, 0xab, 0x25, 0x22, 0xc7, 0xef,
0x7f, 0x46, 0xdd, 0x01, 0x23, 0x10, 0x03, 0x54, 0xe6, 0xef, 0x82, 0xf1, 0x78, 0x40, 0x5d, 0xa7,
0xe7, 0x8c, 0x93, 0xf9, 0xfa, 0x39, 0x70, 0x63, 0x24, 0x18, 0x8f, 0xb1, 0x13, 0x6b, 0xbc, 0x0d,
0xab, 0xc2, 0xf1, 0x18, 0x17, 0xd4, 0x0b, 0xdb, 0x9e, 0x63, 0x47, 0x01, 0xd7, 0x39, 0x5d, 0x19,
0xcb, 0x9b, 0x4a, 0x8c, 0xaf, 0x40, 0xde, 0xa5, 0x1d, 0xe6, 0xf2, 0x62, 0xae, 0x9c, 0xad, 0x18,
0x44, 0x9f, 0xf0, 0x3a, 0xe4, 0x4e, 0xa4, 0xdb, 0x62, 0x5e, 0xc5, 0x14, 0x1f, 0x64, 0x99, 0xd4,
0x47, 0x9b, 0x3b, 0x4f, 0x58, 0x71, 0xa1, 0x8c, 0x2a, 0x39, 0x62, 0x28, 0x49, 0xcb, 0x79, 0x22,
0xd5, 0x46, 0xc4, 0x38, 0x13, 0x32, 0x85, 0xc5, 0x42, 0x19, 0x55, 0x0a, 0x87, 0xaf, 0x90, 0x82,
0x12, 0x91, 0x60, 0x88, 0xdf, 0x04, 0xb0, 0x03, 0xcf, 0x73, 0x62, 0xbd, 0xa1, 0xf5, 0x46, 0x2c,
0x23, 0xc1, 0xb0, 0xb1, 0xa4, 0xba, 0xa0, 0x1d, 0xf7, 0xac, 0x79, 0x07, 0xd6, 0x5b, 0xd4, 0x0b,
0x5d, 0x16, 0xa7, 0x3d, 0x65, 0xc7, 0x9a, 0x2d, 0x78, 0xed, 0x0c, 0x4c, 0xf7, 0xcb, 0xdc, 0x42,
0x5d, 0x83, 0xa5, 0xa0, 0xd7, 0x93, 0xbc, 0x3b, 0x32, 0x9d, 0xaa, 0x52, 0x59, 0xb2, 0x18, 0xcb,
0x54, 0x86, 0xcd, 0xef, 0x11, 0xac, 0x36, 0x07, 0x82, 0x0a, 0xe9, 0x35, 0xe5, 0xe8, 0x4c, 0xdd,
0x97, 0x39, 0x75, 0x5f, 0x1d, 0x0c, 0x6f, 0xa0, 0x27, 0xbe, 0x98, 0x55, 0xbd, 0xbb, 0x39, 0xab,
0x77, 0x9b, 0xda, 0x88, 0x4c, 0xcc, 0xcd, 0x57, 0x61, 0x6d, 0x8a, 0x47, 0x1c, 0x99, 0xf9, 0x2f,
0x9a, 0x92, 0xa6, 0x9d, 0xec, 0x7d, 0x58, 0x60, 0xbe, 0x88, 0x1c, 0x15, 0xb0, 0xe4, 0x70, 0x73,
0x2e, 0x87, 0x69, 0xb7, 0xd6, 0xbe, 0x2f, 0xa2, 0x11, 0x49, 0xb0, 0xa5, 0x2f, 0x21, 0xa7, 0x24,
0xf3, 0xd3, 0x7b, 0x2a, 0xdc, 0xcc, 0xc5, 0xc2, 0xfd, 0x15, 0x01, 0x9e, 0xa6, 0x30, 0x1e, 0xfd,
0x31, 0xf7, 0x78, 0xf6, 0xdf, 0x7d, 0x1e, 0x77, 0x3d, 0xfd, 0x67, 0xc8, 0x7f, 0x9c, 0x90, 0x5f,
0x87, 0x9c, 0xe3, 0x77, 0xd9, 0xd7, 0x8a, 0x7a, 0x96, 0xc4, 0x07, 0x5c, 0x85, 0x7c, 0xdc, 0x8b,
0x7a, 0x78, 0x71, 0x72, 0x4b, 0x14, 0xda, 0x56, 0x4b, 0x69, 0x88, 0xb6, 0x30, 0x7f, 0xcb, 0x40,
0x71, 0xef, 0x88, 0xd9, 0xc7, 0x1f, 0xfa, 0xdd, 0x97, 0xd6, 0x29, 0x87, 0xb0, 0x1a, 0x46, 0xac,
0xeb, 0xd8, 0x54, 0xb0, 0xb6, 0xde, 0xab, 0xf9, 0x34, 0x7b, 0x75, 0x65, 0x0c, 0x8b, 0x05, 0x78,
0x0f, 0x2e, 0x8b, 0x68, 0xc0, 0xda, 0x93, 0x4a, 0x5c, 0x4a, 0x51, 0x89, 0x65, 0x89, 0x49, 0x4e,
0x1c, 0xef, 0xc3, 0x4a, 0x8f, 0xba, 0x7c, 0xda, 0x4b, 0x2e, 0x85, 0x97, 0xcb, 0x0a, 0x34, 0x76,
0x63, 0x1e, 0xc2, 0xd5, 0x19, 0x99, 0xd2, 0xa5, 0xbd, 0x09, 0x6b, 0x93, 0x90, 0x3d, 0x2a, 0xec,
0x23, 0xd6, 0x55, 0x19, 0x2b, 0x90, 0x49, 0x2e, 0x9a, 0xb1, 0xdc, 0xfc, 0x01, 0xc1, 0x55, 0xb9,
0xe1, 0x9b, 0x41, 0xd7, 0xe9, 0x8d, 0x3e, 0x8f, 0x9c, 0x97, 0x92, 0xf5, 0x5d, 0xc8, 0x45, 0x03,
0x97, 0x25, 0xb3, 0xf9, 0xce, 0xbc, 0x77, 0x65, 0xfa, 0xd6, 0x81, 0xcb, 0x48, 0x8c, 0x32, 0x1f,
0x40, 0x69, 0x16, 0x27, 0x1d, 0xdf, 0x36, 0x64, 0xe5, 0xf6, 0x43, 0xaa, 0x8a, 0x1b, 0x73, 0xaa,
0x48, 0xa4, 0xcd, 0xce, 0xef, 0x05, 0x28, 0x34, 0xb4, 0x02, 0xff, 0x84, 0xa0, 0x90, 0x3c, 0x66,
0xf8, 0xad, 0x67, 0x3f, 0x75, 0x2a, 0xfc, 0xd2, 0xf5, 0x34, 0xef, 0xa1, 0xf9, 0xd1, 0x77, 0xff,
0x3c, 0xfd, 0x31, 0x73, 0xdf, 0xbc, 0x2b, 0x7f, 0x64, 0x7c, 0x33, 0xc9, 0xd7, 0x6e, 0x18, 0x05,
0x5f, 0x31, 0x5b, 0xf0, 0x5a, 0xb5, 0xe6, 0xf8, 0x5c, 0x50, 0xdf, 0x66, 0xf2, 0x5b, 0x59, 0xf0,
0x5a, 0xf5, 0xdb, 0x7a, 0xa4, 0x5d, 0xd5, 0x51, 0xf5, 0x36, 0xc2, 0x7f, 0x20, 0x58, 0x3e, 0xb5,
0x77, 0x71, 0x65, 0xd6, 0xfd, 0xb3, 0x36, 0x7a, 0x69, 0x3b, 0x85, 0xa5, 0xa6, 0x7b, 0xa0, 0xe8,
0x7e, 0x80, 0xef, 0x5f, 0x98, 0x2e, 0x9f, 0xf6, 0x77, 0x1b, 0xe1, 0x9f, 0x11, 0x18, 0xe3, 0xf6,
0xc3, 0xd7, 0x9f, 0xb9, 0x40, 0x12, 0xa2, 0x37, 0x9e, 0x63, 0xa5, 0x49, 0xee, 0x2b, 0x92, 0xef,
0x9b, 0xf5, 0x0b, 0x93, 0xf4, 0x12, 0x5f, 0x75, 0x54, 0xc5, 0xbf, 0x20, 0x80, 0xc9, 0x0e, 0xc3,
0x37, 0x52, 0xed, 0xe7, 0xd2, 0xdb, 0xe9, 0x56, 0x61, 0x92, 0x49, 0xf3, 0xde, 0x8b, 0x93, 0xd4,
0xa5, 0xff, 0x0b, 0xc1, 0xda, 0xb9, 0x81, 0xc6, 0x33, 0x57, 0xf2, 0xbc, 0x0d, 0x59, 0xba, 0x95,
0xd2, 0x5a, 0x93, 0x6f, 0x2a, 0xf2, 0x0f, 0xcc, 0xc6, 0x85, 0xc9, 0xdb, 0x67, 0x7d, 0xca, 0x4c,
0xff, 0x8d, 0x00, 0x9f, 0x9f, 0x59, 0x7c, 0x2b, 0xcd, 0xe4, 0x4f, 0x62, 0xb0, 0xd2, 0x9a, 0xeb,
0x20, 0x1e, 0xaa, 0x20, 0x0e, 0xcd, 0xbd, 0x17, 0x1a, 0xbd, 0xd3, 0x4e, 0xeb, 0xa8, 0xda, 0x60,
0x70, 0xc5, 0x0e, 0xbc, 0x19, 0x24, 0x1a, 0xcb, 0xc9, 0x1a, 0x79, 0x24, 0x7f, 0x38, 0x3e, 0x42,
0x5f, 0xd4, 0xb5, 0x51, 0x3f, 0x70, 0xa9, 0xdf, 0xb7, 0x82, 0xa8, 0x5f, 0xeb, 0x33, 0x5f, 0xfd,
0xac, 0xac, 0xc5, 0x2a, 0x1a, 0x3a, 0x7c, 0xfa, 0x0f, 0xc8, 0xbd, 0xe4, 0xbb, 0x93, 0x57, 0x66,
0xef, 0xfd, 0x1f, 0x00, 0x00, 0xff, 0xff, 0x38, 0x8d, 0xf4, 0x91, 0xfb, 0x0c, 0x00, 0x00,
// 1154 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xac, 0x57, 0xdd, 0x6e, 0x1b, 0x45,
0x14, 0x66, 0xec, 0xd8, 0xf1, 0x9e, 0x24, 0x4d, 0x32, 0x84, 0x66, 0x6b, 0x12, 0x70, 0x97, 0x16,
0x1c, 0x97, 0xae, 0x2b, 0xa3, 0x5e, 0xd4, 0x55, 0x0a, 0xd8, 0xe4, 0x07, 0x81, 0xab, 0x6a, 0x2c,
0x15, 0x09, 0x21, 0x59, 0xe3, 0xf5, 0xd8, 0x59, 0xb2, 0x7f, 0xdd, 0x9d, 0x8d, 0x71, 0x11, 0x12,
0xe2, 0x82, 0x07, 0x80, 0x6b, 0xc4, 0x15, 0x02, 0x21, 0xc1, 0x25, 0xb7, 0x5c, 0xf0, 0x08, 0x5c,
0xf0, 0x02, 0x7d, 0x02, 0x9e, 0xa0, 0xda, 0xd9, 0x59, 0xdb, 0x49, 0xec, 0x76, 0x53, 0xf5, 0x6e,
0xe7, 0x9c, 0xf3, 0x9d, 0xf9, 0xce, 0xef, 0xd8, 0x70, 0x75, 0xe0, 0xba, 0x03, 0x8b, 0x55, 0xbb,
0xe6, 0x80, 0xd3, 0xae, 0xc5, 0xaa, 0x27, 0xb5, 0xf1, 0xb7, 0xee, 0xf9, 0x2e, 0x77, 0x31, 0x8e,
0x4d, 0xf4, 0xb1, 0xf8, 0xa4, 0x56, 0xdc, 0x92, 0x30, 0xea, 0x99, 0x55, 0xea, 0x38, 0x2e, 0xa7,
0xdc, 0x74, 0x9d, 0x20, 0x46, 0x14, 0xb7, 0x67, 0x38, 0xed, 0x51, 0x4e, 0xa5, 0xfa, 0x0d, 0xa9,
0x16, 0xa7, 0x6e, 0xd8, 0xaf, 0x0e, 0x7d, 0xea, 0x79, 0xcc, 0x4f, 0xe0, 0x9b, 0x52, 0xef, 0x7b,
0x46, 0x35, 0xe0, 0x94, 0x87, 0x52, 0xa1, 0xfd, 0x85, 0x60, 0x95, 0x30, 0xda, 0x23, 0xee, 0x30,
0x20, 0xec, 0x51, 0xc8, 0x02, 0x8e, 0xb7, 0x01, 0xc4, 0x1d, 0x1d, 0x87, 0xda, 0x4c, 0x45, 0x25,
0x54, 0x56, 0x88, 0x22, 0x24, 0xf7, 0xa9, 0xcd, 0xb0, 0x0e, 0x0b, 0xbe, 0x3b, 0x0c, 0xd4, 0x4c,
0x09, 0x95, 0x97, 0x6a, 0x45, 0xfd, 0x7c, 0x2c, 0x3a, 0x71, 0x87, 0x6d, 0xc6, 0x89, 0xb0, 0xc3,
0xb7, 0x21, 0xdf, 0x37, 0x2d, 0xce, 0x7c, 0x35, 0x2b, 0x10, 0xdb, 0x73, 0x10, 0xfb, 0xc2, 0x88,
0x48, 0xe3, 0x88, 0x45, 0x04, 0xef, 0x58, 0xa6, 0x6d, 0x72, 0x75, 0xa1, 0x84, 0xca, 0x59, 0xa2,
0x44, 0x92, 0x4f, 0x23, 0x81, 0xf6, 0x7f, 0x16, 0xd6, 0x26, 0xc4, 0x03, 0xcf, 0x75, 0x02, 0x86,
0xf7, 0x21, 0x6f, 0x1c, 0x85, 0xce, 0x71, 0xa0, 0xa2, 0x52, 0xb6, 0xbc, 0x54, 0xd3, 0x67, 0x5e,
0x75, 0x06, 0xa5, 0x37, 0x99, 0x65, 0x35, 0x23, 0x18, 0x91, 0x68, 0x5c, 0x85, 0x0d, 0x8b, 0x06,
0xbc, 0x13, 0x18, 0xd4, 0x71, 0x58, 0xaf, 0xe3, 0xbb, 0xc3, 0xce, 0x31, 0x1b, 0x89, 0x90, 0x97,
0xc9, 0x7a, 0xa4, 0x6b, 0xc7, 0x2a, 0xe2, 0x0e, 0x3f, 0x61, 0xa3, 0xe2, 0x93, 0x0c, 0x28, 0x63,
0x37, 0x78, 0x13, 0x16, 0x13, 0x04, 0x12, 0x88, 0xbc, 0x2f, 0xcc, 0xf0, 0x2e, 0x2c, 0xf5, 0xa9,
0x6d, 0x5a, 0xa3, 0x38, 0xb5, 0x71, 0x06, 0xb7, 0x12, 0x92, 0x49, 0xf1, 0xf4, 0x36, 0xf7, 0x4d,
0x67, 0xf0, 0x90, 0x5a, 0x21, 0x23, 0x10, 0x03, 0x44, 0xe6, 0xef, 0x80, 0xf2, 0x28, 0xa4, 0x96,
0xd9, 0x37, 0xc7, 0xc9, 0x7c, 0xfd, 0x1c, 0xb8, 0x31, 0xe2, 0x2c, 0x88, 0xb1, 0x13, 0x6b, 0xbc,
0x03, 0x6b, 0xdc, 0xb4, 0x59, 0xc0, 0xa9, 0xed, 0x75, 0x6c, 0xd3, 0xf0, 0xdd, 0x40, 0xe6, 0x74,
0x75, 0x2c, 0x6f, 0x09, 0x31, 0xbe, 0x0c, 0x79, 0x8b, 0x76, 0x99, 0x15, 0xa8, 0xb9, 0x52, 0xb6,
0xac, 0x10, 0x79, 0xc2, 0x1b, 0x90, 0x3b, 0x89, 0xdc, 0xaa, 0x79, 0x11, 0x53, 0x7c, 0x88, 0xca,
0x24, 0x3e, 0x3a, 0x81, 0xf9, 0x98, 0xa9, 0x8b, 0x25, 0x54, 0xce, 0x11, 0x45, 0x48, 0xda, 0xe6,
0xe3, 0x48, 0xad, 0xf8, 0x2c, 0x60, 0x3c, 0x4a, 0xa1, 0x5a, 0x28, 0xa1, 0x72, 0xe1, 0xf0, 0x15,
0x52, 0x10, 0x22, 0xe2, 0x0e, 0xf1, 0x9b, 0x00, 0x86, 0x6b, 0xdb, 0x66, 0xac, 0x57, 0xa4, 0x5e,
0x89, 0x65, 0xc4, 0x1d, 0x36, 0x96, 0x45, 0x17, 0x74, 0xe2, 0x9e, 0xd5, 0x6e, 0xc3, 0x46, 0x9b,
0xda, 0x9e, 0xc5, 0xe2, 0xb4, 0xa7, 0xec, 0x58, 0xad, 0x0d, 0xaf, 0x9d, 0x81, 0xc9, 0x7e, 0x99,
0x5b, 0xa8, 0xab, 0xb0, 0xec, 0xf6, 0xfb, 0x11, 0xef, 0x6e, 0x94, 0x4e, 0x51, 0xa9, 0x2c, 0x59,
0x8a, 0x65, 0x22, 0xc3, 0xda, 0xf7, 0x08, 0xd6, 0x5a, 0x21, 0xa7, 0x3c, 0xf2, 0x9a, 0x72, 0x74,
0xa6, 0xee, 0xcb, 0x9c, 0xba, 0xaf, 0x0e, 0x8a, 0x1d, 0xca, 0x89, 0x57, 0xb3, 0xa2, 0x77, 0xb7,
0x66, 0xf5, 0x6e, 0x4b, 0x1a, 0x91, 0x89, 0xb9, 0xf6, 0x2a, 0xac, 0x4f, 0xf1, 0x88, 0x23, 0xd3,
0xfe, 0x43, 0x53, 0xd2, 0xb4, 0x93, 0xbd, 0x07, 0x8b, 0xcc, 0xe1, 0xbe, 0x29, 0x02, 0x8e, 0x38,
0xdc, 0x98, 0xcb, 0x61, 0xda, 0xad, 0xbe, 0xe7, 0x70, 0x7f, 0x44, 0x12, 0x6c, 0xf1, 0x0b, 0xc8,
0x09, 0xc9, 0xfc, 0xf4, 0x9e, 0x0a, 0x37, 0x73, 0xb1, 0x70, 0x7f, 0x45, 0x80, 0xa7, 0x29, 0x8c,
0x47, 0x7f, 0xcc, 0x3d, 0x9e, 0xfd, 0x77, 0x9f, 0xc7, 0x5d, 0x4e, 0xff, 0x19, 0xf2, 0x1f, 0x27,
0xe4, 0x37, 0x20, 0x67, 0x3a, 0x3d, 0xf6, 0x95, 0xa0, 0x9e, 0x25, 0xf1, 0x01, 0x57, 0x20, 0x1f,
0xf7, 0xa2, 0x1c, 0x5e, 0x9c, 0xdc, 0xe2, 0x7b, 0x86, 0xde, 0x16, 0x1a, 0x22, 0x2d, 0xb4, 0xdf,
0x32, 0xa0, 0x36, 0x8f, 0x98, 0x71, 0xfc, 0xa1, 0xd3, 0x7b, 0x69, 0x9d, 0x72, 0x08, 0x6b, 0x9e,
0xcf, 0x7a, 0xa6, 0x41, 0x39, 0xeb, 0xc8, 0xbd, 0x9a, 0x4f, 0xb3, 0x57, 0x57, 0xc7, 0xb0, 0x58,
0x80, 0x9b, 0x70, 0x89, 0xfb, 0x21, 0xeb, 0x4c, 0x2a, 0xb1, 0x90, 0xa2, 0x12, 0x2b, 0x11, 0x26,
0x39, 0x05, 0x78, 0x0f, 0x56, 0xfb, 0xd4, 0x0a, 0xa6, 0xbd, 0xe4, 0x52, 0x78, 0xb9, 0x24, 0x40,
0x63, 0x37, 0xda, 0x21, 0x5c, 0x99, 0x91, 0x29, 0x59, 0xda, 0x1b, 0xb0, 0x3e, 0x09, 0xd9, 0xa6,
0xdc, 0x38, 0x62, 0x3d, 0x91, 0xb1, 0x02, 0x99, 0xe4, 0xa2, 0x15, 0xcb, 0xb5, 0x1f, 0x10, 0x5c,
0x89, 0x36, 0x7c, 0xcb, 0xed, 0x99, 0xfd, 0xd1, 0x67, 0xbe, 0xf9, 0x52, 0xb2, 0xbe, 0x0b, 0x39,
0x3f, 0xb4, 0x58, 0x32, 0x9b, 0xef, 0xcc, 0x7b, 0x57, 0xa6, 0x6f, 0x0d, 0x2d, 0x46, 0x62, 0x94,
0x76, 0x00, 0xc5, 0x59, 0x9c, 0x64, 0x7c, 0x3b, 0x90, 0x8d, 0xb6, 0x1f, 0x12, 0x55, 0xdc, 0x9c,
0x53, 0x45, 0x12, 0xd9, 0xd4, 0xfe, 0x28, 0x40, 0xa1, 0x21, 0x15, 0xf8, 0x27, 0x04, 0x85, 0xe4,
0x31, 0xc3, 0x6f, 0x3d, 0xfb, 0xa9, 0x13, 0xe1, 0x17, 0xaf, 0xa5, 0x79, 0x0f, 0xb5, 0x8f, 0xbe,
0xfb, 0xf7, 0xc9, 0x8f, 0x99, 0x7b, 0xda, 0x9d, 0xe8, 0x47, 0xc6, 0xd7, 0x93, 0x7c, 0xed, 0x7a,
0xbe, 0xfb, 0x25, 0x33, 0x78, 0x50, 0xad, 0x54, 0x4d, 0x27, 0xe0, 0xd4, 0x31, 0x58, 0xf4, 0x2d,
0x2c, 0x82, 0x6a, 0xe5, 0x9b, 0xba, 0x2f, 0x5d, 0xd5, 0x51, 0xe5, 0x16, 0xc2, 0x7f, 0x22, 0x58,
0x39, 0xb5, 0x77, 0x71, 0x79, 0xd6, 0xfd, 0xb3, 0x36, 0x7a, 0x71, 0x27, 0x85, 0xa5, 0xa4, 0xbb,
0x2f, 0xe8, 0x7e, 0x80, 0xef, 0x5d, 0x98, 0x6e, 0x30, 0xed, 0xef, 0x16, 0xc2, 0x3f, 0x23, 0x50,
0xc6, 0xed, 0x87, 0xaf, 0x3d, 0x73, 0x81, 0x24, 0x44, 0xaf, 0x3f, 0xc7, 0x4a, 0x92, 0xdc, 0x13,
0x24, 0xdf, 0xd7, 0xea, 0x17, 0x26, 0x69, 0x27, 0xbe, 0xea, 0xa8, 0x82, 0x7f, 0x41, 0x00, 0x93,
0x1d, 0x86, 0xaf, 0xa7, 0xda, 0xcf, 0xc5, 0xb7, 0xd3, 0xad, 0xc2, 0x24, 0x93, 0xda, 0xdd, 0x17,
0x27, 0x29, 0x4b, 0xff, 0x37, 0x82, 0xf5, 0x73, 0x03, 0x8d, 0x67, 0xae, 0xe4, 0x79, 0x1b, 0xb2,
0x78, 0x33, 0xa5, 0xb5, 0x24, 0xdf, 0x12, 0xe4, 0x0f, 0xb4, 0xc6, 0x85, 0xc9, 0x1b, 0x67, 0x7d,
0x46, 0x99, 0xfe, 0x07, 0x01, 0x3e, 0x3f, 0xb3, 0xf8, 0x66, 0x9a, 0xc9, 0x9f, 0xc4, 0xa0, 0xa7,
0x35, 0x97, 0x41, 0xdc, 0x17, 0x41, 0x1c, 0x6a, 0xcd, 0x17, 0x1a, 0xbd, 0xd3, 0x4e, 0xeb, 0xa8,
0xd2, 0xf8, 0x16, 0xc1, 0x65, 0xc3, 0xb5, 0x67, 0xb0, 0x68, 0xac, 0x24, 0x7b, 0xe4, 0x41, 0xf4,
0xcb, 0xf1, 0x01, 0xfa, 0xbc, 0x2e, 0x8d, 0x06, 0xae, 0x45, 0x9d, 0x81, 0xee, 0xfa, 0x83, 0xea,
0x80, 0x39, 0xe2, 0x77, 0x65, 0x35, 0x56, 0x51, 0xcf, 0x0c, 0xa6, 0xff, 0x81, 0xdc, 0x4d, 0xbe,
0x7f, 0xcf, 0xa8, 0x07, 0x31, 0xb8, 0x69, 0xb9, 0x61, 0x4f, 0x4f, 0x5c, 0xeb, 0x0f, 0x6b, 0xdd,
0xbc, 0xf0, 0xf0, 0xde, 0xd3, 0x00, 0x00, 0x00, 0xff, 0xff, 0x74, 0x6b, 0x7a, 0xa3, 0x17, 0x0d,
0x00, 0x00,
}

View File

@ -2024,94 +2024,95 @@ func init() {
func init() { proto.RegisterFile("google/bigtable/v2/data.proto", fileDescriptor1) }
var fileDescriptor1 = []byte{
// 1412 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xac, 0x57, 0xdb, 0x6e, 0xdb, 0x46,
0x13, 0x16, 0x2d, 0xeb, 0x34, 0x94, 0x25, 0x65, 0xe3, 0x38, 0x8a, 0xfe, 0xf8, 0x8f, 0xc1, 0x14,
0xa9, 0xe2, 0xb6, 0x72, 0xab, 0x04, 0xe9, 0x21, 0x45, 0x11, 0xcb, 0x69, 0xaa, 0x36, 0xe7, 0x8d,
0x91, 0x02, 0x01, 0x0a, 0x76, 0x2d, 0xad, 0x54, 0xc2, 0x4b, 0x2e, 0x4b, 0x52, 0x56, 0xf4, 0x22,
0xbd, 0x6f, 0x5f, 0xa3, 0x77, 0x7d, 0x89, 0xf6, 0x31, 0xfa, 0x00, 0xbd, 0x28, 0xf6, 0xc0, 0x93,
0xa2, 0xd8, 0x46, 0x91, 0x3b, 0x72, 0xe6, 0xfb, 0xbe, 0x99, 0x9d, 0x9d, 0x1d, 0x2e, 0x61, 0x7b,
0xca, 0xf9, 0x94, 0xd1, 0xbd, 0x23, 0x67, 0x1a, 0x91, 0x23, 0x46, 0xf7, 0x4e, 0xfa, 0x7b, 0x63,
0x12, 0x91, 0x9e, 0x1f, 0xf0, 0x88, 0x23, 0xa4, 0xdc, 0xbd, 0xd8, 0xdd, 0x3b, 0xe9, 0x5b, 0x4f,
0xa1, 0x88, 0xf9, 0x1c, 0xb5, 0xa0, 0x78, 0x4c, 0x17, 0x6d, 0x63, 0xc7, 0xe8, 0xd6, 0xb1, 0x78,
0x44, 0x77, 0xa0, 0x3a, 0x21, 0xae, 0xc3, 0x1c, 0x1a, 0xb6, 0xd7, 0x76, 0x8a, 0x5d, 0xb3, 0xdf,
0xe9, 0xbd, 0xc9, 0xef, 0x3d, 0x10, 0x98, 0x05, 0x4e, 0xb0, 0x16, 0x86, 0xb2, 0xb2, 0x21, 0x04,
0xeb, 0x1e, 0x71, 0xa9, 0x14, 0xad, 0x61, 0xf9, 0x8c, 0x6e, 0x43, 0x65, 0xc4, 0xd9, 0xcc, 0xf5,
0x4e, 0x15, 0x3d, 0x90, 0x10, 0x1c, 0x43, 0xad, 0x97, 0x50, 0x56, 0x26, 0x74, 0x15, 0x6a, 0x3f,
0xcf, 0x08, 0x73, 0x26, 0x0e, 0x0d, 0x74, 0xb6, 0xa9, 0x01, 0xf5, 0xa0, 0x34, 0xa2, 0x8c, 0xc5,
0xda, 0xed, 0x95, 0xda, 0x94, 0x31, 0xac, 0x60, 0x96, 0x0d, 0xeb, 0xe2, 0x15, 0xdd, 0x84, 0x56,
0xe4, 0xb8, 0x34, 0x8c, 0x88, 0xeb, 0xdb, 0xae, 0x33, 0x0a, 0x78, 0x28, 0xc5, 0x8b, 0xb8, 0x99,
0xd8, 0x1f, 0x4b, 0x33, 0xda, 0x84, 0xd2, 0x09, 0x61, 0x33, 0xda, 0x5e, 0x93, 0xc1, 0xd5, 0x0b,
0xda, 0x82, 0x32, 0x23, 0x47, 0x94, 0x85, 0xed, 0xe2, 0x4e, 0xb1, 0x5b, 0xc3, 0xfa, 0xcd, 0xfa,
0xc3, 0x80, 0x2a, 0xe6, 0x73, 0x4c, 0xbc, 0x29, 0x45, 0xbb, 0xd0, 0x0a, 0x23, 0x12, 0x44, 0xf6,
0x31, 0x5d, 0xd8, 0x23, 0xc6, 0x43, 0x3a, 0x56, 0x4b, 0x18, 0x16, 0x70, 0x43, 0x7a, 0x1e, 0xd2,
0xc5, 0x81, 0xb4, 0xa3, 0x1b, 0xd0, 0x48, 0xb1, 0xdc, 0xa7, 0x9e, 0x8a, 0x37, 0x2c, 0xe0, 0x7a,
0x8c, 0x7c, 0xea, 0x53, 0x0f, 0x59, 0x50, 0xa7, 0xde, 0x38, 0x45, 0x15, 0x25, 0xca, 0xc0, 0x40,
0xbd, 0x71, 0x8c, 0xb9, 0x01, 0x8d, 0x18, 0xa3, 0xa3, 0xae, 0x6b, 0x54, 0x5d, 0xa1, 0x54, 0xcc,
0x81, 0x09, 0xb5, 0x24, 0xe6, 0xa0, 0x06, 0x15, 0x4d, 0xb2, 0x7e, 0x84, 0x32, 0xe6, 0xf3, 0x17,
0x34, 0x42, 0x57, 0xa0, 0x1a, 0xf0, 0xb9, 0x30, 0x8a, 0xfa, 0x14, 0xbb, 0x75, 0x5c, 0x09, 0xf8,
0xfc, 0x21, 0x5d, 0x84, 0xe8, 0x2e, 0x80, 0x70, 0x05, 0x62, 0xa5, 0x71, 0xfd, 0xaf, 0xae, 0xaa,
0x7f, 0x5c, 0x0e, 0x5c, 0x0b, 0xf4, 0x53, 0x68, 0xfd, 0xb6, 0x06, 0xa6, 0xde, 0x73, 0x59, 0xa9,
0x6b, 0x60, 0xca, 0x7e, 0x5a, 0xd8, 0x99, 0x06, 0x02, 0x65, 0x7a, 0x22, 0xda, 0xe8, 0x0e, 0x6c,
0xa9, 0x54, 0x93, 0xbd, 0x8f, 0x97, 0x16, 0x97, 0x69, 0x53, 0xfa, 0x9f, 0xc7, 0x6e, 0x5d, 0xd6,
0x3e, 0x6c, 0x2e, 0xf3, 0x32, 0x65, 0x2b, 0x60, 0x94, 0x67, 0xc9, 0xf2, 0xf5, 0x61, 0x53, 0x54,
0xe2, 0x8d, 0x48, 0x71, 0x11, 0x11, 0xf5, 0xc6, 0xcb, 0x71, 0x7a, 0x80, 0xf2, 0x1c, 0x19, 0xa5,
0xa4, 0x19, 0xad, 0x2c, 0x43, 0xc4, 0x18, 0x5c, 0x80, 0xe6, 0x52, 0x5e, 0x83, 0x26, 0x6c, 0xe4,
0x24, 0xac, 0xd7, 0xd0, 0x38, 0x8c, 0x9b, 0x51, 0x95, 0xe9, 0x76, 0x5c, 0x85, 0xb7, 0x34, 0xaf,
0x5a, 0xeb, 0xe1, 0x52, 0x07, 0x7f, 0xac, 0xd6, 0xf3, 0x06, 0x67, 0x4d, 0x72, 0x44, 0xde, 0x4b,
0x0c, 0xeb, 0x2f, 0x03, 0xe0, 0xa5, 0xe8, 0x73, 0x15, 0xb6, 0x07, 0xaa, 0x4c, 0xb6, 0xec, 0xfd,
0xe5, 0x4e, 0x56, 0x3d, 0x2e, 0xe1, 0xba, 0x18, 0x49, 0xdf, 0x2b, 0x7c, 0xae, 0x9b, 0x1b, 0x29,
0x5a, 0x16, 0x7b, 0x17, 0x44, 0x71, 0xf2, 0xca, 0x71, 0x4f, 0x8b, 0x2e, 0xce, 0xea, 0xea, 0xbe,
0xce, 0xa8, 0x66, 0xfb, 0x3a, 0xd1, 0x1c, 0x6c, 0x80, 0x99, 0x89, 0x2f, 0xda, 0x3c, 0xa1, 0x59,
0xff, 0x98, 0x50, 0xc3, 0x7c, 0xfe, 0xc0, 0x61, 0x11, 0x0d, 0xd0, 0x5d, 0x28, 0x8d, 0x7e, 0x22,
0x8e, 0x27, 0x17, 0x63, 0xf6, 0xaf, 0xbf, 0xa5, 0x7f, 0x15, 0xba, 0x77, 0x20, 0xa0, 0xc3, 0x02,
0x56, 0x1c, 0xf4, 0x1d, 0x80, 0xe3, 0x45, 0x34, 0x60, 0x94, 0x9c, 0xa8, 0xf1, 0x60, 0xf6, 0xbb,
0xa7, 0x2b, 0x7c, 0x9b, 0xe0, 0x87, 0x05, 0x9c, 0x61, 0xa3, 0x6f, 0xa0, 0x36, 0xe2, 0xde, 0xd8,
0x89, 0x1c, 0xae, 0x9a, 0xd3, 0xec, 0xbf, 0x7f, 0x46, 0x32, 0x31, 0x7c, 0x58, 0xc0, 0x29, 0x17,
0x6d, 0xc2, 0x7a, 0xe8, 0x78, 0xc7, 0xed, 0xd6, 0x8e, 0xd1, 0xad, 0x0e, 0x0b, 0x58, 0xbe, 0xa1,
0x2e, 0x34, 0x7d, 0x12, 0x86, 0x36, 0x61, 0xcc, 0x9e, 0x48, 0x7e, 0xfb, 0x82, 0x06, 0x6c, 0x08,
0xc7, 0x3e, 0x63, 0xba, 0x22, 0xbb, 0xd0, 0x3a, 0x62, 0x7c, 0x74, 0x9c, 0x85, 0x22, 0x0d, 0x6d,
0x48, 0x4f, 0x8a, 0xfd, 0x04, 0x36, 0xf5, 0x74, 0xb0, 0x03, 0x3a, 0xa5, 0xaf, 0x63, 0xfc, 0xba,
0xde, 0xeb, 0x0b, 0x6a, 0x56, 0x60, 0xe1, 0xd3, 0x94, 0x0f, 0x41, 0x18, 0xed, 0x90, 0xb8, 0x3e,
0xa3, 0x31, 0xbe, 0xb1, 0x63, 0x74, 0x8d, 0x61, 0x01, 0x37, 0x03, 0x3e, 0x7f, 0x21, 0x3d, 0x1a,
0xfd, 0x39, 0xb4, 0x33, 0x63, 0x21, 0x1f, 0x44, 0x9c, 0xad, 0xda, 0xb0, 0x80, 0x2f, 0xa5, 0x53,
0x22, 0x1b, 0xe8, 0x00, 0xb6, 0xd5, 0xc7, 0x24, 0x73, 0x26, 0x73, 0xfc, 0xb2, 0x4e, 0xb2, 0xa3,
0x60, 0xc9, 0xf1, 0xcc, 0x8a, 0x3c, 0x87, 0x8b, 0x5a, 0x44, 0x8e, 0xb9, 0x98, 0x5a, 0x91, 0xfb,
0x73, 0xed, 0x94, 0x0f, 0x99, 0x40, 0x8b, 0x02, 0x8c, 0xd2, 0x57, 0x2d, 0xf9, 0x0a, 0xb6, 0xd2,
0x83, 0x98, 0x53, 0xad, 0x4a, 0x55, 0x6b, 0x95, 0x6a, 0x7e, 0x0c, 0x88, 0x61, 0x17, 0xe5, 0x2c,
0x5a, 0xbb, 0x07, 0x48, 0x9d, 0x8d, 0xdc, 0x42, 0x6b, 0xf1, 0x39, 0x95, 0xbe, 0xec, 0xf2, 0x9e,
0x24, 0xf8, 0x6c, 0x1e, 0x4d, 0x99, 0xc7, 0xff, 0x57, 0xe5, 0x91, 0xce, 0x84, 0x54, 0x2f, 0x13,
0xff, 0x2b, 0xf8, 0x9f, 0xfc, 0xcc, 0xda, 0xbe, 0x28, 0x36, 0x9f, 0xdb, 0x7c, 0x32, 0x09, 0x69,
0x14, 0x0b, 0xc3, 0x8e, 0xd1, 0x2d, 0x0d, 0x0b, 0xf8, 0xb2, 0x04, 0x3d, 0xa3, 0x01, 0xe6, 0xf3,
0xa7, 0x12, 0xa1, 0xf9, 0x5f, 0x42, 0x27, 0xcf, 0x67, 0x8e, 0xeb, 0x24, 0x74, 0x53, 0xd3, 0xb7,
0x32, 0xf4, 0x47, 0x02, 0xa0, 0xd9, 0x03, 0xd8, 0x4e, 0xd9, 0x7a, 0xdb, 0x72, 0x02, 0x75, 0x2d,
0x70, 0x25, 0x16, 0x50, 0x9b, 0x95, 0xd5, 0xf8, 0x0c, 0x2e, 0x87, 0x51, 0xe0, 0xf8, 0x7a, 0xc6,
0x44, 0x01, 0xf1, 0xc2, 0x09, 0x0f, 0x5c, 0x1a, 0xb4, 0x37, 0xf4, 0x21, 0xb8, 0x24, 0x01, 0xb2,
0x12, 0x87, 0xa9, 0x5b, 0x30, 0x89, 0xef, 0xb3, 0x85, 0x2d, 0x2f, 0x02, 0x39, 0xe6, 0xc5, 0xb8,
0x53, 0x25, 0xe0, 0x91, 0xf0, 0x67, 0x98, 0x9d, 0x7b, 0x50, 0x92, 0x83, 0x05, 0x7d, 0x0a, 0x15,
0x95, 0xa9, 0xfa, 0xd6, 0x9a, 0xfd, 0xed, 0x53, 0x27, 0x00, 0x8e, 0xd1, 0x9d, 0xaf, 0x01, 0xd2,
0xc1, 0xf2, 0xdf, 0x65, 0xfe, 0x34, 0xa0, 0x96, 0x4c, 0x15, 0x34, 0x84, 0x96, 0x1f, 0xd0, 0xb1,
0x33, 0x22, 0x51, 0xd2, 0x1a, 0x6a, 0x4a, 0x9e, 0xa1, 0xd7, 0x4c, 0x68, 0x49, 0x5b, 0x98, 0x51,
0x30, 0x4b, 0x44, 0xd6, 0xce, 0x23, 0x02, 0x82, 0xa1, 0xf9, 0xf7, 0xa0, 0x3e, 0x21, 0x2c, 0x4c,
0x04, 0x8a, 0xe7, 0x11, 0x30, 0x25, 0x45, 0xbd, 0x0c, 0xaa, 0x50, 0x56, 0x5c, 0xeb, 0xef, 0x12,
0x54, 0x1f, 0xcf, 0x22, 0x22, 0x97, 0xb8, 0x0f, 0x55, 0xd1, 0x9e, 0xa2, 0x1d, 0xf4, 0xd2, 0xde,
0x5b, 0x25, 0x1a, 0xe3, 0x7b, 0x2f, 0x68, 0x24, 0x6e, 0x8f, 0xc3, 0x02, 0xae, 0x84, 0xea, 0x11,
0xfd, 0x00, 0x68, 0x4c, 0x19, 0x15, 0x25, 0x0a, 0xb8, 0xab, 0xdb, 0x4e, 0x2f, 0xf1, 0xa3, 0x53,
0xc5, 0xee, 0x4b, 0xda, 0x83, 0x80, 0xbb, 0xaa, 0x0d, 0xc5, 0x89, 0x1a, 0x2f, 0xd9, 0x96, 0xe5,
0xd5, 0xa8, 0xd3, 0x05, 0x38, 0xaf, 0xbc, 0xba, 0x9c, 0xe7, 0xe5, 0xf5, 0x85, 0xfd, 0x10, 0x9a,
0x59, 0xf9, 0x80, 0xcf, 0xe5, 0xec, 0x36, 0xfb, 0xbb, 0xe7, 0xd4, 0xc6, 0x7c, 0x2e, 0x3e, 0x21,
0xe3, 0xac, 0xa1, 0xf3, 0x8b, 0x01, 0x15, 0x5d, 0xaa, 0xb3, 0x2f, 0x76, 0x37, 0xa1, 0xb5, 0x3c,
0xa7, 0xf5, 0x4d, 0xbb, 0xb9, 0x34, 0x98, 0x57, 0x5e, 0xda, 0x8b, 0x67, 0x5c, 0xda, 0xd7, 0x33,
0x97, 0xf6, 0xce, 0xaf, 0x06, 0xb4, 0x96, 0xcb, 0xfe, 0x4e, 0x33, 0xdc, 0x07, 0x10, 0x99, 0xa8,
0x79, 0xaa, 0xb7, 0xe9, 0x1c, 0x03, 0x1d, 0xd7, 0x04, 0x4b, 0x3e, 0x76, 0x6e, 0x65, 0x53, 0xd4,
0xdb, 0x74, 0x56, 0x8a, 0x9d, 0x26, 0x6c, 0xe4, 0xf6, 0x64, 0x00, 0x50, 0x75, 0xf5, 0x6e, 0x59,
0xbf, 0x1b, 0x70, 0x11, 0x53, 0x32, 0x7e, 0xcc, 0xc7, 0xce, 0x64, 0xf1, 0x7d, 0xe0, 0x44, 0x14,
0xcf, 0x18, 0x7d, 0xa7, 0x0b, 0xbf, 0x0e, 0x75, 0xe2, 0xfb, 0xc9, 0x2d, 0x2b, 0xb9, 0x5e, 0x9b,
0xca, 0x2a, 0xa7, 0x25, 0xfa, 0x00, 0x5a, 0x8e, 0x37, 0x0a, 0xa8, 0x4b, 0xbd, 0xc8, 0x26, 0x2e,
0x9f, 0x79, 0x91, 0xdc, 0x9f, 0xa2, 0xf8, 0xf4, 0x27, 0x9e, 0x7d, 0xe9, 0x18, 0x94, 0x61, 0x3d,
0x98, 0x31, 0x3a, 0x20, 0xb0, 0x35, 0xe2, 0xee, 0x8a, 0x1a, 0x0e, 0x6a, 0xf7, 0x49, 0x44, 0x9e,
0x89, 0xff, 0xdc, 0x67, 0xc6, 0xab, 0x2f, 0x34, 0x60, 0xca, 0x19, 0xf1, 0xa6, 0x3d, 0x1e, 0x4c,
0xf7, 0xa6, 0xd4, 0x93, 0x7f, 0xc1, 0x7b, 0xca, 0x45, 0x7c, 0x27, 0xcc, 0xfe, 0x27, 0xdf, 0x8d,
0x9f, 0x8f, 0xca, 0x12, 0x76, 0xeb, 0xdf, 0x00, 0x00, 0x00, 0xff, 0xff, 0xd8, 0xbb, 0x74, 0x4d,
0x4d, 0x0f, 0x00, 0x00,
// 1430 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xac, 0x57, 0xdd, 0x6e, 0x1b, 0x45,
0x14, 0xf6, 0xc6, 0x89, 0x7f, 0xce, 0x3a, 0xb6, 0x3b, 0x4d, 0x53, 0xd7, 0x34, 0x34, 0xda, 0xa2,
0xe2, 0x06, 0x70, 0xc0, 0xad, 0xca, 0x4f, 0x11, 0x6a, 0x9c, 0xd2, 0x1a, 0xfa, 0x3f, 0x8d, 0x8a,
0x54, 0x09, 0x2d, 0x13, 0x7b, 0x6c, 0x56, 0x99, 0xdd, 0x59, 0x76, 0xd7, 0x71, 0x2d, 0xf1, 0x1c,
0xdc, 0xc3, 0x25, 0xaf, 0xc0, 0x1d, 0x2f, 0x01, 0x8f, 0xc1, 0x03, 0x70, 0x81, 0xe6, 0x67, 0xff,
0x5c, 0x37, 0x89, 0x50, 0xef, 0x76, 0xcf, 0xf9, 0xbe, 0xef, 0x9c, 0x39, 0x73, 0xe6, 0xec, 0x2c,
0x6c, 0x4d, 0x38, 0x9f, 0x30, 0xba, 0x7b, 0xe8, 0x4c, 0x22, 0x72, 0xc8, 0xe8, 0xee, 0x71, 0x6f,
0x77, 0x44, 0x22, 0xd2, 0xf5, 0x03, 0x1e, 0x71, 0x84, 0x94, 0xbb, 0x1b, 0xbb, 0xbb, 0xc7, 0x3d,
0xeb, 0x09, 0x14, 0x31, 0x9f, 0xa1, 0x26, 0x14, 0x8f, 0xe8, 0xbc, 0x65, 0x6c, 0x1b, 0x9d, 0x1a,
0x16, 0x8f, 0xe8, 0x16, 0x54, 0xc6, 0xc4, 0x75, 0x98, 0x43, 0xc3, 0xd6, 0xca, 0x76, 0xb1, 0x63,
0xf6, 0xda, 0xdd, 0xd7, 0xf9, 0xdd, 0x7b, 0x02, 0x33, 0xc7, 0x09, 0xd6, 0xc2, 0x50, 0x52, 0x36,
0x84, 0x60, 0xd5, 0x23, 0x2e, 0x95, 0xa2, 0x55, 0x2c, 0x9f, 0xd1, 0x4d, 0x28, 0x0f, 0x39, 0x9b,
0xba, 0xde, 0x89, 0xa2, 0xfb, 0x12, 0x82, 0x63, 0xa8, 0xf5, 0x02, 0x4a, 0xca, 0x84, 0x2e, 0x43,
0xf5, 0xa7, 0x29, 0x61, 0xce, 0xd8, 0xa1, 0x81, 0xce, 0x36, 0x35, 0xa0, 0x2e, 0xac, 0x0d, 0x29,
0x63, 0xb1, 0x76, 0x6b, 0xa9, 0x36, 0x65, 0x0c, 0x2b, 0x98, 0x65, 0xc3, 0xaa, 0x78, 0x45, 0xd7,
0xa1, 0x19, 0x39, 0x2e, 0x0d, 0x23, 0xe2, 0xfa, 0xb6, 0xeb, 0x0c, 0x03, 0x1e, 0x4a, 0xf1, 0x22,
0x6e, 0x24, 0xf6, 0x47, 0xd2, 0x8c, 0x36, 0x60, 0xed, 0x98, 0xb0, 0x29, 0x6d, 0xad, 0xc8, 0xe0,
0xea, 0x05, 0x6d, 0x42, 0x89, 0x91, 0x43, 0xca, 0xc2, 0x56, 0x71, 0xbb, 0xd8, 0xa9, 0x62, 0xfd,
0x66, 0xfd, 0x69, 0x40, 0x05, 0xf3, 0x19, 0x26, 0xde, 0x84, 0xa2, 0x1d, 0x68, 0x86, 0x11, 0x09,
0x22, 0xfb, 0x88, 0xce, 0xed, 0x21, 0xe3, 0x21, 0x1d, 0xa9, 0x25, 0x0c, 0x0a, 0xb8, 0x2e, 0x3d,
0x0f, 0xe8, 0x7c, 0x5f, 0xda, 0xd1, 0x35, 0xa8, 0xa7, 0x58, 0xee, 0x53, 0x4f, 0xc5, 0x1b, 0x14,
0x70, 0x2d, 0x46, 0x3e, 0xf1, 0xa9, 0x87, 0x2c, 0xa8, 0x51, 0x6f, 0x94, 0xa2, 0x8a, 0x12, 0x65,
0x60, 0xa0, 0xde, 0x28, 0xc6, 0x5c, 0x83, 0x7a, 0x8c, 0xd1, 0x51, 0x57, 0x35, 0xaa, 0xa6, 0x50,
0x2a, 0x66, 0xdf, 0x84, 0x6a, 0x12, 0xb3, 0x5f, 0x85, 0xb2, 0x26, 0x59, 0x3f, 0x40, 0x09, 0xf3,
0xd9, 0x73, 0x1a, 0xa1, 0x4b, 0x50, 0x09, 0xf8, 0x4c, 0x18, 0x45, 0x7d, 0x8a, 0x9d, 0x1a, 0x2e,
0x07, 0x7c, 0xf6, 0x80, 0xce, 0x43, 0x74, 0x1b, 0x40, 0xb8, 0x02, 0xb1, 0xd2, 0xb8, 0xfe, 0x97,
0x97, 0xd5, 0x3f, 0x2e, 0x07, 0xae, 0x06, 0xfa, 0x29, 0xb4, 0x7e, 0x5b, 0x01, 0x53, 0xef, 0xb9,
0xac, 0xd4, 0x15, 0x30, 0x65, 0x3f, 0xcd, 0xed, 0x4c, 0x03, 0x81, 0x32, 0x3d, 0x16, 0x6d, 0x74,
0x0b, 0x36, 0x55, 0xaa, 0xc9, 0xde, 0xc7, 0x4b, 0x8b, 0xcb, 0xb4, 0x21, 0xfd, 0xcf, 0x62, 0xb7,
0x2e, 0x6b, 0x0f, 0x36, 0x16, 0x79, 0x99, 0xb2, 0x15, 0x30, 0xca, 0xb3, 0x64, 0xf9, 0x7a, 0xb0,
0x21, 0x2a, 0xf1, 0x5a, 0xa4, 0xb8, 0x88, 0x88, 0x7a, 0xa3, 0xc5, 0x38, 0x5d, 0x40, 0x79, 0x8e,
0x8c, 0xb2, 0xa6, 0x19, 0xcd, 0x2c, 0x43, 0xc4, 0xe8, 0x9f, 0x83, 0xc6, 0x42, 0x5e, 0xfd, 0x06,
0xac, 0xe7, 0x24, 0xac, 0x57, 0x50, 0x3f, 0x88, 0x9b, 0x51, 0x95, 0xe9, 0x66, 0x5c, 0x85, 0x37,
0x34, 0xaf, 0x5a, 0xeb, 0xc1, 0x42, 0x07, 0x7f, 0xac, 0xd6, 0xf3, 0x1a, 0x67, 0x45, 0x72, 0x44,
0xde, 0x0b, 0x0c, 0xeb, 0x6f, 0x03, 0xe0, 0x85, 0xe8, 0x73, 0x15, 0xb6, 0x0b, 0xaa, 0x4c, 0xb6,
0xec, 0xfd, 0xc5, 0x4e, 0x56, 0x3d, 0x2e, 0xe1, 0xba, 0x18, 0x49, 0xdf, 0x2b, 0x7c, 0xae, 0x9b,
0xeb, 0x29, 0x5a, 0x16, 0x7b, 0x07, 0x44, 0x71, 0xf2, 0xca, 0x71, 0x4f, 0x8b, 0x2e, 0xce, 0xea,
0xea, 0xbe, 0xce, 0xa8, 0x66, 0xfb, 0x3a, 0xd1, 0xec, 0xaf, 0x83, 0x99, 0x89, 0x2f, 0xda, 0x3c,
0xa1, 0x59, 0xff, 0x9a, 0x50, 0xc5, 0x7c, 0x76, 0xcf, 0x61, 0x11, 0x0d, 0xd0, 0x6d, 0x58, 0x1b,
0xfe, 0x48, 0x1c, 0x4f, 0x2e, 0xc6, 0xec, 0x5d, 0x7d, 0x43, 0xff, 0x2a, 0x74, 0x77, 0x5f, 0x40,
0x07, 0x05, 0xac, 0x38, 0xe8, 0x5b, 0x00, 0xc7, 0x8b, 0x68, 0xc0, 0x28, 0x39, 0x56, 0xe3, 0xc1,
0xec, 0x75, 0x4e, 0x56, 0xf8, 0x26, 0xc1, 0x0f, 0x0a, 0x38, 0xc3, 0x46, 0xf7, 0xa1, 0x3a, 0xe4,
0xde, 0xc8, 0x89, 0x1c, 0xae, 0x9a, 0xd3, 0xec, 0xbd, 0x7f, 0x4a, 0x32, 0x31, 0x7c, 0x50, 0xc0,
0x29, 0x17, 0x6d, 0xc0, 0x6a, 0xe8, 0x78, 0x47, 0xad, 0xe6, 0xb6, 0xd1, 0xa9, 0x0c, 0x0a, 0x58,
0xbe, 0xa1, 0x0e, 0x34, 0x7c, 0x12, 0x86, 0x36, 0x61, 0xcc, 0x1e, 0x4b, 0x7e, 0xeb, 0x9c, 0x06,
0xac, 0x0b, 0xc7, 0x1e, 0x63, 0xba, 0x22, 0x3b, 0xd0, 0x3c, 0x64, 0x7c, 0x78, 0x94, 0x85, 0x22,
0x0d, 0xad, 0x4b, 0x4f, 0x8a, 0xfd, 0x04, 0x36, 0xf4, 0x74, 0xb0, 0x03, 0x3a, 0xa1, 0xaf, 0x62,
0xfc, 0xaa, 0xde, 0xeb, 0x73, 0x6a, 0x56, 0x60, 0xe1, 0xd3, 0x94, 0x0f, 0x41, 0x18, 0xed, 0x90,
0xb8, 0x3e, 0xa3, 0x31, 0xbe, 0xbe, 0x6d, 0x74, 0x8c, 0x41, 0x01, 0x37, 0x02, 0x3e, 0x7b, 0x2e,
0x3d, 0x1a, 0xfd, 0x39, 0xb4, 0x32, 0x63, 0x21, 0x1f, 0x44, 0x9c, 0xad, 0xea, 0xa0, 0x80, 0x2f,
0xa4, 0x53, 0x22, 0x1b, 0x68, 0x1f, 0xb6, 0xd4, 0xc7, 0x24, 0x73, 0x26, 0x73, 0xfc, 0x92, 0x4e,
0xb2, 0xad, 0x60, 0xc9, 0xf1, 0xcc, 0x8a, 0x3c, 0x83, 0xf3, 0x5a, 0x44, 0x8e, 0xb9, 0x98, 0x5a,
0x96, 0xfb, 0x73, 0xe5, 0x84, 0x0f, 0x99, 0x40, 0x8b, 0x02, 0x0c, 0xd3, 0x57, 0x2d, 0xf9, 0x12,
0x36, 0xd3, 0x83, 0x98, 0x53, 0xad, 0x48, 0x55, 0x6b, 0x99, 0x6a, 0x7e, 0x0c, 0x88, 0x61, 0x17,
0xe5, 0x2c, 0x5a, 0xbb, 0x0b, 0x48, 0x9d, 0x8d, 0xdc, 0x42, 0xab, 0xf1, 0x39, 0x95, 0xbe, 0xec,
0xf2, 0x1e, 0x27, 0xf8, 0x6c, 0x1e, 0x0d, 0x99, 0xc7, 0xbb, 0xcb, 0xf2, 0x48, 0x67, 0x42, 0xaa,
0x97, 0x89, 0xff, 0x15, 0xbc, 0x23, 0x3f, 0xb3, 0xb6, 0x2f, 0x8a, 0xcd, 0x67, 0x36, 0x1f, 0x8f,
0x43, 0x1a, 0xc5, 0xc2, 0xb0, 0x6d, 0x74, 0xd6, 0x06, 0x05, 0x7c, 0x51, 0x82, 0x9e, 0xd2, 0x00,
0xf3, 0xd9, 0x13, 0x89, 0xd0, 0xfc, 0x2f, 0xa1, 0x9d, 0xe7, 0x33, 0xc7, 0x75, 0x12, 0xba, 0xa9,
0xe9, 0x9b, 0x19, 0xfa, 0x43, 0x01, 0xd0, 0xec, 0x3e, 0x6c, 0xa5, 0x6c, 0xbd, 0x6d, 0x39, 0x81,
0x9a, 0x16, 0xb8, 0x14, 0x0b, 0xa8, 0xcd, 0xca, 0x6a, 0x7c, 0x06, 0x17, 0xc3, 0x28, 0x70, 0x7c,
0x3d, 0x63, 0xa2, 0x80, 0x78, 0xe1, 0x98, 0x07, 0x2e, 0x0d, 0x5a, 0xeb, 0xfa, 0x10, 0x5c, 0x90,
0x00, 0x59, 0x89, 0x83, 0xd4, 0x2d, 0x98, 0xc4, 0xf7, 0xd9, 0xdc, 0x96, 0x17, 0x81, 0x1c, 0xf3,
0x7c, 0xdc, 0xa9, 0x12, 0xf0, 0x50, 0xf8, 0x33, 0xcc, 0xf6, 0x1d, 0x58, 0x93, 0x83, 0x05, 0x7d,
0x0a, 0x65, 0x95, 0xa9, 0xfa, 0xd6, 0x9a, 0xbd, 0xad, 0x13, 0x27, 0x00, 0x8e, 0xd1, 0xed, 0xaf,
0x01, 0xd2, 0xc1, 0xf2, 0xff, 0x65, 0xfe, 0x32, 0xa0, 0x9a, 0x4c, 0x15, 0x34, 0x80, 0xa6, 0x1f,
0xd0, 0x91, 0x33, 0x24, 0x51, 0xd2, 0x1a, 0x6a, 0x4a, 0x9e, 0xa2, 0xd7, 0x48, 0x68, 0x49, 0x5b,
0x98, 0x51, 0x30, 0x4d, 0x44, 0x56, 0xce, 0x22, 0x02, 0x82, 0xa1, 0xf9, 0x77, 0xa0, 0x36, 0x26,
0x2c, 0x4c, 0x04, 0x8a, 0x67, 0x11, 0x30, 0x25, 0x45, 0xbd, 0xf4, 0x2b, 0x50, 0x52, 0x5c, 0xeb,
0x9f, 0x35, 0xa8, 0x3c, 0x9a, 0x46, 0x44, 0x2e, 0x71, 0x0f, 0x2a, 0xa2, 0x3d, 0x45, 0x3b, 0xe8,
0xa5, 0xbd, 0xb7, 0x4c, 0x34, 0xc6, 0x77, 0x9f, 0xd3, 0x48, 0xdc, 0x1e, 0x07, 0x05, 0x5c, 0x0e,
0xd5, 0x23, 0xfa, 0x1e, 0xd0, 0x88, 0x32, 0x2a, 0x4a, 0x14, 0x70, 0x57, 0xb7, 0x9d, 0x5e, 0xe2,
0x47, 0x27, 0x8a, 0xdd, 0x95, 0xb4, 0x7b, 0x01, 0x77, 0x55, 0x1b, 0x8a, 0x13, 0x35, 0x5a, 0xb0,
0x2d, 0xca, 0xab, 0x51, 0xa7, 0x0b, 0x70, 0x56, 0x79, 0x75, 0x39, 0xcf, 0xcb, 0xeb, 0x0b, 0xfb,
0x01, 0x34, 0xb2, 0xf2, 0x01, 0x9f, 0xc9, 0xd9, 0x6d, 0xf6, 0x76, 0xce, 0xa8, 0x8d, 0xf9, 0x4c,
0x7c, 0x42, 0x46, 0x59, 0x43, 0xfb, 0x17, 0x03, 0xca, 0xba, 0x54, 0xa7, 0x5f, 0xec, 0xae, 0x43,
0x73, 0x71, 0x4e, 0xeb, 0x9b, 0x76, 0x63, 0x61, 0x30, 0x2f, 0xbd, 0xb4, 0x17, 0x4f, 0xb9, 0xb4,
0xaf, 0x66, 0x2e, 0xed, 0xed, 0x5f, 0x0d, 0x68, 0x2e, 0x96, 0xfd, 0xad, 0x66, 0xb8, 0x07, 0x20,
0x32, 0x51, 0xf3, 0x54, 0x6f, 0xd3, 0x19, 0x06, 0x3a, 0xae, 0x0a, 0x96, 0x7c, 0x6c, 0xdf, 0xc8,
0xa6, 0xa8, 0xb7, 0xe9, 0xb4, 0x14, 0xdb, 0x0d, 0x58, 0xcf, 0xed, 0x49, 0x1f, 0xa0, 0xe2, 0xea,
0xdd, 0xb2, 0xfe, 0x30, 0xe0, 0x3c, 0xa6, 0x64, 0xf4, 0x88, 0x8f, 0x9c, 0xf1, 0xfc, 0xbb, 0xc0,
0x89, 0x28, 0x9e, 0x32, 0xfa, 0x56, 0x17, 0x7e, 0x15, 0x6a, 0xc4, 0xf7, 0x93, 0x5b, 0x56, 0x72,
0xbd, 0x36, 0x95, 0x55, 0x4e, 0x4b, 0xf4, 0x01, 0x34, 0x1d, 0x6f, 0x18, 0x50, 0x97, 0x7a, 0x91,
0x4d, 0x5c, 0x3e, 0xf5, 0x22, 0xb9, 0x3f, 0x45, 0xf1, 0xe9, 0x4f, 0x3c, 0x7b, 0xd2, 0xd1, 0x2f,
0xc1, 0x6a, 0x30, 0x65, 0xb4, 0xff, 0x33, 0x6c, 0x0e, 0xb9, 0xbb, 0xa4, 0x86, 0xfd, 0xea, 0x5d,
0x12, 0x91, 0xa7, 0xe2, 0x3f, 0xf7, 0xa9, 0xf1, 0xf2, 0x0b, 0x0d, 0x98, 0x70, 0x46, 0xbc, 0x49,
0x97, 0x07, 0x93, 0xdd, 0x09, 0xf5, 0xe4, 0x5f, 0xf0, 0xae, 0x72, 0x11, 0xdf, 0x09, 0xb3, 0xff,
0xc9, 0xb7, 0xe3, 0xe7, 0xdf, 0x57, 0x5a, 0xf7, 0x15, 0x79, 0x9f, 0xf1, 0xe9, 0xa8, 0xdb, 0x8f,
0x63, 0xbc, 0xe8, 0x1d, 0x96, 0xa4, 0xc2, 0x8d, 0xff, 0x02, 0x00, 0x00, 0xff, 0xff, 0x32, 0x40,
0x30, 0x5e, 0x68, 0x0f, 0x00, 0x00,
}

View File

@ -17,6 +17,7 @@ It has these top-level messages:
DependencyEdge
EntityMention
TextSpan
ClassificationCategory
AnalyzeSentimentRequest
AnalyzeSentimentResponse
AnalyzeEntitySentimentRequest
@ -25,6 +26,8 @@ It has these top-level messages:
AnalyzeEntitiesResponse
AnalyzeSyntaxRequest
AnalyzeSyntaxResponse
ClassifyTextRequest
ClassifyTextResponse
AnnotateTextRequest
AnnotateTextResponse
*/
@ -1069,7 +1072,7 @@ type Document struct {
// The language of the document (if not specified, the language is
// automatically detected). Both ISO and BCP-47 language codes are
// accepted.<br>
// [Language Support](https://cloud.google.com/natural-language/docs/languages)
// [Language Support](/natural-language/docs/languages)
// lists currently supported languages for each API method.
// If the language (either specified by the caller or automatically detected)
// is not supported by the called API method, an `INVALID_ARGUMENT` error
@ -1595,6 +1598,34 @@ func (m *TextSpan) GetBeginOffset() int32 {
return 0
}
// Represents a category returned from the text classifier.
type ClassificationCategory struct {
// The name of the category representing the document.
Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"`
// The classifier's confidence of the category. Number represents how certain
// the classifier is that this category represents the given text.
Confidence float32 `protobuf:"fixed32,2,opt,name=confidence" json:"confidence,omitempty"`
}
func (m *ClassificationCategory) Reset() { *m = ClassificationCategory{} }
func (m *ClassificationCategory) String() string { return proto.CompactTextString(m) }
func (*ClassificationCategory) ProtoMessage() {}
func (*ClassificationCategory) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{9} }
func (m *ClassificationCategory) GetName() string {
if m != nil {
return m.Name
}
return ""
}
func (m *ClassificationCategory) GetConfidence() float32 {
if m != nil {
return m.Confidence
}
return 0
}
// The sentiment analysis request message.
type AnalyzeSentimentRequest struct {
// Input document.
@ -1607,7 +1638,7 @@ type AnalyzeSentimentRequest struct {
func (m *AnalyzeSentimentRequest) Reset() { *m = AnalyzeSentimentRequest{} }
func (m *AnalyzeSentimentRequest) String() string { return proto.CompactTextString(m) }
func (*AnalyzeSentimentRequest) ProtoMessage() {}
func (*AnalyzeSentimentRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{9} }
func (*AnalyzeSentimentRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{10} }
func (m *AnalyzeSentimentRequest) GetDocument() *Document {
if m != nil {
@ -1638,7 +1669,7 @@ type AnalyzeSentimentResponse struct {
func (m *AnalyzeSentimentResponse) Reset() { *m = AnalyzeSentimentResponse{} }
func (m *AnalyzeSentimentResponse) String() string { return proto.CompactTextString(m) }
func (*AnalyzeSentimentResponse) ProtoMessage() {}
func (*AnalyzeSentimentResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{10} }
func (*AnalyzeSentimentResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{11} }
func (m *AnalyzeSentimentResponse) GetDocumentSentiment() *Sentiment {
if m != nil {
@ -1672,7 +1703,7 @@ type AnalyzeEntitySentimentRequest struct {
func (m *AnalyzeEntitySentimentRequest) Reset() { *m = AnalyzeEntitySentimentRequest{} }
func (m *AnalyzeEntitySentimentRequest) String() string { return proto.CompactTextString(m) }
func (*AnalyzeEntitySentimentRequest) ProtoMessage() {}
func (*AnalyzeEntitySentimentRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{11} }
func (*AnalyzeEntitySentimentRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{12} }
func (m *AnalyzeEntitySentimentRequest) GetDocument() *Document {
if m != nil {
@ -1701,7 +1732,7 @@ type AnalyzeEntitySentimentResponse struct {
func (m *AnalyzeEntitySentimentResponse) Reset() { *m = AnalyzeEntitySentimentResponse{} }
func (m *AnalyzeEntitySentimentResponse) String() string { return proto.CompactTextString(m) }
func (*AnalyzeEntitySentimentResponse) ProtoMessage() {}
func (*AnalyzeEntitySentimentResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{12} }
func (*AnalyzeEntitySentimentResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{13} }
func (m *AnalyzeEntitySentimentResponse) GetEntities() []*Entity {
if m != nil {
@ -1728,7 +1759,7 @@ type AnalyzeEntitiesRequest struct {
func (m *AnalyzeEntitiesRequest) Reset() { *m = AnalyzeEntitiesRequest{} }
func (m *AnalyzeEntitiesRequest) String() string { return proto.CompactTextString(m) }
func (*AnalyzeEntitiesRequest) ProtoMessage() {}
func (*AnalyzeEntitiesRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{13} }
func (*AnalyzeEntitiesRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{14} }
func (m *AnalyzeEntitiesRequest) GetDocument() *Document {
if m != nil {
@ -1757,7 +1788,7 @@ type AnalyzeEntitiesResponse struct {
func (m *AnalyzeEntitiesResponse) Reset() { *m = AnalyzeEntitiesResponse{} }
func (m *AnalyzeEntitiesResponse) String() string { return proto.CompactTextString(m) }
func (*AnalyzeEntitiesResponse) ProtoMessage() {}
func (*AnalyzeEntitiesResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{14} }
func (*AnalyzeEntitiesResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{15} }
func (m *AnalyzeEntitiesResponse) GetEntities() []*Entity {
if m != nil {
@ -1784,7 +1815,7 @@ type AnalyzeSyntaxRequest struct {
func (m *AnalyzeSyntaxRequest) Reset() { *m = AnalyzeSyntaxRequest{} }
func (m *AnalyzeSyntaxRequest) String() string { return proto.CompactTextString(m) }
func (*AnalyzeSyntaxRequest) ProtoMessage() {}
func (*AnalyzeSyntaxRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{15} }
func (*AnalyzeSyntaxRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{16} }
func (m *AnalyzeSyntaxRequest) GetDocument() *Document {
if m != nil {
@ -1815,7 +1846,7 @@ type AnalyzeSyntaxResponse struct {
func (m *AnalyzeSyntaxResponse) Reset() { *m = AnalyzeSyntaxResponse{} }
func (m *AnalyzeSyntaxResponse) String() string { return proto.CompactTextString(m) }
func (*AnalyzeSyntaxResponse) ProtoMessage() {}
func (*AnalyzeSyntaxResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{16} }
func (*AnalyzeSyntaxResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{17} }
func (m *AnalyzeSyntaxResponse) GetSentences() []*Sentence {
if m != nil {
@ -1838,6 +1869,42 @@ func (m *AnalyzeSyntaxResponse) GetLanguage() string {
return ""
}
// The document classification request message.
type ClassifyTextRequest struct {
// Input document.
Document *Document `protobuf:"bytes,1,opt,name=document" json:"document,omitempty"`
}
func (m *ClassifyTextRequest) Reset() { *m = ClassifyTextRequest{} }
func (m *ClassifyTextRequest) String() string { return proto.CompactTextString(m) }
func (*ClassifyTextRequest) ProtoMessage() {}
func (*ClassifyTextRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{18} }
func (m *ClassifyTextRequest) GetDocument() *Document {
if m != nil {
return m.Document
}
return nil
}
// The document classification response message.
type ClassifyTextResponse struct {
// Categories representing the input document.
Categories []*ClassificationCategory `protobuf:"bytes,1,rep,name=categories" json:"categories,omitempty"`
}
func (m *ClassifyTextResponse) Reset() { *m = ClassifyTextResponse{} }
func (m *ClassifyTextResponse) String() string { return proto.CompactTextString(m) }
func (*ClassifyTextResponse) ProtoMessage() {}
func (*ClassifyTextResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{19} }
func (m *ClassifyTextResponse) GetCategories() []*ClassificationCategory {
if m != nil {
return m.Categories
}
return nil
}
// The request message for the text annotation API, which can perform multiple
// analysis types (sentiment, entities, and syntax) in one call.
type AnnotateTextRequest struct {
@ -1852,7 +1919,7 @@ type AnnotateTextRequest struct {
func (m *AnnotateTextRequest) Reset() { *m = AnnotateTextRequest{} }
func (m *AnnotateTextRequest) String() string { return proto.CompactTextString(m) }
func (*AnnotateTextRequest) ProtoMessage() {}
func (*AnnotateTextRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{17} }
func (*AnnotateTextRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{20} }
func (m *AnnotateTextRequest) GetDocument() *Document {
if m != nil {
@ -1886,13 +1953,15 @@ type AnnotateTextRequest_Features struct {
ExtractDocumentSentiment bool `protobuf:"varint,3,opt,name=extract_document_sentiment,json=extractDocumentSentiment" json:"extract_document_sentiment,omitempty"`
// Extract entities and their associated sentiment.
ExtractEntitySentiment bool `protobuf:"varint,4,opt,name=extract_entity_sentiment,json=extractEntitySentiment" json:"extract_entity_sentiment,omitempty"`
// Classify the full document into categories.
ClassifyText bool `protobuf:"varint,6,opt,name=classify_text,json=classifyText" json:"classify_text,omitempty"`
}
func (m *AnnotateTextRequest_Features) Reset() { *m = AnnotateTextRequest_Features{} }
func (m *AnnotateTextRequest_Features) String() string { return proto.CompactTextString(m) }
func (*AnnotateTextRequest_Features) ProtoMessage() {}
func (*AnnotateTextRequest_Features) Descriptor() ([]byte, []int) {
return fileDescriptor0, []int{17, 0}
return fileDescriptor0, []int{20, 0}
}
func (m *AnnotateTextRequest_Features) GetExtractSyntax() bool {
@ -1923,6 +1992,13 @@ func (m *AnnotateTextRequest_Features) GetExtractEntitySentiment() bool {
return false
}
func (m *AnnotateTextRequest_Features) GetClassifyText() bool {
if m != nil {
return m.ClassifyText
}
return false
}
// The text annotations response message.
type AnnotateTextResponse struct {
// Sentences in the input document. Populated if the user enables
@ -1943,12 +2019,14 @@ type AnnotateTextResponse struct {
// in the request or, if not specified, the automatically-detected language.
// See [Document.language][google.cloud.language.v1beta2.Document.language] field for more details.
Language string `protobuf:"bytes,5,opt,name=language" json:"language,omitempty"`
// Categories identified in the input document.
Categories []*ClassificationCategory `protobuf:"bytes,6,rep,name=categories" json:"categories,omitempty"`
}
func (m *AnnotateTextResponse) Reset() { *m = AnnotateTextResponse{} }
func (m *AnnotateTextResponse) String() string { return proto.CompactTextString(m) }
func (*AnnotateTextResponse) ProtoMessage() {}
func (*AnnotateTextResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{18} }
func (*AnnotateTextResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{21} }
func (m *AnnotateTextResponse) GetSentences() []*Sentence {
if m != nil {
@ -1985,6 +2063,13 @@ func (m *AnnotateTextResponse) GetLanguage() string {
return ""
}
func (m *AnnotateTextResponse) GetCategories() []*ClassificationCategory {
if m != nil {
return m.Categories
}
return nil
}
func init() {
proto.RegisterType((*Document)(nil), "google.cloud.language.v1beta2.Document")
proto.RegisterType((*Sentence)(nil), "google.cloud.language.v1beta2.Sentence")
@ -1995,6 +2080,7 @@ func init() {
proto.RegisterType((*DependencyEdge)(nil), "google.cloud.language.v1beta2.DependencyEdge")
proto.RegisterType((*EntityMention)(nil), "google.cloud.language.v1beta2.EntityMention")
proto.RegisterType((*TextSpan)(nil), "google.cloud.language.v1beta2.TextSpan")
proto.RegisterType((*ClassificationCategory)(nil), "google.cloud.language.v1beta2.ClassificationCategory")
proto.RegisterType((*AnalyzeSentimentRequest)(nil), "google.cloud.language.v1beta2.AnalyzeSentimentRequest")
proto.RegisterType((*AnalyzeSentimentResponse)(nil), "google.cloud.language.v1beta2.AnalyzeSentimentResponse")
proto.RegisterType((*AnalyzeEntitySentimentRequest)(nil), "google.cloud.language.v1beta2.AnalyzeEntitySentimentRequest")
@ -2003,6 +2089,8 @@ func init() {
proto.RegisterType((*AnalyzeEntitiesResponse)(nil), "google.cloud.language.v1beta2.AnalyzeEntitiesResponse")
proto.RegisterType((*AnalyzeSyntaxRequest)(nil), "google.cloud.language.v1beta2.AnalyzeSyntaxRequest")
proto.RegisterType((*AnalyzeSyntaxResponse)(nil), "google.cloud.language.v1beta2.AnalyzeSyntaxResponse")
proto.RegisterType((*ClassifyTextRequest)(nil), "google.cloud.language.v1beta2.ClassifyTextRequest")
proto.RegisterType((*ClassifyTextResponse)(nil), "google.cloud.language.v1beta2.ClassifyTextResponse")
proto.RegisterType((*AnnotateTextRequest)(nil), "google.cloud.language.v1beta2.AnnotateTextRequest")
proto.RegisterType((*AnnotateTextRequest_Features)(nil), "google.cloud.language.v1beta2.AnnotateTextRequest.Features")
proto.RegisterType((*AnnotateTextResponse)(nil), "google.cloud.language.v1beta2.AnnotateTextResponse")
@ -2049,8 +2137,10 @@ type LanguageServiceClient interface {
// tokenization along with part of speech tags, dependency trees, and other
// properties.
AnalyzeSyntax(ctx context.Context, in *AnalyzeSyntaxRequest, opts ...grpc.CallOption) (*AnalyzeSyntaxResponse, error)
// A convenience method that provides all syntax, sentiment, and entity
// features in one call.
// Classifies a document into categories.
ClassifyText(ctx context.Context, in *ClassifyTextRequest, opts ...grpc.CallOption) (*ClassifyTextResponse, error)
// A convenience method that provides all syntax, sentiment, entity, and
// classification features in one call.
AnnotateText(ctx context.Context, in *AnnotateTextRequest, opts ...grpc.CallOption) (*AnnotateTextResponse, error)
}
@ -2098,6 +2188,15 @@ func (c *languageServiceClient) AnalyzeSyntax(ctx context.Context, in *AnalyzeSy
return out, nil
}
func (c *languageServiceClient) ClassifyText(ctx context.Context, in *ClassifyTextRequest, opts ...grpc.CallOption) (*ClassifyTextResponse, error) {
out := new(ClassifyTextResponse)
err := grpc.Invoke(ctx, "/google.cloud.language.v1beta2.LanguageService/ClassifyText", in, out, c.cc, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *languageServiceClient) AnnotateText(ctx context.Context, in *AnnotateTextRequest, opts ...grpc.CallOption) (*AnnotateTextResponse, error) {
out := new(AnnotateTextResponse)
err := grpc.Invoke(ctx, "/google.cloud.language.v1beta2.LanguageService/AnnotateText", in, out, c.cc, opts...)
@ -2123,8 +2222,10 @@ type LanguageServiceServer interface {
// tokenization along with part of speech tags, dependency trees, and other
// properties.
AnalyzeSyntax(context.Context, *AnalyzeSyntaxRequest) (*AnalyzeSyntaxResponse, error)
// A convenience method that provides all syntax, sentiment, and entity
// features in one call.
// Classifies a document into categories.
ClassifyText(context.Context, *ClassifyTextRequest) (*ClassifyTextResponse, error)
// A convenience method that provides all syntax, sentiment, entity, and
// classification features in one call.
AnnotateText(context.Context, *AnnotateTextRequest) (*AnnotateTextResponse, error)
}
@ -2204,6 +2305,24 @@ func _LanguageService_AnalyzeSyntax_Handler(srv interface{}, ctx context.Context
return interceptor(ctx, in, info, handler)
}
func _LanguageService_ClassifyText_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ClassifyTextRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(LanguageServiceServer).ClassifyText(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/google.cloud.language.v1beta2.LanguageService/ClassifyText",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(LanguageServiceServer).ClassifyText(ctx, req.(*ClassifyTextRequest))
}
return interceptor(ctx, in, info, handler)
}
func _LanguageService_AnnotateText_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(AnnotateTextRequest)
if err := dec(in); err != nil {
@ -2242,6 +2361,10 @@ var _LanguageService_serviceDesc = grpc.ServiceDesc{
MethodName: "AnalyzeSyntax",
Handler: _LanguageService_AnalyzeSyntax_Handler,
},
{
MethodName: "ClassifyText",
Handler: _LanguageService_ClassifyText_Handler,
},
{
MethodName: "AnnotateText",
Handler: _LanguageService_AnnotateText_Handler,
@ -2256,185 +2379,193 @@ func init() {
}
var fileDescriptor0 = []byte{
// 2873 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xd4, 0x5a, 0x4d, 0x73, 0xdb, 0xc6,
0xf9, 0x37, 0xf8, 0x26, 0x72, 0x29, 0xc9, 0x6b, 0xc4, 0x89, 0xf9, 0xd7, 0x3f, 0x2f, 0x0e, 0x12,
0xd7, 0x8a, 0x9d, 0x50, 0xb1, 0xec, 0x38, 0xae, 0xed, 0xbc, 0x40, 0xc0, 0x92, 0x82, 0x4c, 0x02,
0xc8, 0x02, 0xa0, 0xe5, 0x5c, 0x38, 0x30, 0xb9, 0x62, 0x38, 0x91, 0x00, 0x96, 0x80, 0x3c, 0x56,
0x2f, 0x99, 0xc9, 0x4c, 0x8f, 0x99, 0x1e, 0x72, 0xe8, 0x07, 0xe8, 0xa1, 0xa7, 0x4e, 0x3a, 0xd3,
0x99, 0x4e, 0xfb, 0x19, 0x7a, 0x4c, 0xa7, 0xa7, 0x1e, 0x7b, 0xec, 0xa1, 0x87, 0x1e, 0x7a, 0xec,
0x3c, 0xbb, 0x0b, 0xbe, 0xc8, 0x8e, 0x25, 0x3a, 0x99, 0x4e, 0x7a, 0xdb, 0x7d, 0xf0, 0xfc, 0x9e,
0x7d, 0xde, 0x9f, 0x05, 0x48, 0x74, 0x63, 0x10, 0xc7, 0x83, 0x7d, 0xb6, 0xd1, 0xdb, 0x8f, 0x0f,
0xfb, 0x1b, 0xfb, 0x61, 0x34, 0x38, 0x0c, 0x07, 0x6c, 0xe3, 0xd1, 0xb5, 0x87, 0x2c, 0x0d, 0x37,
0x27, 0x84, 0x6e, 0xc2, 0xc6, 0x8f, 0x86, 0x3d, 0x56, 0x1f, 0x8d, 0xe3, 0x34, 0x56, 0x5f, 0x11,
0xa8, 0x3a, 0x47, 0xd5, 0x33, 0xa6, 0xba, 0x44, 0xad, 0xbd, 0x2c, 0x85, 0x86, 0xa3, 0xe1, 0x46,
0x18, 0x45, 0x71, 0x1a, 0xa6, 0xc3, 0x38, 0x4a, 0x04, 0x78, 0xed, 0x0d, 0xf9, 0x74, 0x3f, 0x8e,
0x06, 0xe3, 0xc3, 0x28, 0x1a, 0x46, 0x83, 0x8d, 0x78, 0xc4, 0xc6, 0x73, 0x4c, 0xaf, 0x49, 0x26,
0xbe, 0x7b, 0x78, 0xb8, 0xb7, 0x91, 0x0e, 0x0f, 0x58, 0x92, 0x86, 0x07, 0x23, 0xc9, 0x70, 0x41,
0x32, 0x8c, 0x47, 0xbd, 0x8d, 0x24, 0x0d, 0xd3, 0x43, 0x89, 0xd4, 0xfe, 0xa9, 0xa0, 0xb2, 0x19,
0xf7, 0x0e, 0x0f, 0x58, 0x94, 0xaa, 0x1f, 0xa3, 0x42, 0x7a, 0x34, 0x62, 0x35, 0xe5, 0xa2, 0xb2,
0xbe, 0xba, 0xf9, 0x76, 0xfd, 0x99, 0x7a, 0xd7, 0x33, 0x58, 0xdd, 0x3f, 0x1a, 0x31, 0xca, 0x91,
0xea, 0x1a, 0x5a, 0xea, 0xc5, 0x51, 0xca, 0xa2, 0xb4, 0x96, 0xbb, 0xa8, 0xac, 0x57, 0xb6, 0xcf,
0xd0, 0x8c, 0xa0, 0xae, 0xa3, 0xb3, 0x83, 0x5e, 0xd2, 0x95, 0xdb, 0xee, 0xe1, 0x78, 0x58, 0xcb,
0x4b, 0x9e, 0x95, 0x41, 0x2f, 0x31, 0x04, 0x3d, 0x18, 0x0f, 0xd5, 0x35, 0x54, 0xce, 0x4e, 0xab,
0x15, 0x80, 0x85, 0x4e, 0xf6, 0xda, 0x4d, 0x54, 0x80, 0xf3, 0xd4, 0xf3, 0x08, 0xfb, 0x0f, 0x5c,
0xd2, 0x0d, 0x6c, 0xcf, 0x25, 0x86, 0xd5, 0xb0, 0x88, 0x89, 0xcf, 0xa8, 0xab, 0x08, 0xb9, 0x2d,
0xdd, 0xb2, 0xbb, 0x3e, 0xd9, 0xf5, 0xb1, 0xa2, 0x96, 0x51, 0x61, 0xdb, 0x6f, 0xb7, 0x70, 0x6e,
0xab, 0x8c, 0x4a, 0x49, 0x7c, 0x38, 0xee, 0x31, 0xed, 0x97, 0x0a, 0x2a, 0x7b, 0x0c, 0x0e, 0xeb,
0x31, 0xf5, 0x0e, 0x2a, 0xa4, 0xec, 0x71, 0xca, 0x4d, 0xae, 0x6e, 0x5e, 0x3e, 0xc1, 0x64, 0x9f,
0x3d, 0x4e, 0xbd, 0x51, 0x18, 0x51, 0x0e, 0x52, 0x1b, 0xa8, 0x92, 0xb0, 0x08, 0x7c, 0x2d, 0xed,
0xad, 0x6e, 0xae, 0x9f, 0x20, 0xc1, 0xcb, 0xf8, 0xe9, 0x14, 0xaa, 0x7d, 0x5d, 0x40, 0x25, 0x12,
0xa5, 0xc3, 0xf4, 0x48, 0x55, 0x51, 0x21, 0x0a, 0x0f, 0x44, 0x08, 0x2a, 0x94, 0xaf, 0xd5, 0x0f,
0x65, 0x58, 0x72, 0x3c, 0x2c, 0x57, 0x4e, 0x38, 0x41, 0x08, 0x9a, 0x0d, 0x8a, 0x83, 0xca, 0x07,
0x2c, 0x0d, 0xfb, 0x61, 0x1a, 0xd6, 0xf2, 0x17, 0xf3, 0xeb, 0xd5, 0xcd, 0xeb, 0xa7, 0x93, 0xd1,
0x96, 0x28, 0x12, 0xa5, 0xe3, 0x23, 0x3a, 0x11, 0x02, 0xf1, 0x49, 0xc2, 0xfd, 0x21, 0x38, 0x90,
0xc7, 0x27, 0x47, 0x27, 0x7b, 0x75, 0x1b, 0x0e, 0x8b, 0x78, 0x72, 0xd6, 0x8a, 0xfc, 0xb0, 0xb7,
0x4f, 0x75, 0x58, 0x5b, 0x80, 0xe8, 0x04, 0x3d, 0xef, 0xdd, 0xd2, 0x73, 0x7b, 0x77, 0xed, 0x0e,
0x5a, 0x99, 0x33, 0x44, 0xc5, 0x28, 0xff, 0x39, 0x3b, 0x92, 0x2e, 0x86, 0xa5, 0x7a, 0x1e, 0x15,
0x1f, 0x85, 0xfb, 0x87, 0xc2, 0xc5, 0x15, 0x2a, 0x36, 0xb7, 0x73, 0xb7, 0x14, 0xed, 0x48, 0xa6,
0x5b, 0x15, 0x2d, 0x05, 0xf6, 0x3d, 0xdb, 0xb9, 0x6f, 0xe3, 0x33, 0x2a, 0x42, 0x25, 0x97, 0x50,
0xcf, 0xb1, 0xb1, 0xa2, 0x2e, 0xa3, 0x72, 0xcb, 0x31, 0x74, 0xdf, 0x72, 0x6c, 0x9c, 0x53, 0x31,
0x5a, 0x76, 0x68, 0x53, 0xb7, 0xad, 0x4f, 0x05, 0x25, 0xaf, 0x56, 0x50, 0x91, 0x74, 0x88, 0xed,
0xe3, 0x82, 0x7a, 0x16, 0x55, 0xef, 0x3b, 0xf4, 0x5e, 0xd7, 0x69, 0x74, 0x75, 0xea, 0xe3, 0xa2,
0x7a, 0x0e, 0xad, 0x18, 0x8e, 0xed, 0x05, 0x6d, 0x42, 0xbb, 0x4d, 0xc7, 0x31, 0x71, 0x09, 0xd8,
0x1d, 0x7f, 0x9b, 0x50, 0xbc, 0xa4, 0xfd, 0x22, 0x87, 0x8a, 0x7e, 0xfc, 0x39, 0x8b, 0xbe, 0x5f,
0x92, 0x7e, 0x82, 0x56, 0x47, 0xe1, 0x38, 0xed, 0xc6, 0x7b, 0xdd, 0x64, 0xc4, 0x58, 0xef, 0x33,
0x99, 0xa9, 0x57, 0x4f, 0x10, 0xe3, 0x86, 0xe3, 0xd4, 0xd9, 0xf3, 0x38, 0x84, 0x2e, 0x8f, 0x66,
0x76, 0x6a, 0x07, 0x9d, 0xed, 0xb3, 0x11, 0x8b, 0xfa, 0x2c, 0xea, 0x1d, 0x75, 0x59, 0x7f, 0xc0,
0x78, 0x25, 0x57, 0x37, 0xdf, 0x39, 0xa9, 0x65, 0x4c, 0x50, 0xa4, 0x3f, 0x60, 0x74, 0xb5, 0x3f,
0xb7, 0x87, 0x30, 0xec, 0xb3, 0x83, 0x83, 0x50, 0x16, 0xbd, 0xd8, 0x68, 0x1f, 0xa1, 0xca, 0x24,
0xae, 0xea, 0xcb, 0xa8, 0x72, 0x10, 0x0e, 0xa2, 0x61, 0x7a, 0xd8, 0x17, 0xd1, 0xca, 0xd1, 0x29,
0x01, 0x04, 0x24, 0xbd, 0x78, 0x2c, 0xd4, 0xc9, 0x51, 0xb1, 0xd1, 0xfe, 0x74, 0x0e, 0x2d, 0xcf,
0x5a, 0xa3, 0xea, 0x28, 0x9f, 0x86, 0x03, 0xd9, 0xe6, 0x36, 0x16, 0xf0, 0x43, 0xdd, 0x0f, 0x07,
0x14, 0xb0, 0xea, 0x0e, 0x2a, 0x85, 0xc9, 0x88, 0xf5, 0x52, 0x59, 0x95, 0x9b, 0x8b, 0x48, 0xd1,
0x39, 0x92, 0x4a, 0x09, 0xaa, 0x89, 0x0a, 0xbd, 0x30, 0x11, 0x4a, 0xaf, 0x6e, 0xbe, 0xbb, 0x88,
0x24, 0x23, 0x4c, 0x18, 0xe5, 0x68, 0x90, 0xb2, 0x17, 0x8f, 0x0f, 0xb8, 0xef, 0x16, 0x94, 0xd2,
0x88, 0xc7, 0x07, 0x94, 0xa3, 0xc1, 0xae, 0x01, 0x84, 0x64, 0x5c, 0x2b, 0x2e, 0x6e, 0x57, 0x93,
0x23, 0xa9, 0x94, 0x00, 0x1a, 0x1d, 0xc4, 0x71, 0x9f, 0xd7, 0xee, 0x82, 0x1a, 0xb5, 0xe3, 0xb8,
0x4f, 0x39, 0x1a, 0x34, 0x8a, 0x0e, 0x0f, 0x1e, 0xb2, 0x71, 0x6d, 0x69, 0x71, 0x8d, 0x6c, 0x8e,
0xa4, 0x52, 0x02, 0xc8, 0x1a, 0xb1, 0x71, 0x12, 0x47, 0xb5, 0xf2, 0xe2, 0xb2, 0x5c, 0x8e, 0xa4,
0x52, 0x02, 0x97, 0x35, 0x86, 0x49, 0x5c, 0xab, 0x3c, 0x87, 0x2c, 0x8e, 0xa4, 0x52, 0x82, 0xfa,
0x00, 0x55, 0xc7, 0xac, 0x37, 0x1c, 0x8d, 0xe3, 0xde, 0x30, 0x3d, 0xaa, 0x21, 0x2e, 0xf0, 0xfd,
0x45, 0x04, 0xd2, 0x29, 0x9c, 0xce, 0xca, 0x52, 0x9b, 0xa8, 0x98, 0xb2, 0x28, 0x61, 0xb5, 0x2a,
0x17, 0x7a, 0x6d, 0xa1, 0x6c, 0x07, 0x20, 0x15, 0x78, 0x10, 0xf4, 0x28, 0x1e, 0xf6, 0x58, 0x6d,
0x79, 0x71, 0x41, 0x1d, 0x00, 0x52, 0x81, 0xd7, 0xbe, 0x52, 0x50, 0xde, 0x0f, 0x07, 0xf3, 0x2d,
0x75, 0x09, 0xe5, 0x75, 0x73, 0x07, 0x2b, 0x62, 0xe1, 0xe2, 0x9c, 0x58, 0x74, 0x70, 0x1e, 0x66,
0xb8, 0xe1, 0xd8, 0x3b, 0xb8, 0x00, 0x24, 0x93, 0x40, 0xe3, 0x2c, 0xa3, 0x82, 0xed, 0x04, 0x36,
0x2e, 0x01, 0xc9, 0x0e, 0xda, 0x78, 0x09, 0x48, 0x2e, 0x75, 0x6c, 0x5c, 0x06, 0x92, 0x4b, 0x7d,
0x5c, 0x81, 0x5e, 0xea, 0x06, 0xb6, 0xe1, 0x63, 0x04, 0x4f, 0x3b, 0x84, 0x6e, 0xe1, 0xaa, 0x5a,
0x44, 0xca, 0x2e, 0x5e, 0x86, 0x67, 0x7a, 0xa3, 0x61, 0xed, 0xe2, 0x15, 0xcd, 0x41, 0x25, 0x51,
0x90, 0xaa, 0x8a, 0x56, 0x75, 0xb8, 0x4d, 0xf8, 0xdd, 0xa9, 0x62, 0x70, 0xa3, 0x20, 0xb4, 0x41,
0x0c, 0xdf, 0xea, 0x10, 0xac, 0x40, 0x87, 0xb7, 0xda, 0x33, 0x94, 0x1c, 0xb4, 0x75, 0x97, 0x3a,
0x4d, 0x4a, 0x3c, 0x0f, 0x08, 0x79, 0xed, 0xdf, 0x0a, 0x2a, 0x40, 0x61, 0x02, 0xaf, 0xa1, 0x7b,
0x64, 0x5e, 0x9a, 0x6e, 0x18, 0x81, 0xa7, 0x4b, 0x69, 0x2b, 0xa8, 0xa2, 0x9b, 0xa0, 0x99, 0xa5,
0xb7, 0x70, 0x4e, 0x0c, 0x84, 0xb6, 0xdb, 0x22, 0x6d, 0x62, 0x73, 0x8e, 0x3c, 0xcc, 0x1a, 0x53,
0x70, 0x17, 0x60, 0xd6, 0x34, 0x89, 0x6d, 0xf1, 0x5d, 0x91, 0x6b, 0x62, 0x7b, 0x3e, 0x0d, 0x80,
0x59, 0x6f, 0xe1, 0xd2, 0x74, 0x16, 0x75, 0x08, 0x5e, 0x82, 0xb3, 0x6c, 0xa7, 0x6d, 0xd9, 0x62,
0x5f, 0x06, 0x7f, 0x3b, 0x5b, 0x2d, 0xeb, 0x93, 0x80, 0xe0, 0x0a, 0x1c, 0xec, 0xea, 0xd4, 0x17,
0xb2, 0x10, 0x1c, 0xec, 0x52, 0xe2, 0x3a, 0x9e, 0x05, 0x63, 0x4b, 0x6f, 0xe1, 0x2a, 0x38, 0x83,
0x92, 0x46, 0x8b, 0xec, 0x5a, 0x1d, 0xd2, 0x05, 0x33, 0xf0, 0x32, 0xb0, 0x51, 0xd2, 0xe2, 0x02,
0x05, 0x69, 0x05, 0xce, 0xec, 0x64, 0x67, 0xae, 0x6a, 0xdf, 0x28, 0xa8, 0x00, 0xdd, 0x04, 0x94,
0x6b, 0x38, 0xb4, 0x3d, 0x63, 0xfa, 0x32, 0x2a, 0xeb, 0x26, 0x28, 0xa4, 0xb7, 0xa4, 0xe1, 0xc1,
0xae, 0xd5, 0xb2, 0x74, 0xfa, 0x00, 0xe7, 0xe0, 0xb0, 0x19, 0xc3, 0x3f, 0x25, 0x14, 0xe7, 0xb9,
0x08, 0xcb, 0xd6, 0x5b, 0x5d, 0x62, 0x9b, 0x96, 0xdd, 0xc4, 0x05, 0xf0, 0x45, 0x93, 0xd0, 0xc0,
0x36, 0x71, 0x11, 0xd6, 0x94, 0xe8, 0x2d, 0xcb, 0x13, 0x76, 0x5b, 0x54, 0xee, 0x96, 0x20, 0xb4,
0xde, 0xb6, 0x43, 0x7d, 0x5c, 0x86, 0xb0, 0xb7, 0x1c, 0xbb, 0x29, 0x72, 0xc1, 0xa1, 0x26, 0xa1,
0x18, 0x01, 0xb7, 0xbc, 0x32, 0x1a, 0xb8, 0xaa, 0x11, 0x54, 0x12, 0x6d, 0x0b, 0x74, 0x68, 0x12,
0xdb, 0x24, 0x74, 0x5e, 0xe9, 0x06, 0x69, 0x5b, 0xb6, 0x65, 0xcb, 0x68, 0xb5, 0x75, 0xcf, 0x08,
0x5a, 0xb0, 0xcd, 0x81, 0x0a, 0x36, 0x09, 0x7c, 0x50, 0x56, 0xfb, 0x02, 0x15, 0xa0, 0x67, 0x81,
0xd2, 0x6d, 0xc7, 0x31, 0x67, 0x44, 0x9c, 0x47, 0xd8, 0x70, 0x6c, 0x53, 0x3a, 0xb6, 0x0b, 0x4f,
0xb1, 0x02, 0xc1, 0xe1, 0x69, 0xa4, 0xcb, 0x24, 0x82, 0xbd, 0x6d, 0x5a, 0xd2, 0x91, 0x79, 0xf0,
0xb4, 0x65, 0xfb, 0x84, 0x52, 0xa7, 0x99, 0x45, 0xbf, 0x8a, 0x96, 0x76, 0x02, 0x91, 0x63, 0x45,
0x48, 0x3a, 0x2f, 0xd8, 0xda, 0x81, 0xf4, 0x06, 0x42, 0x49, 0xfb, 0x18, 0x95, 0x44, 0xb3, 0x03,
0x3b, 0xec, 0xa0, 0xbd, 0x75, 0xdc, 0x0e, 0xcf, 0xb2, 0x9b, 0x41, 0x4b, 0xa7, 0x58, 0xe1, 0xf7,
0x97, 0x56, 0x40, 0x79, 0xca, 0x95, 0x51, 0xc1, 0x0c, 0xf4, 0x16, 0xce, 0x6b, 0x3e, 0x2a, 0x89,
0x16, 0x07, 0x12, 0xc4, 0xfd, 0x66, 0x46, 0x42, 0x05, 0x15, 0x1b, 0x16, 0xf5, 0x7c, 0x01, 0xf7,
0x08, 0xd8, 0x84, 0x73, 0x40, 0xf6, 0xb7, 0x2d, 0x6a, 0xe2, 0x3c, 0x18, 0x3a, 0x4d, 0x18, 0x79,
0x3f, 0x2a, 0x68, 0xb7, 0x50, 0x49, 0x34, 0x3b, 0x2e, 0x95, 0x3a, 0xee, 0x9c, 0x5e, 0xa0, 0x09,
0xa7, 0x09, 0x97, 0xd8, 0x8e, 0xdf, 0x95, 0xfb, 0x9c, 0xb6, 0x83, 0xaa, 0x33, 0x5d, 0x4d, 0xbd,
0x80, 0x5e, 0xa0, 0xc4, 0xb0, 0x5c, 0xea, 0x18, 0x96, 0xff, 0x60, 0xbe, 0xa6, 0xb2, 0x07, 0x3c,
0xb5, 0xc0, 0x7e, 0xc7, 0xee, 0xce, 0xd0, 0x72, 0x5a, 0x82, 0x8a, 0xbc, 0x99, 0x81, 0x5f, 0x7d,
0x62, 0xcf, 0xd5, 0xe4, 0x8b, 0xe8, 0xdc, 0x6c, 0x80, 0xf8, 0x63, 0x61, 0x65, 0x23, 0xf0, 0x03,
0x4a, 0x84, 0x93, 0x5c, 0xdd, 0xf3, 0x71, 0x1e, 0x82, 0xe0, 0x52, 0xe2, 0x89, 0x0b, 0xdd, 0x0a,
0xaa, 0x4c, 0x7a, 0x01, 0x2e, 0x8a, 0x97, 0x8f, 0x20, 0xdb, 0x97, 0xb4, 0x2d, 0x54, 0xe4, 0x8d,
0x0f, 0x0e, 0xed, 0x38, 0x96, 0x41, 0xe6, 0x0d, 0xd7, 0x8d, 0x69, 0x13, 0x30, 0xf4, 0xac, 0x27,
0xe4, 0xf8, 0x11, 0x7a, 0xd6, 0x4b, 0xfe, 0xb5, 0x84, 0x56, 0xe7, 0x6f, 0x4d, 0xea, 0x3a, 0xc2,
0x9f, 0xb1, 0xb0, 0xdf, 0x4d, 0xe1, 0x6e, 0xd8, 0x1d, 0x46, 0x7d, 0xf6, 0x98, 0x5f, 0x65, 0x8a,
0x74, 0x15, 0xe8, 0xfc, 0xca, 0x68, 0x01, 0x55, 0xb5, 0x50, 0x71, 0x3f, 0x7c, 0xc8, 0xf6, 0xe5,
0x1d, 0xe5, 0xfa, 0x42, 0xb7, 0xb3, 0x7a, 0x0b, 0xa0, 0x54, 0x48, 0xd0, 0xfe, 0x51, 0x42, 0x45,
0x4e, 0x78, 0xe2, 0x26, 0xac, 0x6f, 0x6d, 0x51, 0xd2, 0xc1, 0x0a, 0x6f, 0xa9, 0x50, 0xc4, 0x22,
0x2b, 0x74, 0xb3, 0x63, 0xb4, 0x44, 0xff, 0xd2, 0xcd, 0x4e, 0xdb, 0x31, 0x71, 0x01, 0xdc, 0xa8,
0xc3, 0xaa, 0xc8, 0x19, 0x5c, 0xd7, 0x81, 0xe2, 0x05, 0xa2, 0xef, 0x53, 0xbc, 0xc4, 0x3b, 0x7e,
0xb0, 0x2b, 0x3a, 0x95, 0x1e, 0xec, 0x82, 0x13, 0x70, 0x45, 0x2d, 0xa1, 0x9c, 0x61, 0x60, 0x04,
0x10, 0x83, 0x8b, 0xaf, 0x4e, 0x26, 0x02, 0x6f, 0xe3, 0x06, 0xd4, 0x01, 0x5e, 0xe1, 0x5e, 0x84,
0x25, 0x87, 0xad, 0x8a, 0x59, 0xe1, 0xe2, 0xb3, 0xd9, 0xd0, 0xc0, 0xc0, 0x60, 0x5a, 0x9e, 0xe1,
0x04, 0xd4, 0x23, 0xf8, 0x1c, 0x4f, 0x7c, 0x67, 0x6b, 0x07, 0xab, 0xb0, 0x22, 0xbb, 0x6e, 0x0b,
0xbf, 0xc0, 0x1b, 0xac, 0x43, 0xbc, 0xfb, 0x96, 0xbf, 0x8d, 0xcf, 0x03, 0xdd, 0x02, 0x8e, 0x17,
0x61, 0xd5, 0xd6, 0xe9, 0x3d, 0xfc, 0x12, 0x48, 0x6b, 0xdf, 0x27, 0xf8, 0x82, 0x58, 0x74, 0x70,
0x8d, 0x4f, 0x20, 0xd2, 0xc4, 0xff, 0x07, 0x8a, 0xda, 0x36, 0x5e, 0x03, 0x21, 0xb6, 0x2b, 0x6d,
0xfe, 0x7f, 0xd0, 0xd0, 0xe6, 0x1a, 0xbe, 0x0c, 0x0a, 0xd8, 0x13, 0x0d, 0x5f, 0xc9, 0x46, 0xd7,
0xab, 0xbc, 0x8f, 0xf0, 0x82, 0xc5, 0xaf, 0xc1, 0x78, 0x72, 0xf1, 0x45, 0xd9, 0x9e, 0x75, 0x5f,
0xdf, 0xb5, 0x3c, 0xfc, 0xba, 0x48, 0x09, 0xea, 0x83, 0x44, 0x8d, 0x8f, 0x35, 0xee, 0x88, 0x37,
0x78, 0x5e, 0x82, 0x86, 0x6f, 0x8a, 0x95, 0xe7, 0xe1, 0x4b, 0x9c, 0xd7, 0xf1, 0x7c, 0xd0, 0xe9,
0x27, 0x32, 0x5d, 0x39, 0xf7, 0xe5, 0xc9, 0xc6, 0xde, 0xc1, 0xeb, 0xa2, 0xf2, 0x08, 0x78, 0xe6,
0x2d, 0x31, 0x3b, 0x49, 0x03, 0x5f, 0x91, 0x2b, 0x17, 0x5f, 0xe5, 0xa7, 0x50, 0xc7, 0x6e, 0xe1,
0xb7, 0xb3, 0x81, 0xfa, 0x0e, 0x58, 0xe8, 0x7a, 0xb8, 0x0e, 0x16, 0x7e, 0x12, 0xe8, 0x36, 0xd7,
0x67, 0x03, 0x38, 0xa9, 0x01, 0xcb, 0x77, 0xe1, 0x01, 0x5f, 0x52, 0xd2, 0xc2, 0xd7, 0xf8, 0x03,
0x93, 0x3a, 0x2e, 0xde, 0x04, 0x11, 0x70, 0xc0, 0x75, 0xd0, 0x81, 0x92, 0xb6, 0xad, 0xdb, 0x3e,
0xbe, 0x21, 0x2a, 0x17, 0xec, 0xb4, 0xcd, 0xa0, 0x8d, 0xdf, 0x83, 0xd3, 0xa9, 0xe3, 0xf8, 0xf8,
0x26, 0xac, 0x3c, 0x70, 0xce, 0xfb, 0x7c, 0x15, 0x34, 0x1a, 0xf8, 0x16, 0xac, 0xf8, 0x89, 0x3f,
0xe5, 0x4d, 0xc7, 0x71, 0x2d, 0x03, 0xdf, 0xe6, 0x83, 0x1d, 0x88, 0x77, 0xe6, 0x06, 0xd1, 0x5d,
0x60, 0xd9, 0xe5, 0x66, 0x7f, 0xc0, 0xdb, 0x55, 0xc0, 0x67, 0xfd, 0x87, 0x1c, 0x69, 0xf9, 0x2d,
0x82, 0x3f, 0x12, 0xf3, 0xa8, 0xe3, 0x6e, 0x03, 0xfa, 0x63, 0x99, 0x72, 0x50, 0x86, 0x58, 0xe7,
0xd9, 0x19, 0xec, 0x76, 0x3a, 0x78, 0x0b, 0x96, 0x26, 0x3f, 0xd5, 0x00, 0x96, 0x86, 0x43, 0x89,
0xd5, 0xb4, 0xb1, 0x09, 0xae, 0xb8, 0x77, 0x1f, 0x13, 0x3e, 0x61, 0x2c, 0xcf, 0xc7, 0x0d, 0x71,
0x27, 0x69, 0x1b, 0xb8, 0xc9, 0x13, 0xc0, 0x69, 0x8b, 0xbc, 0xdc, 0x86, 0x89, 0x90, 0xed, 0x78,
0xe0, 0x2d, 0xce, 0x19, 0xb4, 0x0d, 0xbc, 0x03, 0x6e, 0x31, 0x1c, 0x17, 0xdf, 0x03, 0x4f, 0x98,
0x96, 0xc7, 0x87, 0x37, 0x31, 0x71, 0x4b, 0xfb, 0x2a, 0x87, 0x56, 0xe6, 0xde, 0x8b, 0xbf, 0xdf,
0x3b, 0x20, 0x99, 0xfb, 0x82, 0x70, 0x6d, 0x91, 0x17, 0xf2, 0xd9, 0x0f, 0x09, 0x73, 0x6f, 0xe4,
0xf9, 0xe7, 0xff, 0xde, 0xf1, 0xae, 0x7c, 0xa9, 0xc6, 0x68, 0x59, 0x7e, 0xc3, 0x79, 0xda, 0x3c,
0x40, 0xa8, 0x64, 0x38, 0xed, 0x36, 0xbc, 0x57, 0x6b, 0x4d, 0x54, 0xce, 0x4c, 0x52, 0x6b, 0xd3,
0x6f, 0x4c, 0xe2, 0x15, 0x7e, 0xf2, 0x85, 0xe9, 0x75, 0xb4, 0xfc, 0x90, 0x0d, 0x86, 0x51, 0x37,
0xde, 0xdb, 0x4b, 0x98, 0x78, 0x35, 0x2b, 0xd2, 0x2a, 0xa7, 0x39, 0x9c, 0xa4, 0xfd, 0x4e, 0x41,
0x17, 0xf4, 0x28, 0xdc, 0x3f, 0xfa, 0x39, 0x9b, 0xaa, 0xc6, 0x7e, 0x76, 0xc8, 0x92, 0x54, 0x35,
0x50, 0xb9, 0x2f, 0xbf, 0x69, 0x9d, 0xd2, 0xcd, 0xd9, 0x27, 0x30, 0x3a, 0x01, 0xaa, 0x2e, 0x5a,
0x61, 0x51, 0x2f, 0xee, 0x0f, 0xa3, 0x41, 0x77, 0xc6, 0xe7, 0x57, 0x4f, 0xf4, 0xb9, 0xc0, 0x70,
0x6f, 0x2f, 0xb3, 0x99, 0x9d, 0xf6, 0x57, 0x05, 0xd5, 0x9e, 0x54, 0x39, 0x19, 0xc5, 0x30, 0xcf,
0xee, 0x23, 0x35, 0x3b, 0xba, 0x3b, 0x8d, 0x8d, 0xb2, 0x60, 0x6c, 0xce, 0x65, 0x32, 0xa6, 0x2f,
0xda, 0xb3, 0xdf, 0xe0, 0x72, 0xf3, 0xdf, 0xe0, 0x54, 0x22, 0xf2, 0x80, 0x45, 0x3d, 0x96, 0xc8,
0x2f, 0x4a, 0x97, 0x4f, 0x71, 0x16, 0xf0, 0xd3, 0x29, 0x52, 0xfb, 0x83, 0x82, 0x5e, 0x91, 0x86,
0x89, 0x94, 0xfb, 0x5f, 0x89, 0xc8, 0x17, 0xe8, 0xd5, 0xef, 0xd2, 0x5b, 0x86, 0x45, 0x47, 0x65,
0xa0, 0xa5, 0x43, 0x96, 0xd4, 0x14, 0xee, 0xa0, 0x4b, 0xa7, 0x2a, 0x3a, 0x3a, 0x81, 0x3d, 0x2b,
0x00, 0x70, 0xcd, 0x7e, 0x69, 0x56, 0x83, 0x21, 0x4b, 0x7e, 0xe4, 0x2e, 0x7b, 0x3c, 0x29, 0xbb,
0xa9, 0xc2, 0xff, 0x1d, 0x5f, 0xfd, 0x56, 0x41, 0xe7, 0xb3, 0xf2, 0x39, 0x8a, 0xd2, 0xf0, 0xf1,
0x8f, 0xdc, 0x53, 0x7f, 0x54, 0xd0, 0x8b, 0xc7, 0xf4, 0x95, 0x8e, 0x9a, 0x2b, 0x3b, 0xe5, 0x79,
0xcb, 0x4e, 0xbd, 0x8b, 0x4a, 0xfc, 0xea, 0x98, 0xd4, 0x72, 0x5c, 0xc6, 0x9b, 0x27, 0xcd, 0x12,
0x60, 0xa6, 0x12, 0x33, 0xe7, 0xea, 0xfc, 0x31, 0x57, 0xff, 0x2d, 0x8f, 0x5e, 0xd0, 0xc5, 0x2f,
0x18, 0x0c, 0xda, 0xf5, 0x0f, 0xea, 0xe9, 0xfb, 0xa8, 0xbc, 0xc7, 0xc2, 0xf4, 0x70, 0xcc, 0x12,
0xf9, 0x05, 0xf3, 0xce, 0x09, 0x42, 0x9e, 0xa2, 0x4a, 0xbd, 0x21, 0x45, 0xd0, 0x89, 0xb0, 0x27,
0x43, 0x98, 0xff, 0x9e, 0x21, 0x5c, 0xfb, 0x8b, 0x82, 0xca, 0xd9, 0x41, 0xea, 0x25, 0xb4, 0xca,
0x1e, 0xa7, 0xe3, 0xb0, 0x97, 0x76, 0x13, 0x1e, 0x4f, 0xee, 0x82, 0x32, 0x5d, 0x91, 0x54, 0x11,
0x64, 0xf5, 0x2d, 0x84, 0x33, 0xb6, 0x49, 0x35, 0xe4, 0x38, 0xe3, 0x59, 0x49, 0xcf, 0x0a, 0x47,
0xbd, 0x8b, 0xd6, 0x32, 0xd6, 0xa7, 0xf4, 0xfe, 0x3c, 0x07, 0xd5, 0x24, 0x87, 0xf9, 0x44, 0x63,
0xbf, 0x85, 0x6a, 0x73, 0x07, 0x1d, 0xcd, 0x60, 0x0b, 0x1c, 0xfb, 0xd2, 0xec, 0x81, 0xd3, 0xe6,
0xa6, 0x7d, 0x9b, 0x83, 0x4a, 0x9a, 0xf5, 0xe9, 0x8f, 0x29, 0x31, 0x67, 0xdb, 0x48, 0xfe, 0xf9,
0xda, 0xc8, 0xd3, 0x87, 0x69, 0xe1, 0x87, 0x1d, 0xa6, 0xc5, 0xf9, 0xa2, 0xb9, 0x72, 0x0b, 0x2d,
0xcf, 0xa6, 0x92, 0xb8, 0x47, 0xda, 0x04, 0x9f, 0x81, 0x55, 0xe0, 0x37, 0x6e, 0x89, 0x57, 0xab,
0xc0, 0x6f, 0x5c, 0xbb, 0x29, 0x5e, 0xad, 0x02, 0xbf, 0x71, 0x7d, 0x13, 0xe7, 0x37, 0x7f, 0xb5,
0x84, 0xce, 0xb6, 0xa4, 0x18, 0x4f, 0xfc, 0xe2, 0xa8, 0xfe, 0x5e, 0x41, 0xf8, 0xf8, 0x65, 0x41,
0xbd, 0x79, 0x62, 0xa1, 0x3c, 0xf5, 0x42, 0xb4, 0xf6, 0xfe, 0xc2, 0x38, 0x91, 0x10, 0x5a, 0xfd,
0xcb, 0x6f, 0xff, 0xfe, 0x75, 0x6e, 0x5d, 0x7b, 0x63, 0xf2, 0xd3, 0x68, 0xe6, 0x93, 0xe4, 0x76,
0x78, 0x0c, 0x74, 0x5b, 0xb9, 0xa2, 0x7e, 0xa3, 0xa0, 0xb3, 0xc7, 0xc6, 0x83, 0xfa, 0xde, 0xe9,
0x0e, 0x3f, 0x36, 0xff, 0xd6, 0x6e, 0x2e, 0x0a, 0x93, 0x2a, 0xbf, 0xc3, 0x55, 0xbe, 0xac, 0x69,
0xdf, 0xad, 0x72, 0x86, 0x01, 0x8d, 0xff, 0x7c, 0x6c, 0x02, 0x4f, 0xcb, 0x44, 0xbd, 0xbb, 0x80,
0x06, 0x4f, 0x5c, 0x79, 0xd6, 0x3e, 0x78, 0x4e, 0xb4, 0x34, 0xe3, 0x06, 0x37, 0xa3, 0xae, 0xbd,
0x75, 0x82, 0x19, 0x47, 0x73, 0xfe, 0xff, 0x8d, 0x82, 0x56, 0xe6, 0x66, 0x8e, 0x7a, 0xfd, 0x94,
0xa1, 0x9f, 0x9d, 0xa8, 0x6b, 0x37, 0x16, 0x03, 0x49, 0x95, 0xaf, 0x72, 0x95, 0x2f, 0x69, 0x17,
0x9f, 0x91, 0x2c, 0x1c, 0x01, 0x9a, 0xfe, 0x5a, 0x41, 0xcb, 0xb3, 0x3d, 0x48, 0xdd, 0x5c, 0x7c,
0x08, 0xac, 0x5d, 0x5f, 0x08, 0x23, 0xd5, 0xbc, 0xc2, 0xd5, 0x7c, 0x53, 0x7b, 0xed, 0xa9, 0x6a,
0x4e, 0x01, 0xb7, 0x95, 0x2b, 0x5b, 0x5f, 0x2a, 0xe8, 0xf5, 0x5e, 0x7c, 0xf0, 0xec, 0x63, 0xb6,
0xce, 0x1f, 0x2b, 0x5e, 0x77, 0x1c, 0xa7, 0xb1, 0xab, 0x7c, 0x4a, 0x24, 0x6c, 0x10, 0x03, 0xa4,
0x1e, 0x8f, 0x07, 0x1b, 0x03, 0x16, 0xf1, 0xdf, 0xeb, 0x37, 0xc4, 0xa3, 0x70, 0x34, 0x4c, 0xbe,
0xe3, 0x4f, 0x08, 0x77, 0x32, 0xc2, 0xc3, 0x12, 0x47, 0x5c, 0xff, 0x4f, 0x00, 0x00, 0x00, 0xff,
0xff, 0x55, 0xc3, 0xe3, 0x00, 0xb5, 0x20, 0x00, 0x00,
// 2996 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xd4, 0x5a, 0xcd, 0x6f, 0xdc, 0xd6,
0xb5, 0x37, 0xe7, 0x4b, 0xa3, 0x3b, 0x92, 0x7c, 0x4d, 0x3b, 0xf6, 0x3c, 0xbd, 0x7c, 0x38, 0x74,
0xfc, 0xac, 0xd8, 0x89, 0x14, 0x4b, 0x8e, 0xe3, 0x67, 0x3b, 0x1f, 0x14, 0x79, 0x67, 0x44, 0x99,
0x43, 0x32, 0x97, 0xe4, 0x58, 0xf6, 0x66, 0x40, 0xcf, 0x50, 0x93, 0x41, 0x24, 0x72, 0xde, 0x90,
0x32, 0xac, 0xb7, 0x09, 0x1a, 0xa0, 0xcb, 0xa0, 0x8b, 0xfc, 0x09, 0x5d, 0x14, 0x28, 0x50, 0xa4,
0x40, 0x81, 0xa2, 0x5d, 0xf4, 0x2f, 0xe8, 0xb2, 0x40, 0xff, 0x82, 0x2e, 0xbb, 0xe8, 0xa2, 0x8b,
0x76, 0x57, 0x9c, 0x7b, 0x2f, 0x67, 0x38, 0xb2, 0x62, 0x69, 0x1c, 0xa3, 0x48, 0x77, 0xf7, 0x9e,
0x39, 0xbf, 0x73, 0xcf, 0xd7, 0x3d, 0xe7, 0xf0, 0x4a, 0xe8, 0x56, 0x3f, 0x8e, 0xfb, 0x7b, 0xe1,
0x5a, 0x77, 0x2f, 0x3e, 0xe8, 0xad, 0xed, 0x05, 0x51, 0xff, 0x20, 0xe8, 0x87, 0x6b, 0x4f, 0x6f,
0x3e, 0x09, 0xd3, 0x60, 0x7d, 0x4c, 0xe8, 0x24, 0xe1, 0xe8, 0xe9, 0xa0, 0x1b, 0xae, 0x0e, 0x47,
0x71, 0x1a, 0xcb, 0x6f, 0x70, 0xd4, 0x2a, 0x43, 0xad, 0x66, 0x4c, 0xab, 0x02, 0xb5, 0xfc, 0xba,
0x10, 0x1a, 0x0c, 0x07, 0x6b, 0x41, 0x14, 0xc5, 0x69, 0x90, 0x0e, 0xe2, 0x28, 0xe1, 0xe0, 0xe5,
0x2b, 0xe2, 0xd7, 0xbd, 0x38, 0xea, 0x8f, 0x0e, 0xa2, 0x68, 0x10, 0xf5, 0xd7, 0xe2, 0x61, 0x38,
0x9a, 0x62, 0x7a, 0x4b, 0x30, 0xb1, 0xdd, 0x93, 0x83, 0xdd, 0xb5, 0x74, 0xb0, 0x1f, 0x26, 0x69,
0xb0, 0x3f, 0x14, 0x0c, 0x97, 0x04, 0xc3, 0x68, 0xd8, 0x5d, 0x4b, 0xd2, 0x20, 0x3d, 0x10, 0x48,
0xe5, 0x6f, 0x12, 0xaa, 0xea, 0x71, 0xf7, 0x60, 0x3f, 0x8c, 0x52, 0xf9, 0x33, 0x54, 0x4a, 0x0f,
0x87, 0x61, 0x5d, 0xba, 0x2c, 0xad, 0x2c, 0xad, 0xbf, 0xb7, 0xfa, 0x42, 0xbd, 0x57, 0x33, 0xd8,
0xaa, 0x77, 0x38, 0x0c, 0x29, 0x43, 0xca, 0xcb, 0x68, 0xae, 0x1b, 0x47, 0x69, 0x18, 0xa5, 0xf5,
0xc2, 0x65, 0x69, 0x65, 0x7e, 0xeb, 0x0c, 0xcd, 0x08, 0xf2, 0x0a, 0x3a, 0xdb, 0xef, 0x26, 0x1d,
0xb1, 0xed, 0x1c, 0x8c, 0x06, 0xf5, 0xa2, 0xe0, 0x59, 0xec, 0x77, 0x13, 0x8d, 0xd3, 0xfd, 0xd1,
0x40, 0x5e, 0x46, 0xd5, 0xec, 0xb4, 0x7a, 0x09, 0x58, 0xe8, 0x78, 0xaf, 0xdc, 0x46, 0x25, 0x38,
0x4f, 0xbe, 0x80, 0xb0, 0xf7, 0xc8, 0x21, 0x1d, 0xdf, 0x72, 0x1d, 0xa2, 0x19, 0x0d, 0x83, 0xe8,
0xf8, 0x8c, 0xbc, 0x84, 0x90, 0x63, 0xaa, 0x86, 0xd5, 0xf1, 0xc8, 0x8e, 0x87, 0x25, 0xb9, 0x8a,
0x4a, 0x5b, 0x5e, 0xcb, 0xc4, 0x85, 0xcd, 0x2a, 0xaa, 0x24, 0xf1, 0xc1, 0xa8, 0x1b, 0x2a, 0x3f,
0x93, 0x50, 0xd5, 0x0d, 0xe1, 0xb0, 0x6e, 0x28, 0xdf, 0x43, 0xa5, 0x34, 0x7c, 0x96, 0x32, 0x93,
0x6b, 0xeb, 0xd7, 0x4e, 0x30, 0xd9, 0x0b, 0x9f, 0xa5, 0xee, 0x30, 0x88, 0x28, 0x03, 0xc9, 0x0d,
0x34, 0x9f, 0x84, 0x11, 0xf8, 0x5a, 0xd8, 0x5b, 0x5b, 0x5f, 0x39, 0x41, 0x82, 0x9b, 0xf1, 0xd3,
0x09, 0x54, 0xf9, 0xb6, 0x84, 0x2a, 0x24, 0x4a, 0x07, 0xe9, 0xa1, 0x2c, 0xa3, 0x52, 0x14, 0xec,
0xf3, 0x10, 0xcc, 0x53, 0xb6, 0x96, 0x3f, 0x11, 0x61, 0x29, 0xb0, 0xb0, 0x5c, 0x3f, 0xe1, 0x04,
0x2e, 0x28, 0x1f, 0x14, 0x1b, 0x55, 0xf7, 0xc3, 0x34, 0xe8, 0x05, 0x69, 0x50, 0x2f, 0x5e, 0x2e,
0xae, 0xd4, 0xd6, 0x37, 0x4e, 0x27, 0xa3, 0x25, 0x50, 0x24, 0x4a, 0x47, 0x87, 0x74, 0x2c, 0x04,
0xe2, 0x93, 0x04, 0x7b, 0x03, 0x70, 0x20, 0x8b, 0x4f, 0x81, 0x8e, 0xf7, 0xf2, 0x16, 0x1c, 0x16,
0xb1, 0xe4, 0xac, 0x97, 0xd9, 0x61, 0xef, 0x9d, 0xea, 0xb0, 0x16, 0x07, 0xd1, 0x31, 0x7a, 0xda,
0xbb, 0x95, 0x97, 0xf6, 0xee, 0xf2, 0x3d, 0xb4, 0x38, 0x65, 0x88, 0x8c, 0x51, 0xf1, 0xcb, 0xf0,
0x50, 0xb8, 0x18, 0x96, 0xf2, 0x05, 0x54, 0x7e, 0x1a, 0xec, 0x1d, 0x70, 0x17, 0xcf, 0x53, 0xbe,
0xb9, 0x5b, 0xb8, 0x23, 0x29, 0x87, 0x22, 0xdd, 0x6a, 0x68, 0xce, 0xb7, 0x1e, 0x58, 0xf6, 0x43,
0x0b, 0x9f, 0x91, 0x11, 0xaa, 0x38, 0x84, 0xba, 0xb6, 0x85, 0x25, 0x79, 0x01, 0x55, 0x4d, 0x5b,
0x53, 0x3d, 0xc3, 0xb6, 0x70, 0x41, 0xc6, 0x68, 0xc1, 0xa6, 0x4d, 0xd5, 0x32, 0x1e, 0x73, 0x4a,
0x51, 0x9e, 0x47, 0x65, 0xd2, 0x26, 0x96, 0x87, 0x4b, 0xf2, 0x59, 0x54, 0x7b, 0x68, 0xd3, 0x07,
0x1d, 0xbb, 0xd1, 0x51, 0xa9, 0x87, 0xcb, 0xf2, 0x39, 0xb4, 0xa8, 0xd9, 0x96, 0xeb, 0xb7, 0x08,
0xed, 0x34, 0x6d, 0x5b, 0xc7, 0x15, 0x60, 0xb7, 0xbd, 0x2d, 0x42, 0xf1, 0x9c, 0xf2, 0xd3, 0x02,
0x2a, 0x7b, 0xf1, 0x97, 0x61, 0xf4, 0xc3, 0x92, 0xf4, 0x73, 0xb4, 0x34, 0x0c, 0x46, 0x69, 0x27,
0xde, 0xed, 0x24, 0xc3, 0x30, 0xec, 0x7e, 0x21, 0x32, 0xf5, 0xc6, 0x09, 0x62, 0x9c, 0x60, 0x94,
0xda, 0xbb, 0x2e, 0x83, 0xd0, 0x85, 0x61, 0x6e, 0x27, 0xb7, 0xd1, 0xd9, 0x5e, 0x38, 0x0c, 0xa3,
0x5e, 0x18, 0x75, 0x0f, 0x3b, 0x61, 0xaf, 0x1f, 0xb2, 0x9b, 0x5c, 0x5b, 0x7f, 0xff, 0xa4, 0x92,
0x31, 0x46, 0x91, 0x5e, 0x3f, 0xa4, 0x4b, 0xbd, 0xa9, 0x3d, 0x84, 0x61, 0x2f, 0xdc, 0xdf, 0x0f,
0xc4, 0xa5, 0xe7, 0x1b, 0xe5, 0x53, 0x34, 0x3f, 0x8e, 0xab, 0xfc, 0x3a, 0x9a, 0xdf, 0x0f, 0xfa,
0xd1, 0x20, 0x3d, 0xe8, 0xf1, 0x68, 0x15, 0xe8, 0x84, 0x00, 0x02, 0x92, 0x6e, 0x3c, 0xe2, 0xea,
0x14, 0x28, 0xdf, 0x28, 0xbf, 0x3f, 0x87, 0x16, 0xf2, 0xd6, 0xc8, 0x2a, 0x2a, 0xa6, 0x41, 0x5f,
0x94, 0xb9, 0xb5, 0x19, 0xfc, 0xb0, 0xea, 0x05, 0x7d, 0x0a, 0x58, 0x79, 0x1b, 0x55, 0x82, 0x64,
0x18, 0x76, 0x53, 0x71, 0x2b, 0xd7, 0x67, 0x91, 0xa2, 0x32, 0x24, 0x15, 0x12, 0x64, 0x1d, 0x95,
0xba, 0x41, 0xc2, 0x95, 0x5e, 0x5a, 0xff, 0x60, 0x16, 0x49, 0x5a, 0x90, 0x84, 0x94, 0xa1, 0x41,
0xca, 0x6e, 0x3c, 0xda, 0x67, 0xbe, 0x9b, 0x51, 0x4a, 0x23, 0x1e, 0xed, 0x53, 0x86, 0x06, 0xbb,
0xfa, 0x10, 0x92, 0x51, 0xbd, 0x3c, 0xbb, 0x5d, 0x4d, 0x86, 0xa4, 0x42, 0x02, 0x68, 0xb4, 0x1f,
0xc7, 0x3d, 0x76, 0x77, 0x67, 0xd4, 0xa8, 0x15, 0xc7, 0x3d, 0xca, 0xd0, 0xa0, 0x51, 0x74, 0xb0,
0xff, 0x24, 0x1c, 0xd5, 0xe7, 0x66, 0xd7, 0xc8, 0x62, 0x48, 0x2a, 0x24, 0x80, 0xac, 0x61, 0x38,
0x4a, 0xe2, 0xa8, 0x5e, 0x9d, 0x5d, 0x96, 0xc3, 0x90, 0x54, 0x48, 0x60, 0xb2, 0x46, 0xd0, 0x89,
0xeb, 0xf3, 0x2f, 0x21, 0x8b, 0x21, 0xa9, 0x90, 0x20, 0x3f, 0x42, 0xb5, 0x51, 0xd8, 0x1d, 0x0c,
0x47, 0x71, 0x77, 0x90, 0x1e, 0xd6, 0x11, 0x13, 0xf8, 0xd1, 0x2c, 0x02, 0xe9, 0x04, 0x4e, 0xf3,
0xb2, 0xe4, 0x26, 0x2a, 0xa7, 0x61, 0x94, 0x84, 0xf5, 0x1a, 0x13, 0x7a, 0x73, 0xa6, 0x6c, 0x07,
0x20, 0xe5, 0x78, 0x10, 0xf4, 0x34, 0x1e, 0x74, 0xc3, 0xfa, 0xc2, 0xec, 0x82, 0xda, 0x00, 0xa4,
0x1c, 0xaf, 0x7c, 0x23, 0xa1, 0xa2, 0x17, 0xf4, 0xa7, 0x4b, 0xea, 0x1c, 0x2a, 0xaa, 0xfa, 0x36,
0x96, 0xf8, 0xc2, 0xc1, 0x05, 0xbe, 0x68, 0xe3, 0x22, 0xf4, 0x70, 0xcd, 0xb6, 0xb6, 0x71, 0x09,
0x48, 0x3a, 0x81, 0xc2, 0x59, 0x45, 0x25, 0xcb, 0xf6, 0x2d, 0x5c, 0x01, 0x92, 0xe5, 0xb7, 0xf0,
0x1c, 0x90, 0x1c, 0x6a, 0x5b, 0xb8, 0x0a, 0x24, 0x87, 0x7a, 0x78, 0x1e, 0x6a, 0xa9, 0xe3, 0x5b,
0x9a, 0x87, 0x11, 0xfc, 0xda, 0x26, 0x74, 0x13, 0xd7, 0xe4, 0x32, 0x92, 0x76, 0xf0, 0x02, 0xfc,
0xa6, 0x36, 0x1a, 0xc6, 0x0e, 0x5e, 0x54, 0x6c, 0x54, 0xe1, 0x17, 0x52, 0x96, 0xd1, 0x92, 0x0a,
0xd3, 0x84, 0xd7, 0x99, 0x28, 0x06, 0x13, 0x05, 0xa1, 0x0d, 0xa2, 0x79, 0x46, 0x9b, 0x60, 0x09,
0x2a, 0xbc, 0xd1, 0xca, 0x51, 0x0a, 0x50, 0xd6, 0x1d, 0x6a, 0x37, 0x29, 0x71, 0x5d, 0x20, 0x14,
0x95, 0x7f, 0x48, 0xa8, 0x04, 0x17, 0x13, 0x78, 0x35, 0xd5, 0x25, 0xd3, 0xd2, 0x54, 0x4d, 0xf3,
0x5d, 0x55, 0x48, 0x5b, 0x44, 0xf3, 0xaa, 0x0e, 0x9a, 0x19, 0xaa, 0x89, 0x0b, 0xbc, 0x21, 0xb4,
0x1c, 0x93, 0xb4, 0x88, 0xc5, 0x38, 0x8a, 0xd0, 0x6b, 0x74, 0xce, 0x5d, 0x82, 0x5e, 0xd3, 0x24,
0x96, 0xc1, 0x76, 0x65, 0xa6, 0x89, 0xe5, 0x7a, 0xd4, 0x07, 0x66, 0xd5, 0xc4, 0x95, 0x49, 0x2f,
0x6a, 0x13, 0x3c, 0x07, 0x67, 0x59, 0x76, 0xcb, 0xb0, 0xf8, 0xbe, 0x0a, 0xfe, 0xb6, 0x37, 0x4d,
0xe3, 0x73, 0x9f, 0xe0, 0x79, 0x38, 0xd8, 0x51, 0xa9, 0xc7, 0x65, 0x21, 0x38, 0xd8, 0xa1, 0xc4,
0xb1, 0x5d, 0x03, 0xda, 0x96, 0x6a, 0xe2, 0x1a, 0x38, 0x83, 0x92, 0x86, 0x49, 0x76, 0x8c, 0x36,
0xe9, 0x80, 0x19, 0x78, 0x01, 0xd8, 0x28, 0x31, 0x99, 0x40, 0x4e, 0x5a, 0x84, 0x33, 0xdb, 0xd9,
0x99, 0x4b, 0xca, 0x77, 0x12, 0x2a, 0x41, 0x35, 0x01, 0xe5, 0x1a, 0x36, 0x6d, 0xe5, 0x4c, 0x5f,
0x40, 0x55, 0x55, 0x07, 0x85, 0x54, 0x53, 0x18, 0xee, 0xef, 0x18, 0xa6, 0xa1, 0xd2, 0x47, 0xb8,
0x00, 0x87, 0xe5, 0x0c, 0x7f, 0x4c, 0x28, 0x2e, 0x32, 0x11, 0x86, 0xa5, 0x9a, 0x1d, 0x62, 0xe9,
0x86, 0xd5, 0xc4, 0x25, 0xf0, 0x45, 0x93, 0x50, 0xdf, 0xd2, 0x71, 0x19, 0xd6, 0x94, 0xa8, 0xa6,
0xe1, 0x72, 0xbb, 0x0d, 0x2a, 0x76, 0x73, 0x10, 0x5a, 0x77, 0xcb, 0xa6, 0x1e, 0xae, 0x42, 0xd8,
0x4d, 0xdb, 0x6a, 0xf2, 0x5c, 0xb0, 0xa9, 0x4e, 0x28, 0x46, 0xc0, 0x2d, 0x46, 0x46, 0x0d, 0xd7,
0x14, 0x82, 0x2a, 0xbc, 0x6c, 0x81, 0x0e, 0x4d, 0x62, 0xe9, 0x84, 0x4e, 0x2b, 0xdd, 0x20, 0x2d,
0xc3, 0x32, 0x2c, 0x11, 0xad, 0x96, 0xea, 0x6a, 0xbe, 0x09, 0xdb, 0x02, 0xa8, 0x60, 0x11, 0xdf,
0x03, 0x65, 0x95, 0xaf, 0x50, 0x09, 0x6a, 0x16, 0x28, 0xdd, 0xb2, 0x6d, 0x3d, 0x27, 0xe2, 0x02,
0xc2, 0x9a, 0x6d, 0xe9, 0xc2, 0xb1, 0x1d, 0xf8, 0x15, 0x4b, 0x10, 0x1c, 0x96, 0x46, 0xaa, 0x48,
0x22, 0xd8, 0x5b, 0xba, 0x21, 0x1c, 0x59, 0x04, 0x4f, 0x1b, 0x96, 0x47, 0x28, 0xb5, 0x9b, 0x59,
0xf4, 0x6b, 0x68, 0x6e, 0xdb, 0xe7, 0x39, 0x56, 0x86, 0xa4, 0x73, 0xfd, 0xcd, 0x6d, 0x48, 0x6f,
0x20, 0x54, 0x94, 0xcf, 0x50, 0x85, 0x17, 0x3b, 0xb0, 0xc3, 0xf2, 0x5b, 0x9b, 0x47, 0xed, 0x70,
0x0d, 0xab, 0xe9, 0x9b, 0x2a, 0xc5, 0x12, 0x9b, 0x5f, 0x4c, 0x9f, 0xb2, 0x94, 0xab, 0xa2, 0x92,
0xee, 0xab, 0x26, 0x2e, 0x2a, 0x1e, 0xaa, 0xf0, 0x12, 0x07, 0x12, 0xf8, 0x7c, 0x93, 0x93, 0x30,
0x8f, 0xca, 0x0d, 0x83, 0xba, 0x1e, 0x87, 0xbb, 0x04, 0x6c, 0xc2, 0x05, 0x20, 0x7b, 0x5b, 0x06,
0xd5, 0x71, 0x11, 0x0c, 0x9d, 0x24, 0x8c, 0x98, 0x8f, 0x4a, 0xca, 0x1d, 0x54, 0xe1, 0xc5, 0x8e,
0x49, 0xa5, 0xb6, 0x33, 0xa5, 0x17, 0x68, 0xc2, 0x68, 0xdc, 0x25, 0x96, 0xed, 0x75, 0xc4, 0xbe,
0xa0, 0x6c, 0xa3, 0x5a, 0xae, 0xaa, 0xc9, 0x97, 0xd0, 0x79, 0x4a, 0x34, 0xc3, 0xa1, 0xb6, 0x66,
0x78, 0x8f, 0xa6, 0xef, 0x54, 0xf6, 0x03, 0x4b, 0x2d, 0xb0, 0xdf, 0xb6, 0x3a, 0x39, 0x5a, 0x41,
0x49, 0x50, 0x99, 0x15, 0x33, 0xf0, 0xab, 0x47, 0xac, 0xa9, 0x3b, 0xf9, 0x1a, 0x3a, 0x97, 0x0f,
0x10, 0xfb, 0x99, 0x5b, 0xd9, 0xf0, 0x3d, 0x9f, 0x12, 0xee, 0x24, 0x47, 0x75, 0x3d, 0x5c, 0x84,
0x20, 0x38, 0x94, 0xb8, 0x7c, 0xa0, 0x5b, 0x44, 0xf3, 0xe3, 0x5a, 0x80, 0xcb, 0xfc, 0xe3, 0xc3,
0xcf, 0xf6, 0x15, 0x65, 0x13, 0x95, 0x59, 0xe1, 0x83, 0x43, 0xdb, 0xb6, 0xa1, 0x91, 0x69, 0xc3,
0x55, 0x6d, 0x52, 0x04, 0x34, 0x35, 0xab, 0x09, 0x05, 0x76, 0x84, 0x9a, 0xd5, 0x92, 0xbf, 0xcf,
0xa1, 0xa5, 0xe9, 0xa9, 0x49, 0x5e, 0x41, 0xf8, 0x8b, 0x30, 0xe8, 0x75, 0x52, 0x98, 0x0d, 0x3b,
0x83, 0xa8, 0x17, 0x3e, 0x63, 0xa3, 0x4c, 0x99, 0x2e, 0x01, 0x9d, 0x8d, 0x8c, 0x06, 0x50, 0x65,
0x03, 0x95, 0xf7, 0x82, 0x27, 0xe1, 0x9e, 0x98, 0x51, 0x36, 0x66, 0x9a, 0xce, 0x56, 0x4d, 0x80,
0x52, 0x2e, 0x41, 0xf9, 0x6b, 0x05, 0x95, 0x19, 0xe1, 0xb9, 0x49, 0x58, 0xdd, 0xdc, 0xa4, 0xa4,
0x8d, 0x25, 0x56, 0x52, 0xe1, 0x12, 0xf3, 0xac, 0x50, 0xf5, 0xb6, 0x66, 0xf2, 0xfa, 0xa5, 0xea,
0xed, 0x96, 0xad, 0xe3, 0x12, 0xb8, 0x51, 0x85, 0x55, 0x99, 0x31, 0x38, 0x8e, 0x0d, 0x97, 0x17,
0x88, 0x9e, 0x47, 0xf1, 0x1c, 0xab, 0xf8, 0xfe, 0x0e, 0xaf, 0x54, 0xaa, 0xbf, 0x03, 0x4e, 0xc0,
0xf3, 0x72, 0x05, 0x15, 0x34, 0x0d, 0x23, 0x80, 0x68, 0x4c, 0x7c, 0x6d, 0xdc, 0x11, 0x58, 0x19,
0xd7, 0xe0, 0x1e, 0xe0, 0x45, 0xe6, 0x45, 0x58, 0x32, 0xd8, 0x12, 0xef, 0x15, 0x0e, 0x3e, 0x9b,
0x35, 0x0d, 0x0c, 0x0c, 0xba, 0xe1, 0x6a, 0xb6, 0x4f, 0x5d, 0x82, 0xcf, 0xb1, 0xc4, 0xb7, 0x37,
0xb7, 0xb1, 0x0c, 0x2b, 0xb2, 0xe3, 0x98, 0xf8, 0x3c, 0x2b, 0xb0, 0x36, 0x71, 0x1f, 0x1a, 0xde,
0x16, 0xbe, 0x00, 0x74, 0x03, 0x38, 0x5e, 0x83, 0x55, 0x4b, 0xa5, 0x0f, 0xf0, 0x45, 0x90, 0xd6,
0x7a, 0x48, 0xf0, 0x25, 0xbe, 0x68, 0xe3, 0x3a, 0xeb, 0x40, 0xa4, 0x89, 0xff, 0x0b, 0x14, 0xb5,
0x2c, 0xbc, 0x0c, 0x42, 0x2c, 0x47, 0xd8, 0xfc, 0xdf, 0xa0, 0xa1, 0xc5, 0x34, 0x7c, 0x1d, 0x14,
0xb0, 0xc6, 0x1a, 0xbe, 0x91, 0xb5, 0xae, 0x37, 0x59, 0x1d, 0x61, 0x17, 0x16, 0xbf, 0x05, 0xed,
0xc9, 0xc1, 0x97, 0x45, 0x79, 0x56, 0x3d, 0x75, 0xc7, 0x70, 0xf1, 0xdb, 0x3c, 0x25, 0xa8, 0x07,
0x12, 0x15, 0xd6, 0xd6, 0x98, 0x23, 0xae, 0xb0, 0xbc, 0x04, 0x0d, 0xdf, 0xe1, 0x2b, 0xd7, 0xc5,
0x57, 0x19, 0xaf, 0xed, 0x7a, 0xa0, 0xd3, 0xff, 0x88, 0x74, 0x65, 0xdc, 0xd7, 0xc6, 0x1b, 0x6b,
0x1b, 0xaf, 0xf0, 0x9b, 0x47, 0xc0, 0x33, 0xef, 0xf2, 0xde, 0x49, 0x1a, 0xf8, 0xba, 0x58, 0x39,
0xf8, 0x06, 0x3b, 0x85, 0xda, 0x96, 0x89, 0xdf, 0xcb, 0x1a, 0xea, 0xfb, 0x60, 0xa1, 0xe3, 0xe2,
0x55, 0xb0, 0xf0, 0x73, 0x5f, 0xb5, 0x98, 0x3e, 0x6b, 0xc0, 0x49, 0x35, 0x58, 0x7e, 0x00, 0x3f,
0xb0, 0x25, 0x25, 0x26, 0xbe, 0xc9, 0x7e, 0xd0, 0xa9, 0xed, 0xe0, 0x75, 0x10, 0x01, 0x07, 0x6c,
0x80, 0x0e, 0x94, 0xb4, 0x2c, 0xd5, 0xf2, 0xf0, 0x2d, 0x7e, 0x73, 0xc1, 0x4e, 0x4b, 0xf7, 0x5b,
0xf8, 0x43, 0x38, 0x9d, 0xda, 0xb6, 0x87, 0x6f, 0xc3, 0xca, 0x05, 0xe7, 0x7c, 0xc4, 0x56, 0x7e,
0xa3, 0x81, 0xef, 0xc0, 0x8a, 0x9d, 0xf8, 0xbf, 0xac, 0xe8, 0xd8, 0x8e, 0xa1, 0xe1, 0xbb, 0xac,
0xb1, 0x03, 0xf1, 0xde, 0x54, 0x23, 0xba, 0x0f, 0x2c, 0x3b, 0xcc, 0xec, 0x8f, 0x59, 0xb9, 0xf2,
0x59, 0xaf, 0xff, 0x84, 0x21, 0x0d, 0xcf, 0x24, 0xf8, 0x53, 0xde, 0x8f, 0xda, 0xce, 0x16, 0xa0,
0x3f, 0x13, 0x29, 0x07, 0xd7, 0x10, 0xab, 0x2c, 0x3b, 0xfd, 0x9d, 0x76, 0x1b, 0x6f, 0xc2, 0x52,
0x67, 0xa7, 0x6a, 0xc0, 0xd2, 0xb0, 0x29, 0x31, 0x9a, 0x16, 0xd6, 0xc1, 0x15, 0x0f, 0x1e, 0x62,
0xc2, 0x3a, 0x8c, 0xe1, 0x7a, 0xb8, 0xc1, 0x67, 0x92, 0x96, 0x86, 0x9b, 0x2c, 0x01, 0xec, 0x16,
0xcf, 0xcb, 0x2d, 0xe8, 0x08, 0xd9, 0x8e, 0x05, 0xde, 0x60, 0x9c, 0x7e, 0x4b, 0xc3, 0xdb, 0xe0,
0x16, 0xcd, 0x76, 0xf0, 0x03, 0xf0, 0x84, 0x6e, 0xb8, 0xac, 0x79, 0x13, 0x1d, 0x9b, 0xca, 0x37,
0x05, 0xb4, 0x38, 0xf5, 0x5d, 0xfc, 0xc3, 0xbe, 0x01, 0xc9, 0xd4, 0x0b, 0xc2, 0xcd, 0x59, 0x3e,
0xc8, 0xf3, 0x0f, 0x09, 0x53, 0x5f, 0xe4, 0xc5, 0x97, 0x7f, 0xef, 0xf8, 0x40, 0x7c, 0x54, 0x63,
0xb4, 0x20, 0xde, 0x70, 0x8e, 0xeb, 0x07, 0x08, 0x55, 0x34, 0xbb, 0xd5, 0x82, 0xef, 0x6a, 0xa5,
0x89, 0xaa, 0x99, 0x49, 0x72, 0x7d, 0xf2, 0xc6, 0xc4, 0x3f, 0xe1, 0xc7, 0x2f, 0x4c, 0x6f, 0xa3,
0x85, 0x27, 0x61, 0x7f, 0x10, 0x75, 0xe2, 0xdd, 0xdd, 0x24, 0xe4, 0x9f, 0x66, 0x65, 0x5a, 0x63,
0x34, 0x9b, 0x91, 0x14, 0x13, 0x5d, 0xd4, 0xf6, 0x82, 0x24, 0x19, 0xec, 0x0e, 0xba, 0xec, 0x09,
0x4d, 0x0b, 0xd2, 0xb0, 0x1f, 0x8f, 0x8e, 0x7f, 0x79, 0x79, 0x13, 0xa1, 0x6e, 0x1c, 0xed, 0x0e,
0x7a, 0xec, 0xa9, 0x83, 0x7f, 0x6e, 0xe6, 0x28, 0xca, 0xaf, 0x25, 0x74, 0x49, 0x8d, 0x82, 0xbd,
0xc3, 0xff, 0x0f, 0x27, 0x86, 0x86, 0xff, 0x77, 0x10, 0x26, 0xa9, 0xac, 0xa1, 0x6a, 0x4f, 0xbc,
0x90, 0x9d, 0x32, 0x68, 0xd9, 0x83, 0x1a, 0x1d, 0x03, 0x65, 0x07, 0x2d, 0x86, 0x51, 0x37, 0xee,
0x0d, 0xa2, 0x7e, 0x27, 0x17, 0xc1, 0x1b, 0x27, 0x46, 0x90, 0x63, 0x58, 0xec, 0x16, 0xc2, 0xdc,
0x4e, 0xf9, 0xb3, 0x84, 0xea, 0xcf, 0xab, 0x9c, 0x0c, 0x63, 0xe8, 0x8e, 0x0f, 0x91, 0x9c, 0x1d,
0xdd, 0x99, 0x44, 0x5a, 0x9a, 0x31, 0xd2, 0xe7, 0x32, 0x19, 0x93, 0xcf, 0xf6, 0xfc, 0x8b, 0x5e,
0x61, 0xfa, 0x45, 0x4f, 0x26, 0x3c, 0xab, 0xc0, 0xa1, 0x89, 0x78, 0x9f, 0xba, 0x76, 0x8a, 0xb3,
0x80, 0x9f, 0x4e, 0x90, 0xca, 0x6f, 0x25, 0xf4, 0x86, 0x30, 0x8c, 0x27, 0xf0, 0x7f, 0x4a, 0x44,
0xbe, 0x42, 0x6f, 0x7e, 0x9f, 0xde, 0x22, 0x2c, 0x2a, 0xaa, 0x02, 0x2d, 0x1d, 0x84, 0x49, 0x5d,
0x62, 0x0e, 0xba, 0x7a, 0xaa, 0x2b, 0x4c, 0xc7, 0xb0, 0x17, 0x05, 0x00, 0x86, 0xf6, 0x8b, 0x79,
0x0d, 0x06, 0x61, 0xf2, 0x23, 0x77, 0xd9, 0xb3, 0xf1, 0xb5, 0x9b, 0x28, 0xfc, 0xef, 0xf1, 0xd5,
0xaf, 0x24, 0x74, 0x21, 0xbb, 0x3e, 0x87, 0x51, 0x1a, 0x3c, 0xfb, 0x91, 0x7b, 0xea, 0x77, 0x12,
0x7a, 0xed, 0x88, 0xbe, 0xc2, 0x51, 0x53, 0xd7, 0x4e, 0x7a, 0xd9, 0x6b, 0x27, 0xdf, 0x47, 0x15,
0x36, 0x88, 0x26, 0xf5, 0x02, 0x93, 0xf1, 0xce, 0x49, 0x9d, 0x09, 0x98, 0xa9, 0xc0, 0x4c, 0xb9,
0xba, 0x78, 0xc4, 0xd5, 0x8f, 0xd1, 0x79, 0x51, 0xaa, 0x0f, 0xa1, 0xf6, 0xbf, 0x4a, 0x47, 0x2b,
0xfb, 0xe8, 0xc2, 0xb4, 0x6c, 0xe1, 0x14, 0x1f, 0xa1, 0x2e, 0x6f, 0x08, 0x93, 0xfc, 0xf9, 0xf0,
0x04, 0xf1, 0xc7, 0xf7, 0x13, 0x9a, 0x13, 0xa4, 0xfc, 0xa4, 0x84, 0xce, 0xab, 0xfc, 0x4f, 0x3b,
0xe1, 0xab, 0xb6, 0x45, 0x7e, 0x88, 0xaa, 0xbb, 0x61, 0x90, 0x1e, 0x8c, 0xc2, 0x44, 0x3c, 0xed,
0xde, 0x3b, 0x41, 0xc8, 0x31, 0xaa, 0xac, 0x36, 0x84, 0x08, 0x3a, 0x16, 0xf6, 0x7c, 0x36, 0x16,
0x7f, 0x60, 0x36, 0x2e, 0xff, 0x53, 0x42, 0xd5, 0xec, 0x20, 0xf9, 0x2a, 0x5a, 0x0a, 0x9f, 0xa5,
0xa3, 0xa0, 0x9b, 0x76, 0x12, 0x96, 0x9a, 0xcc, 0x05, 0x55, 0xba, 0x28, 0xa8, 0x3c, 0x5f, 0xe5,
0x77, 0x11, 0xce, 0xd8, 0xc6, 0x17, 0xbb, 0xc0, 0x18, 0xcf, 0x0a, 0x7a, 0x56, 0x03, 0xe4, 0xfb,
0x68, 0x39, 0x63, 0x3d, 0xa6, 0x8d, 0x15, 0x19, 0xa8, 0x2e, 0x38, 0xf4, 0xe7, 0x7a, 0xd4, 0x1d,
0x54, 0x9f, 0x3a, 0xe8, 0x30, 0x87, 0x2d, 0x31, 0xec, 0xc5, 0xfc, 0x81, 0x93, 0x3a, 0x2d, 0x5f,
0x41, 0x8b, 0x5d, 0x91, 0x4d, 0x1d, 0x36, 0xa4, 0x55, 0x18, 0xfb, 0x42, 0x37, 0x97, 0x62, 0xca,
0x2f, 0x8b, 0x50, 0x39, 0xf2, 0x8e, 0xff, 0x31, 0x5d, 0xc4, 0x7c, 0xd9, 0x2c, 0xbe, 0x5c, 0xd9,
0x3c, 0x7e, 0x78, 0x28, 0xbd, 0xda, 0xe1, 0xa1, 0x7c, 0x64, 0x78, 0x98, 0xbe, 0xb0, 0x95, 0x57,
0x74, 0x61, 0xaf, 0xdf, 0x41, 0x0b, 0xf9, 0x34, 0xe6, 0xc3, 0xbd, 0x45, 0xf0, 0x19, 0x58, 0xf9,
0x5e, 0xe3, 0x0e, 0xff, 0xde, 0xf5, 0xbd, 0xc6, 0xcd, 0xdb, 0xfc, 0x7b, 0xd7, 0xf7, 0x1a, 0x1b,
0xeb, 0xb8, 0xb8, 0xfe, 0x87, 0x2a, 0x3a, 0x6b, 0x8a, 0x13, 0x5d, 0xfe, 0x67, 0x60, 0xf9, 0x37,
0x12, 0xc2, 0x47, 0x67, 0x2e, 0xf9, 0xf6, 0x89, 0x97, 0xf4, 0xd8, 0xb9, 0x72, 0xf9, 0xa3, 0x99,
0x71, 0x3c, 0xcf, 0x94, 0xd5, 0xaf, 0xff, 0xf4, 0x97, 0x6f, 0x0b, 0x2b, 0xca, 0x95, 0xf1, 0xdf,
0xab, 0x33, 0x57, 0x27, 0x77, 0x83, 0x23, 0xa0, 0xbb, 0xd2, 0x75, 0xf9, 0x3b, 0x09, 0x9d, 0x3d,
0xd2, 0x65, 0xe5, 0x0f, 0x4f, 0x77, 0xf8, 0x91, 0x31, 0x62, 0xf9, 0xf6, 0xac, 0x30, 0xa1, 0xf2,
0xfb, 0x4c, 0xe5, 0x6b, 0x8a, 0xf2, 0xfd, 0x2a, 0x67, 0x18, 0xd0, 0xf8, 0x8f, 0x47, 0x06, 0x99,
0xdc, 0x15, 0xbd, 0x3f, 0x83, 0x06, 0xcf, 0x4d, 0x8e, 0xcb, 0x1f, 0xbf, 0x24, 0x5a, 0x98, 0x71,
0x8b, 0x99, 0xb1, 0xaa, 0xbc, 0x7b, 0x82, 0x19, 0x87, 0x53, 0xfe, 0xff, 0x85, 0x84, 0x16, 0xa7,
0x5a, 0xb7, 0xbc, 0x71, 0xca, 0xd0, 0xe7, 0x07, 0x93, 0xe5, 0x5b, 0xb3, 0x81, 0x84, 0xca, 0x37,
0x98, 0xca, 0x57, 0x95, 0xcb, 0x2f, 0x48, 0x16, 0x86, 0x00, 0x4d, 0x7f, 0x2e, 0xa1, 0x85, 0x7c,
0x3b, 0x95, 0xd7, 0x4f, 0x77, 0x03, 0xf3, 0x7d, 0x7d, 0x79, 0x63, 0x26, 0x8c, 0x50, 0xf3, 0x3a,
0x53, 0xf3, 0x1d, 0xe5, 0xad, 0x63, 0xd4, 0xcc, 0x57, 0xdf, 0x4c, 0xcb, 0x7c, 0x01, 0x3e, 0x51,
0xcb, 0x63, 0xda, 0xe4, 0xf2, 0xc6, 0x4c, 0x98, 0x53, 0x68, 0x19, 0xe4, 0x00, 0x77, 0xa5, 0xeb,
0x9b, 0x5f, 0x4b, 0xe8, 0xed, 0x6e, 0xbc, 0xff, 0xe2, 0x63, 0x36, 0x2f, 0x1c, 0x29, 0x31, 0xce,
0x28, 0x4e, 0x63, 0x47, 0x7a, 0x4c, 0x04, 0xac, 0x1f, 0x03, 0x64, 0x35, 0x1e, 0xf5, 0xd7, 0xfa,
0x61, 0xc4, 0xfe, 0xd5, 0x63, 0x8d, 0xff, 0x14, 0x0c, 0x07, 0xc9, 0xf7, 0xfc, 0xff, 0xca, 0xbd,
0x8c, 0xf0, 0xa4, 0xc2, 0x10, 0x1b, 0xff, 0x0a, 0x00, 0x00, 0xff, 0xff, 0x72, 0x26, 0xce, 0x5b,
0xf0, 0x22, 0x00, 0x00,
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More