Basic oplog capability (#6)

The ability to write oplog entries using a ticket based system to ensure serialization.   Uses a go-kms-wrapper for encrypting/decrypting entries.

Co-authored-by: Jim Lambert <jlm.lambert@gmail.com>
Co-authored-by: Michael Gaffney <mgaffney@users.noreply.github.com>
Co-authored-by: Todd Knight <T.Alan.Knight@gmail.com>
Co-authored-by: Jeff Mitchell <jeffrey.mitchell@gmail.com>
This commit is contained in:
authored and GitHub committed 2020-04-08 14:55:40 -04:00
1 parent 6b0756ade9
commit 34f530e0df
30 files changed
+3963

No files matched your search

+16
View File
@@ -0,0 +1,16 @@
# Determine this makefile's path.
# Be sure to place this BEFORE `include` directives, if any.
THIS_FILE := $(lastword $(MAKEFILE_LIST))
### oplog requires protoc-gen-go v1.20.0 or later
# GO111MODULE=on go get -u google.golang.org/protobuf/cmd/protoc-gen-go@v1.20.1
proto:
protoc --go_out=paths=source_relative:. ./internal/oplog/any_operation.proto
protoc --go_out=paths=source_relative:. ./internal/oplog/store/oplog.proto
protoc-go-inject-tag -input=./internal/oplog/store/oplog.pb.go
protoc --go_out=paths=source_relative:. ./internal/oplog/oplog_test/oplog_test.proto
protoc-go-inject-tag -input=./internal/oplog/oplog_test/oplog_test.pb.go
.PHONY: proto
+5
View File
@@ -6,15 +6,19 @@ require (
github.com/armon/go-metrics v0.3.3
github.com/fatih/color v1.9.0
github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32
github.com/golang-migrate/migrate/v4 v4.9.1
github.com/golang/protobuf v1.4.0-rc.4
github.com/hashicorp/errwrap v1.0.0
github.com/hashicorp/go-alpnmux v0.0.0-20200323180452-dee08f00df54
github.com/hashicorp/go-hclog v0.12.1
github.com/hashicorp/go-kms-wrapping v0.5.7
github.com/hashicorp/go-multierror v1.0.0
github.com/hashicorp/go-uuid v1.0.2
github.com/hashicorp/hcl v1.0.0
github.com/hashicorp/vault v1.2.1-0.20200310200732-a321b1217d5a
github.com/hashicorp/vault/api v1.0.5-0.20200310200732-a321b1217d5a
github.com/hashicorp/vault/sdk v0.1.14-0.20200310200732-a321b1217d5a
github.com/jinzhu/gorm v1.9.12
github.com/kr/text v0.2.0
github.com/lib/pq v1.3.0
github.com/mattn/go-colorable v0.1.6
@@ -29,4 +33,5 @@ require (
golang.org/x/net v0.0.0-20200301022130-244492dfa37a
google.golang.org/grpc v1.27.1
google.golang.org/protobuf v1.20.1
gotest.tools v2.2.0+incompatible
)
+174
View File
@@ -0,0 +1,174 @@
# oplog
oplog is a package for writing operation log (oplog) entries for the purpose of replication and verification of the data stored in the Watchtower RDBMS.
- [oplog](#oplog)
- [Usage](#usage)
- [TBD/TODO](#tbdtodo)
- [oplog entry](#oplog-entry)
- [oplog tables](#oplog-tables)
- [oplog optimistic locking using tickets](#oplog-optimistic-locking-using-tickets)
## Usage
```go
// you must init the ticket in its own transaction. You only need
// to init a ticket once in the database. It doesn't need to happen for
// every connection. Once it's persistent in the database, you can simply Get it.
initTx := db.Begin()
ticketer := &GormTicketer{Tx: initTx}
err = ticketer.InitTicket("users")
// if there's no error, then commit the initialized ticket
initTx.Commit()
userCreate := oplog_test.TestUser{
TestUser: oplog_test.TestUser{
Name: userName,
},
}
tx := db.Begin()
// write the user to the database
tx.Create(&userCreate)
ticketer = &GormTicketer{Tx: db}
// get a ticket for writing users to the oplog
ticket, err := ticketer.GetTicket("users")
// create an entry for the oplog with the entry's metadata (likely the entry's Scope)
newLogEntry := NewEntry(
"test-users",
[]Metadata{
Metadata{
Key: "deployment",
Value: "amex",
},
Metadata{
Key: "project",
Value: "central-info-systems",
},
},
cipherer, // wrapping.Wrapper
ticketer,
)
// write an entry with N messages (variadic parameter) in the order they were sent to the database
_, err = newLogEntry.WriteEntryWith(
context.Background(),
&GormWriter{tx},
ticket,
&Message{Message: &userCreate, TypeName: "user", OpType: OpType_CREATE_OP},
)
// if there's an error writing the oplog then roll EVERYTHING back
if err != nil {
tx.Rollback()
}
// no err means you can commit all the things.
tx.Commit()
```
## TBD/TODO
We need to discuss and decide how Watchtower is going to handle the following oplog things:
* SQL migrations: you'll find the package's [SQL migrations](https://github.com/golang-migrate/migrate) under: ./migrations/postgres We need to decide how Watchtower will manage migrations across the system and we will likely need to reference this package's migrations somehow.
## oplog entry
```
Example Oplog Entry for the Target Aggregate
┌────────────────────────────────────────────────┐
│ FIFO with []byte buffer │
│ │
│┌─Msg 4────┐┌─Msg 3────┐┌─Msg 2────┐ │
││ ││ ││ │ │
││ Tags ││ Host ││ HostSet │ │
││┌────────┐││┌────────┐││┌────────┐│ │
│││ ││││ ││││ ││ │
│││ ││││ ││││ ││ │
│││ Tag ││││ Host ││││HostSet ││ │
│││protobuf││││protobuf││││protobuf││ │
│││ ││││ ││││ ││ │
│││ ││││ ││││ ││ │
││└────────┘││└────────┘││└────────┘│ │
││┌────────┐││┌────────┐││┌────────┐│ │
│││ ││││ ││││ ││ │
│││typeName││││typeName││││typeName││ │
│││ Tag ││││ Host ││││HostSet ││ │
││└────────┘││└────────┘││└────────┘│ │
││┌────────┐││┌────────┐││┌────────┐│ │
│││ ││││ ││││ ││ │
│││ OpType ││││ OpType ││││ OpType ││ │
│││ Create ││││ Create ││││ Create ││ │
││└────────┘││└────────┘││└────────┘│ │
│└──────────┘└──────────┘└──────────┘ │
└────────────────────────────────────────────────┘
```
## oplog tables
```
oplog tables:
as the diagram shows, we can split the
oplog_entries into multiple tables if
needed for performance.
┌────────────────┐
│┌───────────────┴┐
││┌───────────────┴┐ ┌────────────────┐
│││ oplog_entries │ │ oplog_ticket │
││├────────────────┤ ├────────────────┤
│││id │╲ │id │
│││aggregate_name │──┼───────┼ │aggregate_name │
└┤│data │╱ │version │
└┤ │ │ │
└────────────────┘ └────────────────┘
┼
│
│
┼
╱│╲
┌────────────────┐
│ oplog_metadata │
├────────────────┤
│id │
│entry_id │
│key │
│value │
└────────────────┘
```
## oplog optimistic locking using tickets
```
Alice's Database
transaction Bob's transaction
│ │ │
│─────────BEGIN──────────────────▶│ │
│ │◀───────────────BEGIN─────────────────┤
│ ┌───────────┴───────────┐ │
│ │ ticket version:1 │ │
│ └───────────┬───────────┘ │
│ Select oplog-ticket for │ │
├──────────────"Target"──────────▶│ Select oplog-ticket for │
│ │◀──────────────"Target"───────────────┤
│ │ │
│ Write to Tables in │ │
├───────Target Aggregate─────────▶│ │
│ │ Write to Tables in │
│ │◀─────────Target Aggregate────────────┤
│ │ │
│ │ │
│ Update Ticket │ │
├────Version = 2 where───────────▶│ │
│ Version = 1 ┌─────────┴─────────┐ │
│ │ ticket version: 2 │ │
│ └─────────┬─────────┘ │
│ │ Update Ticket │
│ │◀───────────Version = 2 where─────────┤
│ │ Version = 1 ┌────┴──────────┐
│ │ │ update failed │
├────────────Commit──────────────▶│ └────┬──────────┘
│ │ │
│ │ │
│ │ │
│ │◀────────────Rollback─────────────────┤
│ │ │
│ │ │
```
+263
View File
@@ -0,0 +1,263 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.20.1
// protoc v3.11.4
// source: internal/oplog/any_operation.proto
package oplog
import (
proto "github.com/golang/protobuf/proto"
field_mask "google.golang.org/genproto/protobuf/field_mask"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
// This is a compile-time assertion that a sufficiently up-to-date version
// of the legacy proto package is being used.
const _ = proto.ProtoPackageIsVersion4
// OpType provides the type of operation the Any message represents (create,
// update, delete)
type OpType int32
const (
// UNKNOWN_OP is an unknown operation
OpType_UNKNOWN_OP OpType = 0
// CREATE_OP is a create operation
OpType_CREATE_OP OpType = 1
// UPDATE_OP is an update operation
OpType_UPDATE_OP OpType = 2
// DELETE_OP is a delete operation
OpType_DELETE_OP OpType = 3
)
// Enum value maps for OpType.
var (
OpType_name = map[int32]string{
0: "UNKNOWN_OP",
1: "CREATE_OP",
2: "UPDATE_OP",
3: "DELETE_OP",
}
OpType_value = map[string]int32{
"UNKNOWN_OP": 0,
"CREATE_OP": 1,
"UPDATE_OP": 2,
"DELETE_OP": 3,
}
)
func (x OpType) Enum() *OpType {
p := new(OpType)
*p = x
return p
}
func (x OpType) String() string {
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
}
func (OpType) Descriptor() protoreflect.EnumDescriptor {
return file_internal_oplog_any_operation_proto_enumTypes[0].Descriptor()
}
func (OpType) Type() protoreflect.EnumType {
return &file_internal_oplog_any_operation_proto_enumTypes[0]
}
func (x OpType) Number() protoreflect.EnumNumber {
return protoreflect.EnumNumber(x)
}
// Deprecated: Use OpType.Descriptor instead.
func (OpType) EnumDescriptor() ([]byte, []int) {
return file_internal_oplog_any_operation_proto_rawDescGZIP(), []int{0}
}
// AnyOperation provides a message for anything and the type of operation it
// represents.
type AnyOperation struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
TypeName string `protobuf:"bytes,1,opt,name=type_name,json=typeName,proto3" json:"type_name,omitempty"`
Value []byte `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"`
OperationType OpType `protobuf:"varint,3,opt,name=operation_type,json=operationType,proto3,enum=hashicorp.watchtower.controller.oplog.v1.OpType" json:"operation_type,omitempty"`
// mask is the mask of fields to update.
FieldMask *field_mask.FieldMask `protobuf:"bytes,4,opt,name=field_mask,json=fieldMask,proto3" json:"field_mask,omitempty"`
}
func (x *AnyOperation) Reset() {
*x = AnyOperation{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_oplog_any_operation_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *AnyOperation) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*AnyOperation) ProtoMessage() {}
func (x *AnyOperation) ProtoReflect() protoreflect.Message {
mi := &file_internal_oplog_any_operation_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use AnyOperation.ProtoReflect.Descriptor instead.
func (*AnyOperation) Descriptor() ([]byte, []int) {
return file_internal_oplog_any_operation_proto_rawDescGZIP(), []int{0}
}
func (x *AnyOperation) GetTypeName() string {
if x != nil {
return x.TypeName
}
return ""
}
func (x *AnyOperation) GetValue() []byte {
if x != nil {
return x.Value
}
return nil
}
func (x *AnyOperation) GetOperationType() OpType {
if x != nil {
return x.OperationType
}
return OpType_UNKNOWN_OP
}
func (x *AnyOperation) GetFieldMask() *field_mask.FieldMask {
if x != nil {
return x.FieldMask
}
return nil
}
var File_internal_oplog_any_operation_proto protoreflect.FileDescriptor
var file_internal_oplog_any_operation_proto_rawDesc = []byte{
0x0a, 0x22, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x6f, 0x70, 0x6c, 0x6f, 0x67,
0x2f, 0x61, 0x6e, 0x79, 0x5f, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x12, 0x28, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e,
0x77, 0x61, 0x74, 0x63, 0x68, 0x74, 0x6f, 0x77, 0x65, 0x72, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72,
0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x6f, 0x70, 0x6c, 0x6f, 0x67, 0x2e, 0x76, 0x31, 0x1a, 0x20,
0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f,
0x66, 0x69, 0x65, 0x6c, 0x64, 0x5f, 0x6d, 0x61, 0x73, 0x6b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
0x22, 0xd5, 0x01, 0x0a, 0x0c, 0x41, 0x6e, 0x79, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f,
0x6e, 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x79, 0x70, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01,
0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x79, 0x70, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x14,
0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76,
0x61, 0x6c, 0x75, 0x65, 0x12, 0x57, 0x0a, 0x0e, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f,
0x6e, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x30, 0x2e, 0x68,
0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x77, 0x61, 0x74, 0x63, 0x68, 0x74, 0x6f,
0x77, 0x65, 0x72, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x6f,
0x70, 0x6c, 0x6f, 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x70, 0x54, 0x79, 0x70, 0x65, 0x52, 0x0d,
0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x12, 0x39, 0x0a,
0x0a, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x5f, 0x6d, 0x61, 0x73, 0x6b, 0x18, 0x04, 0x20, 0x01, 0x28,
0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
0x62, 0x75, 0x66, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x4d, 0x61, 0x73, 0x6b, 0x52, 0x09, 0x66,
0x69, 0x65, 0x6c, 0x64, 0x4d, 0x61, 0x73, 0x6b, 0x2a, 0x45, 0x0a, 0x06, 0x4f, 0x70, 0x54, 0x79,
0x70, 0x65, 0x12, 0x0e, 0x0a, 0x0a, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x5f, 0x4f, 0x50,
0x10, 0x00, 0x12, 0x0d, 0x0a, 0x09, 0x43, 0x52, 0x45, 0x41, 0x54, 0x45, 0x5f, 0x4f, 0x50, 0x10,
0x01, 0x12, 0x0d, 0x0a, 0x09, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x5f, 0x4f, 0x50, 0x10, 0x02,
0x12, 0x0d, 0x0a, 0x09, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x5f, 0x4f, 0x50, 0x10, 0x03, 0x42,
0x3c, 0x5a, 0x3a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61,
0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x77, 0x61, 0x74, 0x63, 0x68, 0x74, 0x6f, 0x77,
0x65, 0x72, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x6f, 0x70, 0x6c, 0x6f,
0x67, 0x2f, 0x6f, 0x70, 0x6c, 0x6f, 0x67, 0x3b, 0x6f, 0x70, 0x6c, 0x6f, 0x67, 0x62, 0x06, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
file_internal_oplog_any_operation_proto_rawDescOnce sync.Once
file_internal_oplog_any_operation_proto_rawDescData = file_internal_oplog_any_operation_proto_rawDesc
)
func file_internal_oplog_any_operation_proto_rawDescGZIP() []byte {
file_internal_oplog_any_operation_proto_rawDescOnce.Do(func() {
file_internal_oplog_any_operation_proto_rawDescData = protoimpl.X.CompressGZIP(file_internal_oplog_any_operation_proto_rawDescData)
})
return file_internal_oplog_any_operation_proto_rawDescData
}
var file_internal_oplog_any_operation_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
var file_internal_oplog_any_operation_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
var file_internal_oplog_any_operation_proto_goTypes = []interface{}{
(OpType)(0), // 0: hashicorp.watchtower.controller.oplog.v1.OpType
(*AnyOperation)(nil), // 1: hashicorp.watchtower.controller.oplog.v1.AnyOperation
(*field_mask.FieldMask)(nil), // 2: google.protobuf.FieldMask
}
var file_internal_oplog_any_operation_proto_depIdxs = []int32{
0, // 0: hashicorp.watchtower.controller.oplog.v1.AnyOperation.operation_type:type_name -> hashicorp.watchtower.controller.oplog.v1.OpType
2, // 1: hashicorp.watchtower.controller.oplog.v1.AnyOperation.field_mask:type_name -> google.protobuf.FieldMask
2, // [2:2] is the sub-list for method output_type
2, // [2:2] is the sub-list for method input_type
2, // [2:2] is the sub-list for extension type_name
2, // [2:2] is the sub-list for extension extendee
0, // [0:2] is the sub-list for field type_name
}
func init() { file_internal_oplog_any_operation_proto_init() }
func file_internal_oplog_any_operation_proto_init() {
if File_internal_oplog_any_operation_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_internal_oplog_any_operation_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*AnyOperation); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_internal_oplog_any_operation_proto_rawDesc,
NumEnums: 1,
NumMessages: 1,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_internal_oplog_any_operation_proto_goTypes,
DependencyIndexes: file_internal_oplog_any_operation_proto_depIdxs,
EnumInfos: file_internal_oplog_any_operation_proto_enumTypes,
MessageInfos: file_internal_oplog_any_operation_proto_msgTypes,
}.Build()
File_internal_oplog_any_operation_proto = out.File
file_internal_oplog_any_operation_proto_rawDesc = nil
file_internal_oplog_any_operation_proto_goTypes = nil
file_internal_oplog_any_operation_proto_depIdxs = nil
}
+29
View File
@@ -0,0 +1,29 @@
syntax = "proto3";
import "google/protobuf/field_mask.proto";
package hashicorp.watchtower.controller.oplog.v1;
option go_package = "github.com/hashicorp/watchtower/internal/oplog/oplog;oplog";
// OpType provides the type of operation the Any message represents (create,
// update, delete)
enum OpType {
// UNKNOWN_OP is an unknown operation
UNKNOWN_OP = 0;
// CREATE_OP is a create operation
CREATE_OP = 1;
// UPDATE_OP is an update operation
UPDATE_OP = 2;
// DELETE_OP is a delete operation
DELETE_OP = 3;
}
// AnyOperation provides a message for anything and the type of operation it
// represents.
message AnyOperation {
string type_name = 1;
bytes value = 2;
OpType operation_type = 3;
// mask is the mask of fields to update.
google.protobuf.FieldMask field_mask = 4;
}
+3
View File
@@ -0,0 +1,3 @@
// oplog is a package for writing operational log (oplog) entries for the purpose of replication and verification of the data stored in the Watchtower RDBMS.
// please see README.md in this directory for more information.
package oplog
@@ -0,0 +1,5 @@
drop table if exists oplog_entry;
drop table if exists oplog_metadata;
drop table if exists oplog_ticket;
drop index if exists idx_oplog_metatadata_key;
drop index if exists idx_oplog_metatadata_value
@@ -0,0 +1,24 @@
CREATE TABLE if not exists oplog_entry (
id bigint generated always as identity primary key,
create_time timestamp with time zone default current_timestamp,
update_time timestamp with time zone default current_timestamp,
version text NOT NULL,
aggregate_name text NOT NULL,
"data" bytea NOT NULL
);
CREATE TABLE if not exists oplog_ticket (
id bigint generated always as identity primary key,
create_time timestamp with time zone default current_timestamp,
update_time timestamp with time zone default current_timestamp,
"name" text NOT NULL UNIQUE,
"version" bigint NOT NULL
);
CREATE TABLE if not exists oplog_metadata (
id bigint generated always as identity primary key,
create_time timestamp with time zone default current_timestamp,
entry_id bigint NOT NULL REFERENCES oplog_entry(id) ON DELETE CASCADE ON UPDATE CASCADE,
"key" text NOT NULL,
value text NULL
);
create index if not exists idx_oplog_metatadata_key on oplog_metadata(key);
create index if not exists idx_oplog_metatadata_value on oplog_metadata(value);
+237
View File
@@ -0,0 +1,237 @@
package oplog
import (
"bytes"
"context"
"encoding/base64"
"errors"
"fmt"
"io"
protoV1 "github.com/golang/protobuf/proto"
wrapping "github.com/hashicorp/go-kms-wrapping"
"github.com/hashicorp/watchtower/internal/oplog/store"
_ "github.com/lib/pq"
"google.golang.org/protobuf/proto"
)
// Version of oplog entries (among other things, it's used to manage upgrade compatibility when replicating)
const Version = "v1"
// Message wraps a proto.Message and adds a operation type (Create, Update, Delete)
type Message struct {
proto.Message
TypeName string
OpType OpType
FieldMaskPaths []string
}
// Entry represents an oplog entry
type Entry struct {
*store.Entry
Cipherer wrapping.Wrapper `sql:"-"`
Ticketer Ticketer `sql:"-"`
}
// Metadata provides meta information about the Entry
type Metadata map[string][]string
// NewEntry creates a new Entry
func NewEntry(aggregateName string, metadata Metadata, cipherer wrapping.Wrapper, ticketer Ticketer) (*Entry, error) {
entry := Entry{
Entry: &store.Entry{
AggregateName: aggregateName,
Version: Version,
},
Cipherer: cipherer,
Ticketer: ticketer,
}
if len(metadata) > 0 {
storeMD := []*store.Metadata{}
for k, v := range metadata {
if len(v) > 0 {
for _, vv := range v {
storeMD = append(storeMD, &store.Metadata{Key: k, Value: vv})
}
continue
}
// this metadata just has a key with no values
storeMD = append(storeMD, &store.Metadata{Key: k})
}
entry.Metadata = storeMD
}
if err := entry.validate(); err != nil {
return nil, fmt.Errorf("error creating entry: %w", err)
}
return &entry, nil
}
func (e *Entry) validate() error {
if e.Cipherer == nil {
return errors.New("entry Cipherer is nil")
}
if e.Ticketer == nil {
return errors.New("entry Ticketer is nil")
}
if e.Entry == nil {
return errors.New("store.Entry is nil")
}
if e.Entry.Version == "" {
return errors.New("entry version is not set")
}
if e.Entry.AggregateName == "" {
return errors.New("entry aggregate name is not set")
}
return nil
}
// UnmarshalData the data attribute from []byte (treated as a FIFO QueueBuffer) to a []proto.Message
func (e *Entry) UnmarshalData(types *TypeCatalog) ([]Message, error) {
if types == nil {
return nil, errors.New("TypeCatalog is nil")
}
if len(e.Data) == 0 {
return nil, errors.New("no Data to unmarshal")
}
msgs := []Message{}
queue := Queue{
Buffer: *bytes.NewBuffer(e.Data),
Catalog: types,
}
for {
m, typ, fieldMaskPaths, err := queue.Remove()
if err == io.EOF {
break
}
if err != nil {
return nil, fmt.Errorf("error removing item from queue: %w", err)
}
name, err := types.GetTypeName(m)
if err != nil {
return nil, fmt.Errorf("error getting TypeName: %w", err)
}
msgs = append(msgs, Message{Message: m, TypeName: name, OpType: typ, FieldMaskPaths: fieldMaskPaths})
}
return msgs, nil
}
// WriteEntryWith the []proto.Message marshaled into the entry data as a FIFO QueueBuffer
// if Cipherer != nil then the data is authentication encrypted
func (e *Entry) WriteEntryWith(ctx context.Context, tx Writer, ticket *store.Ticket, msgs ...*Message) error {
if tx == nil {
return errors.New("bad writer")
}
if err := e.validate(); err != nil {
return fmt.Errorf("error vetting entry for writing: %w", err)
}
if ticket == nil || ticket.Version == 0 {
return errors.New("bad ticket")
}
queue := Queue{}
for _, m := range msgs {
if m == nil {
return errors.New("bad message")
}
if err := queue.Add(m.Message, m.TypeName, m.OpType, WithFieldMaskPaths(m.FieldMaskPaths)); err != nil {
return fmt.Errorf("error adding message to queue: %w", err)
}
}
e.Data = append(e.Data, []byte(queue.Bytes())...)
if e.Cipherer != nil {
if err := e.EncryptData(ctx); err != nil {
return fmt.Errorf("error encrypting entry: %w", err)
}
}
if err := tx.Create(e); err != nil {
return fmt.Errorf("error writing data to storage: %w", err)
}
return e.Ticketer.Redeem(ticket)
}
// Write the entry as is with whatever it has for e.Data marshaled into a FIFO QueueBuffer
// Cipherer != nil then the data is authentication encrypted
func (e *Entry) Write(ctx context.Context, tx Writer, ticket *store.Ticket) error {
if err := e.validate(); err != nil {
return fmt.Errorf("error vetting entry for writing: %w", err)
}
if ticket == nil || ticket.Version == 0 {
return errors.New("bad ticket")
}
if e.Cipherer != nil {
if err := e.EncryptData(ctx); err != nil {
return fmt.Errorf("error encrypting entry: %w", err)
}
}
if err := tx.Create(e); err != nil {
return fmt.Errorf("error writing data to storage: %w", err)
}
return e.Ticketer.Redeem(ticket)
}
// EncryptData the entry's data using its Cipherer (wrapping.Wrapper)
func (e *Entry) EncryptData(ctx context.Context) error {
d, err := e.Cipherer.Encrypt(ctx, e.Data, nil)
if err != nil {
return fmt.Errorf("error encrypting entry: %w", err)
}
blob, err := protoV1.Marshal(d)
if err != nil {
return fmt.Errorf("error marshaling encrypted data: %w", err)
}
e.Data = []byte(base64.RawURLEncoding.EncodeToString(blob))
return nil
}
// DecryptData will decrypt the entry's data using its Cipherer (wrapping.Wrapper)
func (e *Entry) DecryptData(ctx context.Context) error {
blob, err := base64.RawURLEncoding.DecodeString(string(e.Data))
if err != nil {
return fmt.Errorf("error decoding encrypted data: %w", err)
}
var msg wrapping.EncryptedBlobInfo
err = protoV1.Unmarshal(blob, &msg)
if err != nil {
return fmt.Errorf("error unmarshaling encrypted data: %w", err)
}
d, err := e.Cipherer.Decrypt(ctx, &msg, nil)
if err != nil {
return fmt.Errorf("error decrypting data: %w", err)
}
e.Data = d
return nil
}
// Replay provides the ability to replay an entry. you must initialize any new tables ending with the tableSuffix before
// calling Replay, otherwise you'll get "a table doesn't exist" error.
func (e *Entry) Replay(ctx context.Context, tx Writer, types *TypeCatalog, tableSuffix string) error {
msgs, err := e.UnmarshalData(types)
if err != nil {
return fmt.Errorf("error on UnmarshalData: %w", err)
}
for _, m := range msgs {
em, ok := m.Message.(ReplayableMessage)
if !ok {
return fmt.Errorf("%T is not a ReplayableMessage", m.Message)
}
origTableName := em.TableName()
defer em.SetTableName(origTableName)
em.SetTableName(origTableName + tableSuffix)
switch m.OpType {
case OpType_CREATE_OP:
if err := tx.Create(m.Message); err != nil {
return fmt.Errorf("replay error: %w", err)
}
case OpType_UPDATE_OP:
if err := tx.Update(m.Message, m.FieldMaskPaths); err != nil {
return fmt.Errorf("replay error: %w", err)
}
case OpType_DELETE_OP:
if err := tx.Delete(m.Message); err != nil {
return fmt.Errorf("replay error: %w", err)
}
default:
return fmt.Errorf("replay error: unhandled operation %T", m.OpType)
}
}
return nil
}
+886
View File
@@ -0,0 +1,886 @@
package oplog
import (
"context"
"crypto/rand"
"database/sql"
"fmt"
"os"
"strings"
"sync"
"testing"
wrapping "github.com/hashicorp/go-kms-wrapping"
"github.com/hashicorp/go-kms-wrapping/wrappers/aead"
"github.com/hashicorp/go-uuid"
"github.com/hashicorp/watchtower/internal/oplog/oplog_test"
"github.com/jinzhu/gorm"
"gotest.tools/assert"
"github.com/golang-migrate/migrate/v4"
_ "github.com/golang-migrate/migrate/v4/database/postgres"
_ "github.com/golang-migrate/migrate/v4/source/file"
"github.com/ory/dockertest/v3"
)
// Need a way to manage the shared database resource for these oplog
// tests, so runningTests gives us a waitgroup to do this and clean up
// the shared database resource when we're done.
var runningTests sync.WaitGroup
// startTest signals that we're starting a test that uses the shared test resources
func startTest() {
runningTests.Add(1)
}
// completeTest signals that we've finished a test that uses the shared test resources
func completeTest() {
runningTests.Done()
}
// waitForTests will wait for all the tests that are sharing resources like the database
func waitForTests() {
runningTests.Wait()
}
// testDatabaseURL is initialized once using sync.Once and set to the database URL for testing
var testDatabaseURL string
// testInitDatabase ensures that the database is only initialized once during the tests.
var testInitDatabase sync.Once
// setup the tests (initialize the database one-time and intialized testDatabaseURL)
func setup(t *testing.T) (func(), string) {
cleanup := func() {}
var url string
var err error
testInitDatabase.Do(func() {
cleanup, url, err = initDbInDocker(t)
if err != nil {
panic(err)
}
testDatabaseURL = url
db, err := test_dbconn(url)
if err != nil {
panic(err)
}
defer db.Close()
oplog_test.Init(db)
})
return cleanup, testDatabaseURL
}
// Test_BasicOplog provides some basic unit tests for oplogs
func Test_BasicOplog(t *testing.T) {
t.Parallel()
startTest()
cleanup, url := setup(t)
defer cleanup()
defer completeTest() // must come after the "defer cleanup()"
db, err := test_dbconn(url)
assert.NilError(t, err)
defer db.Close()
t.Run("EncryptData/DecryptData/UnmarshalData", func(t *testing.T) {
cipherer := initWrapper(t)
// now let's us optimistic locking via a ticketing system for a serialized oplog
ticketer, err := NewGormTicketer(db, WithAggregateNames(true))
assert.NilError(t, err)
types, err := NewTypeCatalog(Type{new(oplog_test.TestUser), "user"})
assert.NilError(t, err)
queue := Queue{Catalog: types}
id, err := uuid.GenerateUUID()
assert.NilError(t, err)
user := oplog_test.TestUser{
Name: "foo-" + id,
}
resp := db.Create(&user)
assert.NilError(t, resp.Error)
err = queue.Add(&user, "user", OpType_CREATE_OP)
assert.NilError(t, err)
l, err := NewEntry(
"test-users",
Metadata{
"key-only": nil,
"deployment": []string{"amex"},
"project": []string{"central-info-systems", "local-info-systems"},
},
cipherer,
ticketer,
)
assert.NilError(t, err)
l.Data = queue.Bytes()
err = l.EncryptData(context.Background())
assert.NilError(t, err)
resp = db.Create(&l)
assert.NilError(t, resp.Error)
entryId := l.Id
var foundEntry Entry
err = db.Where("id = ?", entryId).First(&foundEntry).Error
assert.NilError(t, err)
foundEntry.Cipherer = cipherer
err = foundEntry.DecryptData(context.Background())
assert.NilError(t, err)
foundUsers, err := foundEntry.UnmarshalData(types)
assert.NilError(t, err)
assert.Assert(t, foundUsers[0].Message.(*oplog_test.TestUser).Name == user.Name)
foundUsers, err = foundEntry.UnmarshalData(types)
assert.NilError(t, err)
assert.Assert(t, foundUsers[0].Message.(*oplog_test.TestUser).Name == user.Name)
})
t.Run("write entry", func(t *testing.T) {
cipherer := initWrapper(t)
// now let's us optimistic locking via a ticketing system for a serialized oplog
ticketer, err := NewGormTicketer(db, WithAggregateNames(true))
assert.NilError(t, err)
types, err := NewTypeCatalog(Type{new(oplog_test.TestUser), "user"})
assert.NilError(t, err)
queue := Queue{Catalog: types}
id, err := uuid.GenerateUUID()
assert.NilError(t, err)
user := oplog_test.TestUser{
Name: "foo-" + id,
}
resp := db.Create(&user)
assert.NilError(t, resp.Error)
ticketName, err := uuid.GenerateUUID()
assert.NilError(t, err)
err = ticketer.InitTicket(ticketName)
assert.NilError(t, err)
ticket, err := ticketer.GetTicket(ticketName)
assert.NilError(t, err)
queue = Queue{}
err = queue.Add(&user, "user", OpType_CREATE_OP)
assert.NilError(t, err)
newLogEntry, err := NewEntry(
"test-users",
Metadata{
"key-only": nil,
"deployment": []string{"amex"},
"project": []string{"central-info-systems", "local-info-systems"},
},
cipherer,
ticketer,
)
assert.NilError(t, err)
newLogEntry.Data = queue.Bytes()
err = newLogEntry.Write(context.Background(), &GormWriter{db}, ticket)
assert.NilError(t, err)
assert.Assert(t, newLogEntry.Id != 0)
})
}
// Test_NewEntry provides some basic unit tests for NewEntry
func Test_NewEntry(t *testing.T) {
t.Parallel()
startTest()
cleanup, url := setup(t)
defer cleanup()
defer completeTest() // must come after the "defer cleanup()"
db, err := test_dbconn(url)
assert.NilError(t, err)
defer db.Close()
t.Run("valid", func(t *testing.T) {
cipherer := initWrapper(t)
ticketer, err := NewGormTicketer(db, WithAggregateNames(true))
assert.NilError(t, err)
_, err = NewEntry(
"test-users",
Metadata{
"key-only": nil,
"deployment": []string{"amex"},
"project": []string{"central-info-systems", "local-info-systems"},
},
cipherer,
ticketer,
)
assert.NilError(t, err)
})
t.Run("no metadata success", func(t *testing.T) {
cipherer := initWrapper(t)
ticketer, err := NewGormTicketer(db, WithAggregateNames(true))
assert.NilError(t, err)
_, err = NewEntry(
"test-users",
nil,
cipherer,
ticketer,
)
assert.NilError(t, err)
})
t.Run("no aggregateName", func(t *testing.T) {
cipherer := initWrapper(t)
ticketer, err := NewGormTicketer(db, WithAggregateNames(true))
assert.NilError(t, err)
_, err = NewEntry(
"",
Metadata{
"key-only": nil,
"deployment": []string{"amex"},
"project": []string{"central-info-systems", "local-info-systems"},
},
cipherer,
ticketer,
)
assert.Equal(t, err.Error(), "error creating entry: entry aggregate name is not set")
})
t.Run("bad cipherer", func(t *testing.T) {
ticketer, err := NewGormTicketer(db, WithAggregateNames(true))
assert.NilError(t, err)
_, err = NewEntry(
"test-users",
Metadata{
"key-only": nil,
"deployment": []string{"amex"},
"project": []string{"central-info-systems", "local-info-systems"},
},
nil,
ticketer,
)
assert.Equal(t, err.Error(), "error creating entry: entry Cipherer is nil")
})
t.Run("bad ticket", func(t *testing.T) {
cipherer := initWrapper(t)
_, err := NewEntry(
"test-users",
Metadata{
"key-only": nil,
"deployment": []string{"amex"},
"project": []string{"central-info-systems", "local-info-systems"},
},
cipherer,
nil,
)
assert.Equal(t, err.Error(), "error creating entry: entry Ticketer is nil")
})
}
func Test_UnmarshalData(t *testing.T) {
t.Parallel()
startTest()
cleanup, url := setup(t)
defer cleanup()
defer completeTest() // must come after the "defer cleanup()"
db, err := test_dbconn(url)
assert.NilError(t, err)
defer db.Close()
cipherer := initWrapper(t)
// now let's us optimistic locking via a ticketing system for a serialized oplog
ticketer, err := NewGormTicketer(db, WithAggregateNames(true))
types, err := NewTypeCatalog(Type{new(oplog_test.TestUser), "user"})
assert.NilError(t, err)
id, err := uuid.GenerateUUID()
assert.NilError(t, err)
t.Run("valid", func(t *testing.T) {
queue := Queue{Catalog: types}
user := oplog_test.TestUser{
Name: "foo-" + id,
}
err = queue.Add(&user, "user", OpType_CREATE_OP)
assert.NilError(t, err)
entry, err := NewEntry(
"test-users",
Metadata{
"key-only": nil,
"deployment": []string{"amex"},
"project": []string{"central-info-systems", "local-info-systems"},
},
cipherer,
ticketer,
)
assert.NilError(t, err)
entry.Data = queue.Bytes()
marshaledUsers, err := entry.UnmarshalData(types)
assert.NilError(t, err)
assert.Assert(t, marshaledUsers[0].Message.(*oplog_test.TestUser).Name == user.Name)
take2marshaledUsers, err := entry.UnmarshalData(types)
assert.NilError(t, err)
assert.Assert(t, take2marshaledUsers[0].Message.(*oplog_test.TestUser).Name == user.Name)
})
t.Run("no data", func(t *testing.T) {
queue := Queue{Catalog: types}
entry, err := NewEntry(
"test-users",
Metadata{
"key-only": nil,
"deployment": []string{"amex"},
"project": []string{"central-info-systems", "local-info-systems"},
},
cipherer,
ticketer,
)
assert.NilError(t, err)
entry.Data = queue.Bytes()
_, err = entry.UnmarshalData(types)
assert.Equal(t, err.Error(), "no Data to unmarshal")
})
t.Run("nil types", func(t *testing.T) {
queue := Queue{Catalog: types}
user := oplog_test.TestUser{
Name: "foo-" + id,
}
err = queue.Add(&user, "user", OpType_CREATE_OP)
assert.NilError(t, err)
entry, err := NewEntry(
"test-users",
Metadata{
"key-only": nil,
"deployment": []string{"amex"},
"project": []string{"central-info-systems", "local-info-systems"},
},
cipherer,
ticketer,
)
assert.NilError(t, err)
_, err = entry.UnmarshalData(nil)
assert.Equal(t, err.Error(), "TypeCatalog is nil")
})
t.Run("missing type", func(t *testing.T) {
queue := Queue{Catalog: types}
user := oplog_test.TestUser{
Name: "foo-" + id,
}
err = queue.Add(&user, "user", OpType_CREATE_OP)
assert.NilError(t, err)
entry, err := NewEntry(
"test-users",
Metadata{
"key-only": nil,
"deployment": []string{"amex"},
"project": []string{"central-info-systems", "local-info-systems"},
},
cipherer,
ticketer,
)
assert.NilError(t, err)
entry.Data = queue.Bytes()
types, err := NewTypeCatalog(Type{new(oplog_test.TestUser), "not-valid-name"})
assert.NilError(t, err)
_, err = entry.UnmarshalData(types)
assert.Equal(t, err.Error(), "error removing item from queue: error getting the TypeName for Remove: error typeName is not found for Get")
})
}
// Test_Replay provides some basic unit tests for replaying entries
func Test_Replay(t *testing.T) {
startTest()
t.Parallel()
cleanup, url := setup(t)
defer cleanup()
defer completeTest() // must come after the "defer cleanup()"
db, err := test_dbconn(url)
assert.NilError(t, err)
defer db.Close()
cipherer := initWrapper(t)
id, err := uuid.GenerateUUID()
assert.NilError(t, err)
// setup new tables for replay
tableSuffix := "_" + id
tmpUserModel := &oplog_test.ReplayableTestUser{}
tmpUserModel.SetTableName(fmt.Sprintf("%s%s", tmpUserModel.TableName(), tableSuffix))
db.AutoMigrate(tmpUserModel)
defer db.DropTableIfExists(tmpUserModel)
ticketName, err := uuid.GenerateUUID()
assert.NilError(t, err)
ticketer, err := NewGormTicketer(db, WithAggregateNames(true))
assert.NilError(t, err)
err = ticketer.InitTicket(ticketName)
assert.NilError(t, err)
t.Run("replay:create/update", func(t *testing.T) {
ticket, err := ticketer.GetTicket(ticketName)
assert.NilError(t, err)
newLogEntry, err := NewEntry(
"test-users",
Metadata{
"key-only": nil,
"deployment": []string{"amex"},
"project": []string{"central-info-systems", "local-info-systems"},
},
cipherer,
ticketer,
)
assert.NilError(t, err)
tx := db.Begin()
id3, err := uuid.GenerateUUID()
assert.NilError(t, err)
userName := "foo-" + id3
// create a user that's replayable
userCreate := oplog_test.ReplayableTestUser{
TestUser: oplog_test.TestUser{
Name: userName,
},
}
err = tx.Create(&userCreate).Error
assert.NilError(t, err)
userSave := oplog_test.ReplayableTestUser{
TestUser: oplog_test.TestUser{
Id: userCreate.Id,
Name: userCreate.Name,
Email: userName + "@hashicorp.com",
},
}
err = tx.Save(&userSave).Error
assert.NilError(t, err)
userUpdate := oplog_test.ReplayableTestUser{
TestUser: oplog_test.TestUser{
Id: userCreate.Id,
PhoneNumber: "867-5309",
},
}
err = tx.Model(&userUpdate).Updates(map[string]interface{}{"PhoneNumber": "867-5309"}).Error
assert.NilError(t, err)
err = newLogEntry.WriteEntryWith(context.Background(), &GormWriter{tx}, ticket,
&Message{Message: &userCreate, TypeName: "user", OpType: OpType_CREATE_OP},
&Message{Message: &userSave, TypeName: "user", OpType: OpType_UPDATE_OP},
&Message{Message: &userUpdate, TypeName: "user", OpType: OpType_UPDATE_OP, FieldMaskPaths: []string{"PhoneNumber"}},
)
assert.NilError(t, err)
types, err := NewTypeCatalog(Type{new(oplog_test.ReplayableTestUser), "user"})
assert.NilError(t, err)
var foundEntry Entry
err = tx.Where("id = ?", newLogEntry.Id).First(&foundEntry).Error
assert.NilError(t, err)
foundEntry.Cipherer = cipherer
err = foundEntry.DecryptData(context.Background())
assert.NilError(t, err)
err = foundEntry.Replay(context.Background(), &GormWriter{tx}, types, tableSuffix)
assert.NilError(t, err)
var foundUser oplog_test.TestUser
err = tx.Where("id = ?", userCreate.Id).First(&foundUser).Error
assert.NilError(t, err)
var foundReplayedUser oplog_test.TestUser
err = tx.Where("id = ?", userCreate.Id).First(&foundReplayedUser).Error
assert.NilError(t, err)
assert.Assert(t, foundUser.Id == foundReplayedUser.Id)
assert.Assert(t, foundUser.Name == foundReplayedUser.Name && foundUser.Name == userName)
assert.Assert(t, foundUser.PhoneNumber == foundReplayedUser.PhoneNumber && foundReplayedUser.PhoneNumber == "867-5309")
assert.Assert(t, foundUser.Email == foundReplayedUser.Email && foundReplayedUser.Email == userName+"@hashicorp.com")
tx.Commit()
})
t.Run("replay:delete", func(t *testing.T) {
// we need to test delete replays now...
tx2 := db.Begin()
ticket2, err := ticketer.GetTicket(ticketName)
assert.NilError(t, err)
id4, err := uuid.GenerateUUID()
assert.NilError(t, err)
userName2 := "foo-" + id4
// create a user that's replayable
userCreate2 := oplog_test.ReplayableTestUser{
TestUser: oplog_test.TestUser{
Name: userName2,
},
}
err = tx2.Create(&userCreate2).Error
assert.NilError(t, err)
deleteUser2 := oplog_test.ReplayableTestUser{
TestUser: oplog_test.TestUser{
Id: userCreate2.Id,
},
}
err = tx2.Delete(&deleteUser2).Error
assert.NilError(t, err)
newLogEntry2, err := NewEntry(
"test-users",
Metadata{
"key-only": nil,
"deployment": []string{"amex"},
"project": []string{"central-info-systems", "local-info-systems"},
},
cipherer,
ticketer,
)
assert.NilError(t, err)
err = newLogEntry2.WriteEntryWith(context.Background(), &GormWriter{tx2}, ticket2,
&Message{Message: &userCreate2, TypeName: "user", OpType: OpType_CREATE_OP},
&Message{Message: &deleteUser2, TypeName: "user", OpType: OpType_DELETE_OP},
)
assert.NilError(t, err)
var foundEntry2 Entry
err = tx2.Where("id = ?", newLogEntry2.Id).First(&foundEntry2).Error
assert.NilError(t, err)
foundEntry2.Cipherer = cipherer
err = foundEntry2.DecryptData(context.Background())
assert.NilError(t, err)
types, err := NewTypeCatalog(Type{new(oplog_test.ReplayableTestUser), "user"})
assert.NilError(t, err)
err = foundEntry2.Replay(context.Background(), &GormWriter{tx2}, types, tableSuffix)
assert.NilError(t, err)
var foundUser2 oplog_test.TestUser
err = tx2.Where("id = ?", userCreate2.Id).First(&foundUser2).Error
assert.Assert(t, err == gorm.ErrRecordNotFound)
var foundReplayedUser2 oplog_test.TestUser
err = tx2.Where("id = ?", userCreate2.Id).First(&foundReplayedUser2).Error
assert.Assert(t, err == gorm.ErrRecordNotFound)
tx2.Commit()
})
}
// Test_WriteEntryWith provides unit tests for oplog.WriteEntryWith
func Test_WriteEntryWith(t *testing.T) {
t.Parallel()
startTest()
cleanup, url := setup(t)
defer cleanup()
defer completeTest() // must come after the "defer cleanup()"
db, err := test_dbconn(url)
assert.NilError(t, err)
defer db.Close()
cipherer := initWrapper(t)
id, err := uuid.GenerateUUID()
assert.NilError(t, err)
u := oplog_test.TestUser{
Name: "foo-" + id,
}
t.Log(&u)
id2, err := uuid.GenerateUUID()
assert.NilError(t, err)
u2 := oplog_test.TestUser{
Name: "foo-" + id2,
}
t.Log(&u2)
ticketName, err := uuid.GenerateUUID()
assert.NilError(t, err)
ticketer, err := NewGormTicketer(db, WithAggregateNames(true))
assert.NilError(t, err)
err = ticketer.InitTicket(ticketName)
assert.NilError(t, err)
ticket, err := ticketer.GetTicket(ticketName)
assert.NilError(t, err)
t.Run("successful", func(t *testing.T) {
newLogEntry, err := NewEntry(
"test-users",
Metadata{
"key-only": nil,
"deployment": []string{"amex"},
"project": []string{"central-info-systems", "local-info-systems"},
},
cipherer,
ticketer,
)
assert.NilError(t, err)
err = newLogEntry.WriteEntryWith(context.Background(), &GormWriter{db}, ticket,
&Message{Message: &u, TypeName: "user", OpType: OpType_CREATE_OP},
&Message{Message: &u2, TypeName: "user", OpType: OpType_CREATE_OP})
assert.NilError(t, err)
var foundEntry Entry
err = db.Where("id = ?", newLogEntry.Id).First(&foundEntry).Error
assert.NilError(t, err)
foundEntry.Cipherer = cipherer
types, err := NewTypeCatalog(Type{new(oplog_test.TestUser), "user"})
assert.NilError(t, err)
err = foundEntry.DecryptData(context.Background())
assert.NilError(t, err)
foundUsers, err := foundEntry.UnmarshalData(types)
assert.NilError(t, err)
assert.Assert(t, foundUsers[0].Message.(*oplog_test.TestUser).Name == u.Name)
})
t.Run("nil writer", func(t *testing.T) {
newLogEntry, err := NewEntry(
"test-users",
Metadata{
"key-only": nil,
"deployment": []string{"amex"},
"project": []string{"central-info-systems", "local-info-systems"},
},
cipherer,
ticketer,
)
assert.NilError(t, err)
err = newLogEntry.WriteEntryWith(context.Background(), nil, ticket,
&Message{Message: &u, TypeName: "user", OpType: OpType_CREATE_OP},
&Message{Message: &u2, TypeName: "user", OpType: OpType_CREATE_OP})
assert.Equal(t, err.Error(), "bad writer")
})
t.Run("nil ticket", func(t *testing.T) {
newLogEntry, err := NewEntry(
"test-users",
Metadata{
"key-only": nil,
"deployment": []string{"amex"},
"project": []string{"central-info-systems", "local-info-systems"},
},
cipherer,
ticketer,
)
assert.NilError(t, err)
err = newLogEntry.WriteEntryWith(context.Background(), &GormWriter{db}, nil,
&Message{Message: &u, TypeName: "user", OpType: OpType_CREATE_OP},
&Message{Message: &u2, TypeName: "user", OpType: OpType_CREATE_OP})
assert.Equal(t, err.Error(), "bad ticket")
})
t.Run("nil ticket", func(t *testing.T) {
newLogEntry, err := NewEntry(
"test-users",
Metadata{
"key-only": nil,
"deployment": []string{"amex"},
"project": []string{"central-info-systems", "local-info-systems"},
},
cipherer,
ticketer,
)
assert.NilError(t, err)
err = newLogEntry.WriteEntryWith(context.Background(), &GormWriter{db}, ticket, nil)
assert.Equal(t, err.Error(), "bad message")
})
}
// Test_TicketSerialization provides unit tests for making sure oplog.Tickets properly serialize writes to oplog entries
func Test_TicketSerialization(t *testing.T) {
startTest()
t.Parallel()
cleanup, url := setup(t)
defer cleanup()
defer completeTest() // must come after the "defer cleanup()"
db, err := test_dbconn(url)
assert.NilError(t, err)
defer db.Close()
ticketName, err := uuid.GenerateUUID()
assert.NilError(t, err)
ticketer, err := NewGormTicketer(db, WithAggregateNames(true))
assert.NilError(t, err)
// in it's own transaction, init the ticket
_ = ticketer.InitTicket(ticketName)
cipherer := initWrapper(t)
id, err := uuid.GenerateUUID()
assert.NilError(t, err)
firstTx := db.Begin()
firstUser := oplog_test.TestUser{
Name: "foo-" + id,
}
err = firstTx.Create(&firstUser).Error
assert.NilError(t, err)
firstTicket, err := ticketer.GetTicket(ticketName)
assert.NilError(t, err)
firstQueue := Queue{}
err = firstQueue.Add(&firstUser, "user", OpType_CREATE_OP)
assert.NilError(t, err)
firstLogEntry, err := NewEntry(
"test-users",
Metadata{
"key-only": nil,
"deployment": []string{"amex"},
"project": []string{"central-info-systems", "local-info-systems"},
},
cipherer,
ticketer,
)
assert.NilError(t, err)
firstLogEntry.Data = firstQueue.Bytes()
id2, err := uuid.GenerateUUID()
assert.NilError(t, err)
secondTx := db.Begin()
secondUser := oplog_test.TestUser{
Name: "foo-" + id2,
}
err = secondTx.Create(&secondUser).Error
assert.NilError(t, err)
secondTicket, err := ticketer.GetTicket(ticketName)
assert.NilError(t, err)
secondQueue := Queue{}
err = secondQueue.Add(&secondUser, "user", OpType_CREATE_OP)
assert.NilError(t, err)
secondLogEntry, err := NewEntry(
"foobar",
Metadata{
"key-only": nil,
"deployment": []string{"amex"},
"project": []string{"central-info-systems", "local-info-systems"},
},
cipherer,
ticketer,
)
assert.NilError(t, err)
secondLogEntry.Data = secondQueue.Bytes()
err = secondLogEntry.Write(context.Background(), &GormWriter{secondTx}, secondTicket)
assert.NilError(t, err)
assert.Assert(t, secondLogEntry.Id != 0)
err = secondTx.Commit().Error
assert.NilError(t, err)
assert.Assert(t, secondLogEntry.Id != 0)
err = firstLogEntry.Write(context.Background(), &GormWriter{firstTx}, firstTicket)
if err != nil {
firstTx.Rollback()
} else {
firstTx.Commit()
t.Error("should have failed to write firstLogEntry")
}
}
// initDbInDocker initializes postgres within dockertest for the unit tests
func initDbInDocker(t *testing.T) (cleanup func(), retURL string, err error) {
if os.Getenv("PG_URL") != "" {
initTestStore(t, func() {}, os.Getenv("PG_URL"))
return func() {}, os.Getenv("PG_URL"), nil
}
pool, err := dockertest.NewPool("")
if err != nil {
return func() {}, "", fmt.Errorf("could not connect to docker: %w", err)
}
resource, err := pool.Run("postgres", "latest", []string{"POSTGRES_PASSWORD=secret", "POSTGRES_DB=watchtower"})
if err != nil {
return func() {}, "", fmt.Errorf("could not start resource: %w", err)
}
c := func() {
cleanupResource(t, pool, resource)
}
url := fmt.Sprintf("postgres://postgres:secret@localhost:%s?sslmode=disable", resource.GetPort("5432/tcp"))
if err := pool.Retry(func() error {
db, err := sql.Open("postgres", url)
if err != nil {
return fmt.Errorf("error opening postgres dev container: %w", err)
}
if err := db.Ping(); err != nil {
return err
}
defer db.Close()
return nil
}); err != nil {
return func() {}, "", fmt.Errorf("could not connect to docker: %w", err)
}
initTestStore(t, c, url)
return c, url, nil
}
// initWrapper initializes an AEAD wrapping.Wrapper for testing the oplog
func initWrapper(t *testing.T) wrapping.Wrapper {
rootKey := make([]byte, 32)
n, err := rand.Read(rootKey)
if err != nil {
t.Fatal(err)
}
if n != 32 {
t.Fatal(n)
}
root := aead.NewWrapper(nil)
if err := root.SetAESGCMKeyBytes(rootKey); err != nil {
t.Fatal(err)
}
return root
}
// initTestStore will execute the migrations needed to initialize the store for tests
func initTestStore(t *testing.T, cleanup func(), url string) {
// run migrations
m, err := migrate.New("file://migrations/postgres", url)
if err != nil {
cleanup()
t.Fatalf("Error creating migrations: %s", err)
}
if err := m.Up(); err != nil && err != migrate.ErrNoChange {
cleanup()
t.Fatalf("Error running migrations: %s", err)
}
}
// cleanupResource will clean up the dockertest resources (postgres)
func cleanupResource(t *testing.T, pool *dockertest.Pool, resource *dockertest.Resource) {
waitForTests()
var err error
for i := 0; i < 10; i++ {
err = pool.Purge(resource)
if err == nil {
return
}
}
if strings.Contains(err.Error(), "No such container") {
return
}
t.Fatalf("Failed to cleanup local container: %s", err)
}
func test_dbconn(url string) (*gorm.DB, error) {
return gorm.Open("postgres", url)
}
+48
View File
@@ -0,0 +1,48 @@
// Package oplog_test provides some gorm helper funcs for testing oplog database integrations
package oplog_test
import "github.com/jinzhu/gorm"
// Init will use gorm migrations to init tables for test models
func Init(db *gorm.DB) {
db.AutoMigrate(&TestUser{})
db.AutoMigrate(&TestCar{})
db.AutoMigrate(&TestRental{})
}
// Reinit will use gorm to drop then init tables for test models
func Reinit(db *gorm.DB) {
db.DropTableIfExists(&TestUser{})
db.DropTableIfExists(&TestCar{})
db.DropTableIfExists(&TestRental{})
Init(db)
}
// ReplayableTestUser is simply that: a user we can replay for tests
// the big diff is that it supports overriding the table name
type ReplayableTestUser struct {
TestUser
Table string `gorm:"-"`
}
// TableName overrides the table name for the test user model
func (u *ReplayableTestUser) TableName() string {
if u.Table != "" {
return u.Table
}
return "oplog_test_user"
}
func (u *ReplayableTestUser) SetTableName(name string) {
if name != "" {
u.Table = name
}
}
func (*TestUser) TableName() string { return "oplog_test_user" }
// TableName overrides the table name for the test car model
func (*TestCar) TableName() string { return "oplog_test_car" }
// TableName overrids the table name for the test rental model
func (*TestRental) TableName() string { return "oplog_test_rental" }
+342
View File
@@ -0,0 +1,342 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.20.1
// protoc v3.11.4
// source: internal/oplog/oplog_test/oplog_test.proto
// define a test proto package
package oplog_test
import (
proto "github.com/golang/protobuf/proto"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
// This is a compile-time assertion that a sufficiently up-to-date version
// of the legacy proto package is being used.
const _ = proto.ProtoPackageIsVersion4
// TestUser for gorm test user model
type TestUser struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// @inject_tag: gorm:"primary_key"
Id uint32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty" gorm:"primary_key"`
Name string `protobuf:"bytes,4,opt,name=name,proto3" json:"name,omitempty"`
PhoneNumber string `protobuf:"bytes,5,opt,name=phone_number,json=phoneNumber,proto3" json:"phone_number,omitempty"`
Email string `protobuf:"bytes,6,opt,name=email,proto3" json:"email,omitempty"`
}
func (x *TestUser) Reset() {
*x = TestUser{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_oplog_oplog_test_oplog_test_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *TestUser) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*TestUser) ProtoMessage() {}
func (x *TestUser) ProtoReflect() protoreflect.Message {
mi := &file_internal_oplog_oplog_test_oplog_test_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use TestUser.ProtoReflect.Descriptor instead.
func (*TestUser) Descriptor() ([]byte, []int) {
return file_internal_oplog_oplog_test_oplog_test_proto_rawDescGZIP(), []int{0}
}
func (x *TestUser) GetId() uint32 {
if x != nil {
return x.Id
}
return 0
}
func (x *TestUser) GetName() string {
if x != nil {
return x.Name
}
return ""
}
func (x *TestUser) GetPhoneNumber() string {
if x != nil {
return x.PhoneNumber
}
return ""
}
func (x *TestUser) GetEmail() string {
if x != nil {
return x.Email
}
return ""
}
// TestCar for gorm test car model
type TestCar struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// @inject_tag: gorm:"primary_key"
Id uint32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty" gorm:"primary_key"`
Model string `protobuf:"bytes,4,opt,name=model,proto3" json:"model,omitempty"`
Mpg int32 `protobuf:"varint,5,opt,name=mpg,proto3" json:"mpg,omitempty"`
}
func (x *TestCar) Reset() {
*x = TestCar{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_oplog_oplog_test_oplog_test_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *TestCar) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*TestCar) ProtoMessage() {}
func (x *TestCar) ProtoReflect() protoreflect.Message {
mi := &file_internal_oplog_oplog_test_oplog_test_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use TestCar.ProtoReflect.Descriptor instead.
func (*TestCar) Descriptor() ([]byte, []int) {
return file_internal_oplog_oplog_test_oplog_test_proto_rawDescGZIP(), []int{1}
}
func (x *TestCar) GetId() uint32 {
if x != nil {
return x.Id
}
return 0
}
func (x *TestCar) GetModel() string {
if x != nil {
return x.Model
}
return ""
}
func (x *TestCar) GetMpg() int32 {
if x != nil {
return x.Mpg
}
return 0
}
// TestRental for gorm test rental model
type TestRental struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
UserId uint32 `protobuf:"varint,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"`
CarId uint32 `protobuf:"varint,2,opt,name=car_id,json=carId,proto3" json:"car_id,omitempty"`
}
func (x *TestRental) Reset() {
*x = TestRental{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_oplog_oplog_test_oplog_test_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *TestRental) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*TestRental) ProtoMessage() {}
func (x *TestRental) ProtoReflect() protoreflect.Message {
mi := &file_internal_oplog_oplog_test_oplog_test_proto_msgTypes[2]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use TestRental.ProtoReflect.Descriptor instead.
func (*TestRental) Descriptor() ([]byte, []int) {
return file_internal_oplog_oplog_test_oplog_test_proto_rawDescGZIP(), []int{2}
}
func (x *TestRental) GetUserId() uint32 {
if x != nil {
return x.UserId
}
return 0
}
func (x *TestRental) GetCarId() uint32 {
if x != nil {
return x.CarId
}
return 0
}
var File_internal_oplog_oplog_test_oplog_test_proto protoreflect.FileDescriptor
var file_internal_oplog_oplog_test_oplog_test_proto_rawDesc = []byte{
0x0a, 0x2a, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x6f, 0x70, 0x6c, 0x6f, 0x67,
0x2f, 0x6f, 0x70, 0x6c, 0x6f, 0x67, 0x5f, 0x74, 0x65, 0x73, 0x74, 0x2f, 0x6f, 0x70, 0x6c, 0x6f,
0x67, 0x5f, 0x74, 0x65, 0x73, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x2d, 0x68, 0x61,
0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x77, 0x61, 0x74, 0x63, 0x68, 0x74, 0x6f, 0x77,
0x65, 0x72, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x6f, 0x70,
0x6c, 0x6f, 0x67, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x2e, 0x76, 0x31, 0x22, 0x67, 0x0a, 0x08, 0x54,
0x65, 0x73, 0x74, 0x55, 0x73, 0x65, 0x72, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20,
0x01, 0x28, 0x0d, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18,
0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x70,
0x68, 0x6f, 0x6e, 0x65, 0x5f, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, 0x05, 0x20, 0x01, 0x28,
0x09, 0x52, 0x0b, 0x70, 0x68, 0x6f, 0x6e, 0x65, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x12, 0x14,
0x0a, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65,
0x6d, 0x61, 0x69, 0x6c, 0x22, 0x41, 0x0a, 0x07, 0x54, 0x65, 0x73, 0x74, 0x43, 0x61, 0x72, 0x12,
0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x02, 0x69, 0x64, 0x12,
0x14, 0x0a, 0x05, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05,
0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x12, 0x10, 0x0a, 0x03, 0x6d, 0x70, 0x67, 0x18, 0x05, 0x20, 0x01,
0x28, 0x05, 0x52, 0x03, 0x6d, 0x70, 0x67, 0x22, 0x3c, 0x0a, 0x0a, 0x54, 0x65, 0x73, 0x74, 0x52,
0x65, 0x6e, 0x74, 0x61, 0x6c, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64,
0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x15,
0x0a, 0x06, 0x63, 0x61, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05,
0x63, 0x61, 0x72, 0x49, 0x64, 0x42, 0x46, 0x5a, 0x44, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e,
0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x77, 0x61,
0x74, 0x63, 0x68, 0x74, 0x6f, 0x77, 0x65, 0x72, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61,
0x6c, 0x2f, 0x6f, 0x70, 0x6c, 0x6f, 0x67, 0x2f, 0x6f, 0x70, 0x6c, 0x6f, 0x67, 0x5f, 0x74, 0x65,
0x73, 0x74, 0x3b, 0x6f, 0x70, 0x6c, 0x6f, 0x67, 0x5f, 0x74, 0x65, 0x73, 0x74, 0x62, 0x06, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
file_internal_oplog_oplog_test_oplog_test_proto_rawDescOnce sync.Once
file_internal_oplog_oplog_test_oplog_test_proto_rawDescData = file_internal_oplog_oplog_test_oplog_test_proto_rawDesc
)
func file_internal_oplog_oplog_test_oplog_test_proto_rawDescGZIP() []byte {
file_internal_oplog_oplog_test_oplog_test_proto_rawDescOnce.Do(func() {
file_internal_oplog_oplog_test_oplog_test_proto_rawDescData = protoimpl.X.CompressGZIP(file_internal_oplog_oplog_test_oplog_test_proto_rawDescData)
})
return file_internal_oplog_oplog_test_oplog_test_proto_rawDescData
}
var file_internal_oplog_oplog_test_oplog_test_proto_msgTypes = make([]protoimpl.MessageInfo, 3)
var file_internal_oplog_oplog_test_oplog_test_proto_goTypes = []interface{}{
(*TestUser)(nil), // 0: hashicorp.watchtower.controller.oplog.test.v1.TestUser
(*TestCar)(nil), // 1: hashicorp.watchtower.controller.oplog.test.v1.TestCar
(*TestRental)(nil), // 2: hashicorp.watchtower.controller.oplog.test.v1.TestRental
}
var file_internal_oplog_oplog_test_oplog_test_proto_depIdxs = []int32{
0, // [0:0] is the sub-list for method output_type
0, // [0:0] is the sub-list for method input_type
0, // [0:0] is the sub-list for extension type_name
0, // [0:0] is the sub-list for extension extendee
0, // [0:0] is the sub-list for field type_name
}
func init() { file_internal_oplog_oplog_test_oplog_test_proto_init() }
func file_internal_oplog_oplog_test_oplog_test_proto_init() {
if File_internal_oplog_oplog_test_oplog_test_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_internal_oplog_oplog_test_oplog_test_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*TestUser); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_internal_oplog_oplog_test_oplog_test_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*TestCar); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_internal_oplog_oplog_test_oplog_test_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*TestRental); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_internal_oplog_oplog_test_oplog_test_proto_rawDesc,
NumEnums: 0,
NumMessages: 3,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_internal_oplog_oplog_test_oplog_test_proto_goTypes,
DependencyIndexes: file_internal_oplog_oplog_test_oplog_test_proto_depIdxs,
MessageInfos: file_internal_oplog_oplog_test_oplog_test_proto_msgTypes,
}.Build()
File_internal_oplog_oplog_test_oplog_test_proto = out.File
file_internal_oplog_oplog_test_oplog_test_proto_rawDesc = nil
file_internal_oplog_oplog_test_oplog_test_proto_goTypes = nil
file_internal_oplog_oplog_test_oplog_test_proto_depIdxs = nil
}
@@ -0,0 +1,26 @@
syntax = "proto3";
// define a test proto package
package hashicorp.watchtower.controller.oplog.test.v1;
option go_package = "github.com/hashicorp/watchtower/internal/oplog/oplog_test;oplog_test";
// TestUser for gorm test user model
message TestUser {
// @inject_tag: gorm:"primary_key"
uint32 id = 1;
string name = 4;
string phone_number = 5;
string email = 6;
}
// TestCar for gorm test car model
message TestCar {
// @inject_tag: gorm:"primary_key"
uint32 id = 1;
string model = 4;
int32 mpg = 5;
}
// TestRental for gorm test rental model
message TestRental {
uint32 user_id = 1;
uint32 car_id = 2;
}
+42
View File
@@ -0,0 +1,42 @@
package oplog
// GetOpts - iterate the inbound Options and return a struct
func GetOpts(opt ...Option) Options {
opts := getDefaultOptions()
for _, o := range opt {
o(opts)
}
return opts
}
// Option - how Options are passed as arguments
type Option func(Options)
// Options = how options are represented
type Options map[string]interface{}
func getDefaultOptions() Options {
return Options{
optionWithFieldMaskPaths: []string{},
optionWithAggregateNames: false,
}
}
const optionWithFieldMaskPaths = "optionWithFieldMaskPaths"
// WithFieldMaskPaths represents an optional set of symbolic field paths (for example: "f.a", "f.b.d") used
// to specify a subset of fields that should be updated. (see google.golang.org/genproto/protobuf/field_mask)
func WithFieldMaskPaths(fieldMaskPaths []string) Option {
return func(o Options) {
o[optionWithFieldMaskPaths] = fieldMaskPaths
}
}
const optionWithAggregateNames = "optionWithAggregateNames"
// WithAggregateNames enables/disables the use of multiple aggregate names for Ticketers
func WithAggregateNames(enabled bool) Option {
return func(o Options) {
o[optionWithAggregateNames] = enabled
}
}
+91
View File
@@ -0,0 +1,91 @@
package oplog
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"sync"
"google.golang.org/genproto/protobuf/field_mask"
"google.golang.org/protobuf/proto"
)
// Queue provides a FIFO queue
type Queue struct {
// Buffer for the queue
bytes.Buffer
// Catalog provides a TypeCatalog for the types added to the queue
Catalog *TypeCatalog
mx sync.Mutex
}
// Add pb message to queue
func (r *Queue) Add(m proto.Message, typeName string, t OpType, opt ...Option) error {
// we're not checking the Catalog for nil, since it's not used
// when Adding messages to the queue
opts := GetOpts(opt...)
withPaths := opts[optionWithFieldMaskPaths].([]string)
value, err := proto.Marshal(m)
if err != nil {
return fmt.Errorf("error marshaling add parameter: %w", err)
}
msg := &AnyOperation{
TypeName: typeName,
Value: value,
OperationType: t,
FieldMask: &field_mask.FieldMask{Paths: withPaths},
}
data, err := proto.Marshal(msg)
if err != nil {
return fmt.Errorf("error marhaling the msg for Add: %w", err)
}
r.mx.Lock()
defer r.mx.Unlock()
err = binary.Write(r, binary.LittleEndian, uint32(len(data)))
if err != nil {
return err
}
n, err := r.Write(data)
if err != nil {
return fmt.Errorf("error writing to queue buffer: %w", err)
}
if n != len(data) {
return fmt.Errorf("error writing to queue buffer (incorrect number of bytes %d of %d)", n, len(data))
}
return nil
}
// Remove pb message from the queue and EOF if empty
func (r *Queue) Remove() (proto.Message, OpType, []string, error) {
if r.Catalog == nil {
return nil, OpType_UNKNOWN_OP, nil, errors.New("remove Catalog is nil")
}
r.mx.Lock()
defer r.mx.Unlock()
var n uint32
err := binary.Read(r, binary.LittleEndian, &n)
if err != nil {
return nil, 0, nil, err // intentionally not wrapping error so client can test for sentinel EOF error
}
data := r.Next(int(n))
msg := new(AnyOperation)
err = proto.Unmarshal(data, msg)
if err != nil {
return nil, 0, nil, fmt.Errorf("error marshaling the msg for Remove: %w", err)
}
if msg.Value == nil {
return nil, 0, nil, nil
}
any, err := r.Catalog.Get(msg.TypeName)
if err != nil {
return nil, 0, nil, fmt.Errorf("error getting the TypeName for Remove: %w", err)
}
pm := any.(proto.Message)
if err = proto.Unmarshal(msg.Value, pm); err != nil {
return nil, 0, nil, fmt.Errorf("error unmarshaling the value for Remove: %w", err)
}
return pm, msg.OperationType, msg.FieldMask.GetPaths(), nil
}
+89
View File
@@ -0,0 +1,89 @@
package oplog
import (
reflect "reflect"
"testing"
"github.com/hashicorp/watchtower/internal/oplog/oplog_test"
"google.golang.org/protobuf/proto"
"gotest.tools/assert"
)
// Test_Queue provides basic tests for the Queue type
func Test_Queue(t *testing.T) {
t.Parallel()
types, err := NewTypeCatalog(
Type{new(oplog_test.TestUser), "user"},
Type{new(oplog_test.TestCar), "car"},
Type{new(oplog_test.TestRental), "rental"},
)
assert.NilError(t, err)
queue := Queue{Catalog: types}
user := &oplog_test.TestUser{
Id: 1,
Name: "Alice",
PhoneNumber: "867-5309",
Email: "alice@bob.com",
}
car := &oplog_test.TestCar{
Id: 2,
Model: "Jeep",
Mpg: 25,
}
rental := &oplog_test.TestRental{
UserId: 1,
CarId: 2,
}
userUpdate := &oplog_test.TestUser{
Id: 1,
Name: "bob",
Email: "bob@alice.com",
}
t.Run("add/remove", func(t *testing.T) {
err = queue.Add(user, "user", OpType_CREATE_OP)
assert.NilError(t, err)
err = queue.Add(car, "car", OpType_CREATE_OP)
assert.NilError(t, err)
err = queue.Add(rental, "rental", OpType_CREATE_OP)
assert.NilError(t, err)
err = queue.Add(userUpdate, "user", OpType_UPDATE_OP, WithFieldMaskPaths([]string{"Name", "Email"}))
assert.NilError(t, err)
queuedUser, ty, fm, err := queue.Remove()
assert.NilError(t, err)
assert.Assert(t, proto.Equal(user, queuedUser))
assert.Assert(t, ty == OpType_CREATE_OP)
assert.Assert(t, fm == nil)
queuedCar, ty, fm, err := queue.Remove()
assert.NilError(t, err)
assert.Assert(t, proto.Equal(car, queuedCar))
assert.Assert(t, ty == OpType_CREATE_OP)
assert.Assert(t, fm == nil)
queuedRental, ty, fm, err := queue.Remove()
assert.NilError(t, err)
assert.Assert(t, proto.Equal(rental, queuedRental))
assert.Assert(t, ty == OpType_CREATE_OP)
assert.Assert(t, fm == nil)
queuedUserUpdate, ty, fm, err := queue.Remove()
assert.NilError(t, err)
assert.Assert(t, proto.Equal(userUpdate, queuedUserUpdate))
assert.Assert(t, ty == OpType_UPDATE_OP)
assert.Assert(t, reflect.DeepEqual(fm, []string{"Name", "Email"}))
})
t.Run("valid with nil type catalog", func(t *testing.T) {
queue := Queue{}
assert.NilError(t, queue.Add(user, "user", OpType_CREATE_OP))
})
t.Run("error with nil type catalog", func(t *testing.T) {
queue := Queue{}
_, _, _, err := queue.Remove()
assert.Check(t, err != nil)
assert.Equal(t, err.Error(), "remove Catalog is nil")
})
}
+8
View File
@@ -0,0 +1,8 @@
package oplog
// Replayable defines an interface for messages that can be replayed from the oplog entries. we need to be
// able to replay into different table names.
type ReplayableMessage interface {
TableName() string
SetTableName(name string)
}
+2
View File
@@ -0,0 +1,2 @@
// Package store provides storage types/behavior for the oplog
package store
+552
View File
@@ -0,0 +1,552 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.20.1
// protoc v3.11.4
// source: internal/oplog/store/oplog.proto
// define an oplog package within the watchtower controller
package store
import (
proto "github.com/golang/protobuf/proto"
timestamp "github.com/golang/protobuf/ptypes/timestamp"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
// This is a compile-time assertion that a sufficiently up-to-date version
// of the legacy proto package is being used.
const _ = proto.ProtoPackageIsVersion4
// Timestamp for oplog messages. We've defined a new local type wrapper
// of google.protobuf.Timestamp so we can implement sql.Scanner and sql.Valuer
// interfaces. See:
// https://golang.org/pkg/database/sql/#Scanner
// https://golang.org/pkg/database/sql/driver/#Valuer
type Timestamp struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Timestamp *timestamp.Timestamp `protobuf:"bytes,1,opt,name=timestamp,proto3" json:"timestamp,omitempty"`
}
func (x *Timestamp) Reset() {
*x = Timestamp{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_oplog_store_oplog_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Timestamp) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Timestamp) ProtoMessage() {}
func (x *Timestamp) ProtoReflect() protoreflect.Message {
mi := &file_internal_oplog_store_oplog_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Timestamp.ProtoReflect.Descriptor instead.
func (*Timestamp) Descriptor() ([]byte, []int) {
return file_internal_oplog_store_oplog_proto_rawDescGZIP(), []int{0}
}
func (x *Timestamp) GetTimestamp() *timestamp.Timestamp {
if x != nil {
return x.Timestamp
}
return nil
}
// Entry provides a message for oplog entries that's compatible with gorm
type Entry struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// @inject_tag: gorm:"primary_key"
Id uint32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty" gorm:"primary_key"`
// @inject_tag: gorm:"not_null;type:TIMESTAMP"
CreateTime *Timestamp `protobuf:"bytes,2,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty" gorm:"not_null;type:TIMESTAMP"`
// @inject_tag: gorm:"not_null;type:TIMESTAMP"
UpdateTime *Timestamp `protobuf:"bytes,3,opt,name=update_time,json=updateTime,proto3" json:"update_time,omitempty" gorm:"not_null;type:TIMESTAMP"`
// @inject_tag: gorm:"not_null"
Version string `protobuf:"bytes,4,opt,name=version,proto3" json:"version,omitempty" gorm:"not_null"`
// @inject_tat: gorm:"not_null"
AggregateName string `protobuf:"bytes,5,opt,name=aggregate_name,json=aggregateName,proto3" json:"aggregate_name,omitempty"`
// one to many relationship
// @inject_tag: gorm:"foreignkey:entry_id"
Metadata []*Metadata `protobuf:"bytes,6,rep,name=metadata,proto3" json:"metadata,omitempty" gorm:"foreignkey:entry_id"`
// @inject_tat: gorm:"not_null"
Data []byte `protobuf:"bytes,7,opt,name=data,proto3" json:"data,omitempty"`
}
func (x *Entry) Reset() {
*x = Entry{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_oplog_store_oplog_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Entry) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Entry) ProtoMessage() {}
func (x *Entry) ProtoReflect() protoreflect.Message {
mi := &file_internal_oplog_store_oplog_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Entry.ProtoReflect.Descriptor instead.
func (*Entry) Descriptor() ([]byte, []int) {
return file_internal_oplog_store_oplog_proto_rawDescGZIP(), []int{1}
}
func (x *Entry) GetId() uint32 {
if x != nil {
return x.Id
}
return 0
}
func (x *Entry) GetCreateTime() *Timestamp {
if x != nil {
return x.CreateTime
}
return nil
}
func (x *Entry) GetUpdateTime() *Timestamp {
if x != nil {
return x.UpdateTime
}
return nil
}
func (x *Entry) GetVersion() string {
if x != nil {
return x.Version
}
return ""
}
func (x *Entry) GetAggregateName() string {
if x != nil {
return x.AggregateName
}
return ""
}
func (x *Entry) GetMetadata() []*Metadata {
if x != nil {
return x.Metadata
}
return nil
}
func (x *Entry) GetData() []byte {
if x != nil {
return x.Data
}
return nil
}
// Metadata provides a message for oplog metadata that's compatible with gorm
type Metadata struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// @inject_tag: gorm:"primary_key"
Id uint32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty" gorm:"primary_key"`
// @inject_tag: gorm:"not_null;type:TIMESTAMP"
CreateTime *Timestamp `protobuf:"bytes,2,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty" gorm:"not_null;type:TIMESTAMP"` // @inject_tag: gorm:"not_nul
EntryId uint32 `protobuf:"varint,3,opt,name=entry_id,json=entryId,proto3" json:"entry_id,omitempty"`
// @inject_tag: gorm:"foreignkey:EntryId"
Entry *Entry `protobuf:"bytes,4,opt,name=entry,proto3" json:"entry,omitempty" gorm:"foreignkey:EntryId"`
// @inject_tag: gorm:"not_null"
Key string `protobuf:"bytes,5,opt,name=key,proto3" json:"key,omitempty" gorm:"not_null"`
// @inject_tag: gorm:"not_null"
Value string `protobuf:"bytes,6,opt,name=value,proto3" json:"value,omitempty" gorm:"not_null"`
}
func (x *Metadata) Reset() {
*x = Metadata{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_oplog_store_oplog_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Metadata) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Metadata) ProtoMessage() {}
func (x *Metadata) ProtoReflect() protoreflect.Message {
mi := &file_internal_oplog_store_oplog_proto_msgTypes[2]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Metadata.ProtoReflect.Descriptor instead.
func (*Metadata) Descriptor() ([]byte, []int) {
return file_internal_oplog_store_oplog_proto_rawDescGZIP(), []int{2}
}
func (x *Metadata) GetId() uint32 {
if x != nil {
return x.Id
}
return 0
}
func (x *Metadata) GetCreateTime() *Timestamp {
if x != nil {
return x.CreateTime
}
return nil
}
func (x *Metadata) GetEntryId() uint32 {
if x != nil {
return x.EntryId
}
return 0
}
func (x *Metadata) GetEntry() *Entry {
if x != nil {
return x.Entry
}
return nil
}
func (x *Metadata) GetKey() string {
if x != nil {
return x.Key
}
return ""
}
func (x *Metadata) GetValue() string {
if x != nil {
return x.Value
}
return ""
}
// Ticket provides a message for oplog tickets that's compatible with gorm
type Ticket struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// @inject_tag: gorm:"primary_key"
Id uint32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty" gorm:"primary_key"`
// @inject_tag: gorm:"not_null;type:TIMESTAMP"
CreateTime *Timestamp `protobuf:"bytes,2,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty" gorm:"not_null;type:TIMESTAMP"`
// @inject_tag: gorm:"not_null;type:TIMESTAMP"
UpdateTime *Timestamp `protobuf:"bytes,3,opt,name=update_time,json=updateTime,proto3" json:"update_time,omitempty" gorm:"not_null;type:TIMESTAMP"`
// @inject_tat: gorm:"not_null"
Name string `protobuf:"bytes,6,opt,name=name,proto3" json:"name,omitempty"`
// @inject_tat: gorm:"not_null;default:'1'"
Version uint32 `protobuf:"varint,7,opt,name=version,proto3" json:"version,omitempty"`
}
func (x *Ticket) Reset() {
*x = Ticket{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_oplog_store_oplog_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Ticket) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Ticket) ProtoMessage() {}
func (x *Ticket) ProtoReflect() protoreflect.Message {
mi := &file_internal_oplog_store_oplog_proto_msgTypes[3]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Ticket.ProtoReflect.Descriptor instead.
func (*Ticket) Descriptor() ([]byte, []int) {
return file_internal_oplog_store_oplog_proto_rawDescGZIP(), []int{3}
}
func (x *Ticket) GetId() uint32 {
if x != nil {
return x.Id
}
return 0
}
func (x *Ticket) GetCreateTime() *Timestamp {
if x != nil {
return x.CreateTime
}
return nil
}
func (x *Ticket) GetUpdateTime() *Timestamp {
if x != nil {
return x.UpdateTime
}
return nil
}
func (x *Ticket) GetName() string {
if x != nil {
return x.Name
}
return ""
}
func (x *Ticket) GetVersion() uint32 {
if x != nil {
return x.Version
}
return 0
}
var File_internal_oplog_store_oplog_proto protoreflect.FileDescriptor
var file_internal_oplog_store_oplog_proto_rawDesc = []byte{
0x0a, 0x20, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x6f, 0x70, 0x6c, 0x6f, 0x67,
0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2f, 0x6f, 0x70, 0x6c, 0x6f, 0x67, 0x2e, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x12, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x77, 0x61,
0x74, 0x63, 0x68, 0x74, 0x6f, 0x77, 0x65, 0x72, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c,
0x6c, 0x65, 0x72, 0x2e, 0x6f, 0x70, 0x6c, 0x6f, 0x67, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e,
0x76, 0x31, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f,
0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x22, 0x45, 0x0a, 0x09, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70,
0x12, 0x38, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x01, 0x20,
0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52,
0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x22, 0xfa, 0x02, 0x0a, 0x05, 0x45,
0x6e, 0x74, 0x72, 0x79, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d,
0x52, 0x02, 0x69, 0x64, 0x12, 0x5a, 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74,
0x69, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x39, 0x2e, 0x68, 0x61, 0x73, 0x68,
0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x77, 0x61, 0x74, 0x63, 0x68, 0x74, 0x6f, 0x77, 0x65, 0x72,
0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x6f, 0x70, 0x6c, 0x6f,
0x67, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73,
0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65,
0x12, 0x5a, 0x0a, 0x0b, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18,
0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x39, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72,
0x70, 0x2e, 0x77, 0x61, 0x74, 0x63, 0x68, 0x74, 0x6f, 0x77, 0x65, 0x72, 0x2e, 0x63, 0x6f, 0x6e,
0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x6f, 0x70, 0x6c, 0x6f, 0x67, 0x2e, 0x73, 0x74,
0x6f, 0x72, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70,
0x52, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07,
0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x76,
0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x25, 0x0a, 0x0e, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67,
0x61, 0x74, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d,
0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x54, 0x0a,
0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32,
0x38, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x77, 0x61, 0x74, 0x63,
0x68, 0x74, 0x6f, 0x77, 0x65, 0x72, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65,
0x72, 0x2e, 0x6f, 0x70, 0x6c, 0x6f, 0x67, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x76, 0x31,
0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64,
0x61, 0x74, 0x61, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x07, 0x20, 0x01, 0x28,
0x0c, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x22, 0x86, 0x02, 0x0a, 0x08, 0x4d, 0x65, 0x74, 0x61,
0x64, 0x61, 0x74, 0x61, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d,
0x52, 0x02, 0x69, 0x64, 0x12, 0x5a, 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74,
0x69, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x39, 0x2e, 0x68, 0x61, 0x73, 0x68,
0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x77, 0x61, 0x74, 0x63, 0x68, 0x74, 0x6f, 0x77, 0x65, 0x72,
0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x6f, 0x70, 0x6c, 0x6f,
0x67, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73,
0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65,
0x12, 0x19, 0x0a, 0x08, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01,
0x28, 0x0d, 0x52, 0x07, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x49, 0x64, 0x12, 0x4b, 0x0a, 0x05, 0x65,
0x6e, 0x74, 0x72, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x35, 0x2e, 0x68, 0x61, 0x73,
0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x77, 0x61, 0x74, 0x63, 0x68, 0x74, 0x6f, 0x77, 0x65,
0x72, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x6f, 0x70, 0x6c,
0x6f, 0x67, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x6e, 0x74, 0x72,
0x79, 0x52, 0x05, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18,
0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61,
0x6c, 0x75, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65,
0x22, 0xfe, 0x01, 0x0a, 0x06, 0x54, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69,
0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x02, 0x69, 0x64, 0x12, 0x5a, 0x0a, 0x0b, 0x63,
0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b,
0x32, 0x39, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x77, 0x61, 0x74,
0x63, 0x68, 0x74, 0x6f, 0x77, 0x65, 0x72, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c,
0x65, 0x72, 0x2e, 0x6f, 0x70, 0x6c, 0x6f, 0x67, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x76,
0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x63, 0x72, 0x65,
0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x5a, 0x0a, 0x0b, 0x75, 0x70, 0x64, 0x61, 0x74,
0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x39, 0x2e, 0x68,
0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x77, 0x61, 0x74, 0x63, 0x68, 0x74, 0x6f,
0x77, 0x65, 0x72, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x6f,
0x70, 0x6c, 0x6f, 0x67, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69,
0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x54,
0x69, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28,
0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69,
0x6f, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f,
0x6e, 0x42, 0x3c, 0x5a, 0x3a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f,
0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x77, 0x61, 0x74, 0x63, 0x68, 0x74,
0x6f, 0x77, 0x65, 0x72, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x6f, 0x70,
0x6c, 0x6f, 0x67, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x3b, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x62,
0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
file_internal_oplog_store_oplog_proto_rawDescOnce sync.Once
file_internal_oplog_store_oplog_proto_rawDescData = file_internal_oplog_store_oplog_proto_rawDesc
)
func file_internal_oplog_store_oplog_proto_rawDescGZIP() []byte {
file_internal_oplog_store_oplog_proto_rawDescOnce.Do(func() {
file_internal_oplog_store_oplog_proto_rawDescData = protoimpl.X.CompressGZIP(file_internal_oplog_store_oplog_proto_rawDescData)
})
return file_internal_oplog_store_oplog_proto_rawDescData
}
var file_internal_oplog_store_oplog_proto_msgTypes = make([]protoimpl.MessageInfo, 4)
var file_internal_oplog_store_oplog_proto_goTypes = []interface{}{
(*Timestamp)(nil), // 0: hashicorp.watchtower.controller.oplog.store.v1.Timestamp
(*Entry)(nil), // 1: hashicorp.watchtower.controller.oplog.store.v1.Entry
(*Metadata)(nil), // 2: hashicorp.watchtower.controller.oplog.store.v1.Metadata
(*Ticket)(nil), // 3: hashicorp.watchtower.controller.oplog.store.v1.Ticket
(*timestamp.Timestamp)(nil), // 4: google.protobuf.Timestamp
}
var file_internal_oplog_store_oplog_proto_depIdxs = []int32{
4, // 0: hashicorp.watchtower.controller.oplog.store.v1.Timestamp.timestamp:type_name -> google.protobuf.Timestamp
0, // 1: hashicorp.watchtower.controller.oplog.store.v1.Entry.create_time:type_name -> hashicorp.watchtower.controller.oplog.store.v1.Timestamp
0, // 2: hashicorp.watchtower.controller.oplog.store.v1.Entry.update_time:type_name -> hashicorp.watchtower.controller.oplog.store.v1.Timestamp
2, // 3: hashicorp.watchtower.controller.oplog.store.v1.Entry.metadata:type_name -> hashicorp.watchtower.controller.oplog.store.v1.Metadata
0, // 4: hashicorp.watchtower.controller.oplog.store.v1.Metadata.create_time:type_name -> hashicorp.watchtower.controller.oplog.store.v1.Timestamp
1, // 5: hashicorp.watchtower.controller.oplog.store.v1.Metadata.entry:type_name -> hashicorp.watchtower.controller.oplog.store.v1.Entry
0, // 6: hashicorp.watchtower.controller.oplog.store.v1.Ticket.create_time:type_name -> hashicorp.watchtower.controller.oplog.store.v1.Timestamp
0, // 7: hashicorp.watchtower.controller.oplog.store.v1.Ticket.update_time:type_name -> hashicorp.watchtower.controller.oplog.store.v1.Timestamp
8, // [8:8] is the sub-list for method output_type
8, // [8:8] is the sub-list for method input_type
8, // [8:8] is the sub-list for extension type_name
8, // [8:8] is the sub-list for extension extendee
0, // [0:8] is the sub-list for field type_name
}
func init() { file_internal_oplog_store_oplog_proto_init() }
func file_internal_oplog_store_oplog_proto_init() {
if File_internal_oplog_store_oplog_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_internal_oplog_store_oplog_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Timestamp); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_internal_oplog_store_oplog_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Entry); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_internal_oplog_store_oplog_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Metadata); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_internal_oplog_store_oplog_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Ticket); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_internal_oplog_store_oplog_proto_rawDesc,
NumEnums: 0,
NumMessages: 4,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_internal_oplog_store_oplog_proto_goTypes,
DependencyIndexes: file_internal_oplog_store_oplog_proto_depIdxs,
MessageInfos: file_internal_oplog_store_oplog_proto_msgTypes,
}.Build()
File_internal_oplog_store_oplog_proto = out.File
file_internal_oplog_store_oplog_proto_rawDesc = nil
file_internal_oplog_store_oplog_proto_goTypes = nil
file_internal_oplog_store_oplog_proto_depIdxs = nil
}
+82
View File
@@ -0,0 +1,82 @@
// Code generated by protoc-gen-go-json. DO NOT EDIT.
// source: oplog.proto
package store
import (
"bytes"
"github.com/golang/protobuf/jsonpb"
)
// MarshalJSON implements json.Marshaler
func (msg *Timestamp) MarshalJSON() ([]byte, error) {
var buf bytes.Buffer
err := (&jsonpb.Marshaler{
EnumsAsInts: false,
EmitDefaults: false,
OrigName: false,
}).Marshal(&buf, msg)
return buf.Bytes(), err
}
// UnmarshalJSON implements json.Unmarshaler
func (msg *Timestamp) UnmarshalJSON(b []byte) error {
return (&jsonpb.Unmarshaler{
AllowUnknownFields: false,
}).Unmarshal(bytes.NewReader(b), msg)
}
// MarshalJSON implements json.Marshaler
func (msg *Entry) MarshalJSON() ([]byte, error) {
var buf bytes.Buffer
err := (&jsonpb.Marshaler{
EnumsAsInts: false,
EmitDefaults: false,
OrigName: false,
}).Marshal(&buf, msg)
return buf.Bytes(), err
}
// UnmarshalJSON implements json.Unmarshaler
func (msg *Entry) UnmarshalJSON(b []byte) error {
return (&jsonpb.Unmarshaler{
AllowUnknownFields: false,
}).Unmarshal(bytes.NewReader(b), msg)
}
// MarshalJSON implements json.Marshaler
func (msg *Metadata) MarshalJSON() ([]byte, error) {
var buf bytes.Buffer
err := (&jsonpb.Marshaler{
EnumsAsInts: false,
EmitDefaults: false,
OrigName: false,
}).Marshal(&buf, msg)
return buf.Bytes(), err
}
// UnmarshalJSON implements json.Unmarshaler
func (msg *Metadata) UnmarshalJSON(b []byte) error {
return (&jsonpb.Unmarshaler{
AllowUnknownFields: false,
}).Unmarshal(bytes.NewReader(b), msg)
}
// MarshalJSON implements json.Marshaler
func (msg *Ticket) MarshalJSON() ([]byte, error) {
var buf bytes.Buffer
err := (&jsonpb.Marshaler{
EnumsAsInts: false,
EmitDefaults: false,
OrigName: false,
}).Marshal(&buf, msg)
return buf.Bytes(), err
}
// UnmarshalJSON implements json.Unmarshaler
func (msg *Ticket) UnmarshalJSON(b []byte) error {
return (&jsonpb.Unmarshaler{
AllowUnknownFields: false,
}).Unmarshal(bytes.NewReader(b), msg)
}
+73
View File
@@ -0,0 +1,73 @@
syntax = "proto3";
// define an oplog package within the watchtower controller
package hashicorp.watchtower.controller.oplog.store.v1;
option go_package = "github.com/hashicorp/watchtower/internal/oplog/store;store";
import "google/protobuf/timestamp.proto";
// Timestamp for oplog messages. We've defined a new local type wrapper
// of google.protobuf.Timestamp so we can implement sql.Scanner and sql.Valuer
// interfaces. See:
// https://golang.org/pkg/database/sql/#Scanner
// https://golang.org/pkg/database/sql/driver/#Valuer
message Timestamp {
google.protobuf.Timestamp timestamp = 1;
}
// Entry provides a message for oplog entries that's compatible with gorm
message Entry {
// @inject_tag: gorm:"primary_key"
uint32 id = 1;
// @inject_tag: gorm:"not_null;type:TIMESTAMP"
Timestamp create_time = 2;
// @inject_tag: gorm:"not_null;type:TIMESTAMP"
Timestamp update_time = 3;
// @inject_tag: gorm:"not_null"
string version = 4;
// @inject_tat: gorm:"not_null"
string aggregate_name = 5;
// one to many relationship
// @inject_tag: gorm:"foreignkey:entry_id"
repeated Metadata metadata = 6;
// @inject_tat: gorm:"not_null"
bytes data = 7;
}
// Metadata provides a message for oplog metadata that's compatible with gorm
message Metadata {
// @inject_tag: gorm:"primary_key"
uint32 id = 1;
// @inject_tag: gorm:"not_null;type:TIMESTAMP"
Timestamp create_time = 2;
// @inject_tag: gorm:"not_nul
uint32 entry_id = 3;
// @inject_tag: gorm:"foreignkey:EntryId"
Entry entry = 4;
// @inject_tag: gorm:"not_null"
string key = 5;
// @inject_tag: gorm:"not_null"
string value = 6;
}
// Ticket provides a message for oplog tickets that's compatible with gorm
message Ticket {
// @inject_tag: gorm:"primary_key"
uint32 id = 1;
// @inject_tag: gorm:"not_null;type:TIMESTAMP"
Timestamp create_time = 2;
// @inject_tag: gorm:"not_null;type:TIMESTAMP"
Timestamp update_time = 3;
// @inject_tat: gorm:"not_null"
string name = 6;
// @inject_tat: gorm:"not_null;default:'1'"
uint32 version = 7;
}
+10
View File
@@ -0,0 +1,10 @@
package store
// TableName overrides the gorm database table for the store.Oplog messages
func (*Entry) TableName() string { return "oplog_entry" }
// TableName overrides the gorm database table for the store.Ticket messages
func (*Ticket) TableName() string { return "oplog_ticket" }
// TableName overrides the gorm database table for the store.Metadata messages
func (*Metadata) TableName() string { return "oplog_metadata" }
+73
View File
@@ -0,0 +1,73 @@
package store
import (
"reflect"
"testing"
"time"
"github.com/golang/protobuf/ptypes/timestamp"
"gotest.tools/assert"
)
func Test_TimestampValue(t *testing.T) {
t.Parallel()
t.Run("valid", func(t *testing.T) {
ts := Timestamp{Timestamp: &timestamp.Timestamp{Seconds: 0, Nanos: 0}}
v, err := ts.Value()
assert.NilError(t, err)
assert.Equal(t, v, utcDate(1970, 1, 1))
})
t.Run("valid nil ts", func(t *testing.T) {
var ts *Timestamp
v, err := ts.Value()
assert.NilError(t, err)
assert.Equal(t, v, nil)
})
t.Run("invalid ts", func(t *testing.T) {
ts := Timestamp{Timestamp: &timestamp.Timestamp{Seconds: maxValidSeconds, Nanos: 0}}
v, err := ts.Value()
assert.Check(t, err != nil)
assert.Equal(t, err.Error(), "timestamp: seconds:253402300800 after 10000-01-01")
assert.Equal(t, v, utcDate(10000, 1, 1))
})
}
func Test_TimestampScan(t *testing.T) {
t.Parallel()
t.Run("valid", func(t *testing.T) {
v := time.Unix(0, 0)
ts := Timestamp{}
err := ts.Scan(v)
assert.NilError(t, err)
assert.Check(t, reflect.DeepEqual(ts.Timestamp, &timestamp.Timestamp{Seconds: 0, Nanos: 0}))
})
t.Run("valid default time", func(t *testing.T) {
var v time.Time
ts := Timestamp{}
err := ts.Scan(v)
assert.NilError(t, err)
assert.Check(t, reflect.DeepEqual(ts.Timestamp, &timestamp.Timestamp{Seconds: -62135596800, Nanos: 0}))
})
t.Run("invalid type", func(t *testing.T) {
v := 1
ts := Timestamp{}
err := ts.Scan(v)
assert.Check(t, err != nil)
assert.Equal(t, err.Error(), "Not a protobuf Timestamp")
})
t.Run("invalid time", func(t *testing.T) {
v := time.Unix(maxValidSeconds, 0)
ts := Timestamp{}
err := ts.Scan(v)
assert.Check(t, err != nil)
assert.Equal(t, err.Error(), "error converting the timestamp: timestamp: seconds:253402300800 after 10000-01-01")
})
}
const maxValidSeconds = 253402300800
func utcDate(year, month, day int) time.Time {
return time.Date(year, time.Month(month), day, 0, 0, 0, 0, time.UTC)
}
+33
View File
@@ -0,0 +1,33 @@
package store
import (
"database/sql/driver"
"errors"
"fmt"
"time"
"github.com/golang/protobuf/ptypes"
)
// Scan supports Timestamps for oplogs
func (ts *Timestamp) Scan(value interface{}) error {
switch t := value.(type) {
case time.Time:
var err error
ts.Timestamp, err = ptypes.TimestampProto(t) // google proto version
if err != nil {
return fmt.Errorf("error converting the timestamp: %w", err)
}
default:
return errors.New("Not a protobuf Timestamp")
}
return nil
}
// Value supports Timestamps for oplogs
func (ts *Timestamp) Value() (driver.Value, error) {
if ts == nil {
return nil, nil
}
return ptypes.Timestamp(ts.Timestamp)
}
+110
View File
@@ -0,0 +1,110 @@
package oplog
import (
"errors"
"fmt"
"github.com/hashicorp/watchtower/internal/oplog/store"
"github.com/jinzhu/gorm"
)
var (
ErrTicketNotFound = errors.New("ticket not found")
ErrTicketAlreadyRedeemed = errors.New("ticket already redeemed")
ErrTicketRedeeming = errors.New("error trying to redeem ticket")
)
const DefaultAggregateName = "global"
// Ticketer provides an interface to storage for Tickets, so you can easily substitute your own ticketer
type Ticketer interface {
// InitTicket initializes a ticket. You MUST initialize a ticket in its own transaction (and commit it),
// before you GetTicket in a later transaction to write to the oplog. InitTicket will check to see if
// the ticket has already been initialized before creating a new one.
InitTicket(aggregateName string) error
// GetTicket returns a ticket for the specified name. You MUST GetTicket in the same transaction
// that you're using to write to the database tables. Names allow us to shard tickets around domain root names
GetTicket(aggregateName string) (*store.Ticket, error)
Redeem(ticket *store.Ticket) error
}
// GormTicketer uses a gorm DB connection for ticket storage
type GormTicketer struct {
tx *gorm.DB
withAggregateNames bool
}
// NewGormTicketer creates a new ticketer that uses gorm for storage
func NewGormTicketer(tx *gorm.DB, opt ...Option) (*GormTicketer, error) {
if tx == nil {
return nil, errors.New("tx is nil")
}
opts := GetOpts(opt...)
enableAggregateNames := opts[optionWithAggregateNames].(bool)
return &GormTicketer{tx: tx, withAggregateNames: enableAggregateNames}, nil
}
// GetTicket returns a ticket for the specified name. You MUST GetTicket in the same transaction
// that you're using to write to the database tables. Names allow us to shard tickets around domain root names
func (ticketer *GormTicketer) GetTicket(aggregateName string) (*store.Ticket, error) {
if aggregateName == "" {
return nil, errors.New("bad ticket name")
}
name := DefaultAggregateName
if ticketer.withAggregateNames {
name = aggregateName
}
ticket := store.Ticket{}
if err := ticketer.tx.First(&ticket, store.Ticket{Name: name}).Error; err != nil {
if gorm.IsRecordNotFoundError(err) {
return nil, ErrTicketNotFound
}
return nil, fmt.Errorf("error retreiving ticket from storage: %w", err)
}
return &ticket, nil
}
// InitTicket initializes a ticket. You MUST initialize a ticket in its own transaction (and commit it),
// before you GetTicket in a later transaction to write to the oplog. InitTicket will check to see if
// the ticket has already been initialized before creating a new one.
func (ticketer *GormTicketer) InitTicket(aggregateName string) error {
// no existing ticket found, so let's initialize a new one
if aggregateName == "" {
return fmt.Errorf("bad ticket name")
}
// check to see if a ticket has already been initialized
existingTicket, err := ticketer.GetTicket(aggregateName)
if err == nil && existingTicket != nil {
return nil // found an existing intialized ticket without errors
}
if err != ErrTicketNotFound {
return fmt.Errorf("error retreiving ticket from storage: %w", err)
}
name := DefaultAggregateName
if ticketer.withAggregateNames {
name = aggregateName
}
newTicket := store.Ticket{Name: name, Version: 1}
if err := ticketer.tx.Create(&newTicket).Error; err != nil {
return fmt.Errorf("error creating ticket in storage: %w", err)
}
return nil
}
// Redeem will attempt to redeem the ticket. If the ticket version has already been used, then an error is returned
func (ticketer *GormTicketer) Redeem(t *store.Ticket) error {
if t == nil {
return errors.New("ticket is nil")
}
tx := ticketer.tx.Model(t).Where("version = ?", t.Version).Update("version", t.Version+1)
if tx.Error != nil {
return ErrTicketRedeeming
}
if tx.RowsAffected != 1 {
return ErrTicketAlreadyRedeemed
}
return nil
}
+154
View File
@@ -0,0 +1,154 @@
package oplog
import (
"testing"
"github.com/hashicorp/go-uuid"
"gotest.tools/assert"
)
// Test_NewGormTicketer provides unit tests for creating a Gorm ticketer
func Test_NewGormTicketer(t *testing.T) {
t.Parallel()
startTest()
cleanup, url := setup(t)
defer cleanup()
defer completeTest() // must come after the "defer cleanup()"
db, err := test_dbconn(url)
assert.NilError(t, err)
defer db.Close()
t.Run("valid", func(t *testing.T) {
ticketer, err := NewGormTicketer(db, WithAggregateNames(true))
assert.NilError(t, err)
assert.Assert(t, ticketer != nil)
})
t.Run("bad db", func(t *testing.T) {
_, err := NewGormTicketer(nil, WithAggregateNames(true))
assert.Equal(t, err.Error(), "tx is nil")
})
}
// Test_GetTicket provides unit tests for getting oplog.Tickets
func Test_GetTicket(t *testing.T) {
startTest()
t.Parallel()
cleanup, url := setup(t)
defer cleanup()
defer completeTest() // must come after the "defer cleanup()"
db, err := test_dbconn(url)
assert.NilError(t, err)
defer db.Close()
ticketName, err := uuid.GenerateUUID()
assert.NilError(t, err)
ticketer, err := NewGormTicketer(db, WithAggregateNames(true))
assert.NilError(t, err)
err = ticketer.InitTicket(ticketName)
assert.NilError(t, err)
t.Run("valid", func(t *testing.T) {
ticket, err := ticketer.GetTicket(ticketName)
assert.NilError(t, err)
assert.Equal(t, ticket.Name, ticketName)
assert.Check(t, ticket.Version != 0)
})
t.Run("no name", func(t *testing.T) {
ticket, err := ticketer.GetTicket("")
assert.Equal(t, err.Error(), "bad ticket name")
assert.Check(t, ticket == nil)
})
}
// Test_InitTicket provides unit tests for initializing tickets
func Test_InitTicket(t *testing.T) {
startTest()
t.Parallel()
cleanup, url := setup(t)
defer cleanup()
defer completeTest() // must come after the "defer cleanup()"
db, err := test_dbconn(url)
assert.NilError(t, err)
defer db.Close()
ticketName, err := uuid.GenerateUUID()
assert.NilError(t, err)
ticketer, err := NewGormTicketer(db, WithAggregateNames(true))
assert.NilError(t, err)
t.Run("valid", func(t *testing.T) {
err = ticketer.InitTicket(ticketName)
assert.NilError(t, err)
})
t.Run("no name", func(t *testing.T) {
err = ticketer.InitTicket("")
assert.Equal(t, err.Error(), "bad ticket name")
})
}
// Test_Redeem provides unit tests for redeeming tickets
func Test_Redeem(t *testing.T) {
startTest()
t.Parallel()
cleanup, url := setup(t)
defer cleanup()
defer completeTest() // must come after the "defer cleanup()"
db, err := test_dbconn(url)
assert.NilError(t, err)
defer db.Close()
ticketName, err := uuid.GenerateUUID()
assert.NilError(t, err)
ticketer, err := NewGormTicketer(db, WithAggregateNames(true))
assert.NilError(t, err)
// in it's own transaction, init the ticket
_ = ticketer.InitTicket(ticketName)
t.Run("valid", func(t *testing.T) {
tx := db.Begin()
ticketer, err := NewGormTicketer(tx, WithAggregateNames(true))
assert.NilError(t, err)
ticket, err := ticketer.GetTicket(ticketName)
assert.NilError(t, err)
err = ticketer.Redeem(ticket)
assert.NilError(t, err)
tx.Commit()
})
t.Run("nil ticket", func(t *testing.T) {
tx := db.Begin()
ticketer, err := NewGormTicketer(tx, WithAggregateNames(true))
assert.NilError(t, err)
err = ticketer.Redeem(nil)
assert.Equal(t, err.Error(), "ticket is nil")
tx.Commit()
})
t.Run("detect two redemptions in separate concurrent transactions", func(t *testing.T) {
tx := db.Begin()
ticketer, err := NewGormTicketer(tx, WithAggregateNames(true))
assert.NilError(t, err)
ticket, err := ticketer.GetTicket(ticketName)
assert.NilError(t, err)
secondTx := db.Begin()
secondTicketer, err := NewGormTicketer(secondTx, WithAggregateNames(true))
assert.NilError(t, err)
secondTicket, err := secondTicketer.GetTicket(ticketName)
assert.NilError(t, err)
err = ticketer.Redeem(ticket)
assert.NilError(t, err)
tx.Commit()
err = secondTicketer.Redeem(secondTicket)
assert.Equal(t, err.Error(), "ticket already redeemed")
})
}
+78
View File
@@ -0,0 +1,78 @@
package oplog
import (
"errors"
fmt "fmt"
"reflect"
)
// TypeCatalog is an abstraction for dealing with oplog data and their underlying types
type TypeCatalog map[string]reflect.Type
// Type provides the ability to associate an interface with a Type.Name which
// will decouple the interface from it's reflection type string, so you can
// refactor the type name without breaking the catalog
type Type struct {
// Interface of the type
Interface interface{}
// Name for the interface
Name string
}
// NewTypeCatalog creates a catalog with the types you pass in
func NewTypeCatalog(withTypes ...Type) (*TypeCatalog, error) {
reg := TypeCatalog{}
for _, t := range withTypes {
if t == (Type{}) {
return nil, errors.New("error type is {} (in NewTypeCatalog)")
}
if err := reg.Set(t.Interface, t.Name); err != nil {
return nil, fmt.Errorf("error setting the type: %w (in NewTypeCatalog)", err)
}
}
return &reg, nil
}
// GetTypeName returns the interface's name from the catalog
func (t *TypeCatalog) GetTypeName(i interface{}) (string, error) {
if i == nil {
return "", errors.New("error interface parameter is nil for GetTypeName")
}
if reflect.ValueOf(i).Kind() != reflect.Ptr {
return "", errors.New("error interface parameter must to be a pointer for GetTypeName")
}
interfaceType := reflect.TypeOf(i)
for name, t := range *t {
if t == interfaceType {
return name, nil
}
}
return "", fmt.Errorf("error unknown name for interface: %T", i)
}
// Set creates an entry in the catalog for the interface
func (t TypeCatalog) Set(i interface{}, typeName string) error {
if i == nil {
return errors.New("error interface parameter is nil for Set")
}
if reflect.ValueOf(i).Kind() != reflect.Ptr {
return errors.New("error interface parameter must to be a pointer for Set")
}
if typeName == "" {
return errors.New("typeName is an empty string for Set")
}
t[typeName] = reflect.TypeOf(i)
return nil
}
// Get retrieves the interface via a name
func (t TypeCatalog) Get(typeName string) (interface{}, error) {
if typeName == "" {
return nil, errors.New("error typeName is empty string for Get")
}
if typ, ok := t[typeName]; ok {
return reflect.New(typ.Elem()).Elem().Addr().Interface(), nil
}
return nil, errors.New("error typeName is not found for Get")
}
+187
View File
@@ -0,0 +1,187 @@
package oplog
import (
"reflect"
"testing"
"github.com/hashicorp/watchtower/internal/oplog/oplog_test"
"gotest.tools/assert"
)
// Test_TypeCatalog provides basic red/green unit tests
func Test_TypeCatalog(t *testing.T) {
t.Parallel()
types, err := NewTypeCatalog(
Type{new(oplog_test.TestUser), "user"},
Type{new(oplog_test.TestCar), "car"},
Type{new(oplog_test.TestRental), "rental"},
)
assert.NilError(t, err)
name, err := types.GetTypeName(new(oplog_test.TestUser))
assert.NilError(t, err)
assert.Assert(t, name == "user")
_, err = types.GetTypeName(oplog_test.TestUser{})
assert.Assert(t, err != nil)
s := "string"
_, err = types.GetTypeName(&s)
assert.Assert(t, err != nil)
_, err = types.Get("unknown")
assert.Assert(t, err != nil)
}
// Test_NewTypeCatalog provides unit tests for NewTypeCatalog
func Test_NewTypeCatalog(t *testing.T) {
t.Parallel()
t.Run("valid", func(t *testing.T) {
types, err := NewTypeCatalog(
Type{new(oplog_test.TestUser), "user"},
Type{new(oplog_test.TestCar), "car"},
Type{new(oplog_test.TestRental), "rental"},
)
assert.NilError(t, err)
u, err := types.Get("user")
assert.NilError(t, err)
assert.Equal(t, reflect.TypeOf(u), reflect.TypeOf(new(oplog_test.TestUser)))
})
t.Run("missing Type.Name", func(t *testing.T) {
types, err := NewTypeCatalog(
Type{new(oplog_test.TestUser), ""},
)
assert.Check(t, types == nil)
assert.Check(t, err != nil)
assert.Equal(t, err.Error(), "error setting the type: typeName is an empty string for Set (in NewTypeCatalog)")
})
t.Run("missing Type.Interface", func(t *testing.T) {
types, err := NewTypeCatalog(
Type{nil, ""},
)
assert.Check(t, types == nil)
assert.Check(t, err != nil)
assert.Equal(t, err.Error(), "error type is {} (in NewTypeCatalog)")
})
t.Run("empty Type", func(t *testing.T) {
types, err := NewTypeCatalog(
Type{},
)
assert.Check(t, types == nil)
assert.Check(t, err != nil)
assert.Equal(t, err.Error(), "error type is {} (in NewTypeCatalog)")
})
}
// Test_GetTypeName provides unit tests for GetTypeName
func Test_GetTypeName(t *testing.T) {
t.Parallel()
t.Run("valid", func(t *testing.T) {
types, err := NewTypeCatalog(
Type{new(oplog_test.TestUser), "user"},
Type{new(oplog_test.TestCar), "car"},
)
assert.NilError(t, err)
n, err := types.GetTypeName(new(oplog_test.TestUser))
assert.NilError(t, err)
assert.Equal(t, n, "user")
})
t.Run("bad name", func(t *testing.T) {
types, err := NewTypeCatalog(
Type{new(oplog_test.TestUser), "user"},
)
assert.NilError(t, err)
n, err := types.GetTypeName(new(oplog_test.TestCar))
assert.Check(t, err != nil)
assert.Equal(t, n, "")
assert.Equal(t, err.Error(), "error unknown name for interface: *oplog_test.TestCar")
})
t.Run("nil interface", func(t *testing.T) {
types, err := NewTypeCatalog(
Type{new(oplog_test.TestUser), "user"},
)
assert.NilError(t, err)
n, err := types.GetTypeName(nil)
assert.Check(t, err != nil)
assert.Equal(t, n, "")
assert.Equal(t, err.Error(), "error interface parameter is nil for GetTypeName")
})
}
// Test_Get provides unit tests for Get
func Test_Get(t *testing.T) {
t.Parallel()
t.Run("valid", func(t *testing.T) {
types, err := NewTypeCatalog(
Type{new(oplog_test.TestUser), "user"},
Type{new(oplog_test.TestCar), "car"},
)
assert.NilError(t, err)
u, err := types.Get("user")
assert.NilError(t, err)
assert.Equal(t, reflect.TypeOf(u), reflect.TypeOf(new(oplog_test.TestUser)))
})
t.Run("bad name", func(t *testing.T) {
types, err := NewTypeCatalog(
Type{new(oplog_test.TestUser), "user"},
)
assert.NilError(t, err)
n, err := types.Get("car")
assert.Check(t, err != nil)
assert.Equal(t, n, nil)
assert.Equal(t, err.Error(), "error typeName is not found for Get")
})
t.Run("bad typeName", func(t *testing.T) {
types, err := NewTypeCatalog(
Type{new(oplog_test.TestUser), "user"},
)
assert.NilError(t, err)
n, err := types.Get("")
assert.Check(t, err != nil)
assert.Equal(t, n, nil)
assert.Equal(t, err.Error(), "error typeName is empty string for Get")
})
}
// Test_Set provides unit tests for Set
func Test_Set(t *testing.T) {
t.Parallel()
t.Run("valid", func(t *testing.T) {
types, err := NewTypeCatalog()
assert.NilError(t, err)
u := new(oplog_test.TestUser)
err = types.Set(u, "user")
assert.NilError(t, err)
assert.Equal(t, reflect.TypeOf(u), reflect.TypeOf(new(oplog_test.TestUser)))
})
t.Run("bad name", func(t *testing.T) {
types, err := NewTypeCatalog()
assert.NilError(t, err)
u := new(oplog_test.TestUser)
err = types.Set(u, "")
assert.Check(t, err != nil)
assert.Equal(t, err.Error(), "typeName is an empty string for Set")
assert.Assert(t, reflect.DeepEqual(types, &TypeCatalog{}))
})
t.Run("bad interface", func(t *testing.T) {
types, err := NewTypeCatalog()
assert.NilError(t, err)
err = types.Set(nil, "")
assert.Check(t, err != nil)
assert.Equal(t, err.Error(), "error interface parameter is nil for Set")
assert.Assert(t, reflect.DeepEqual(types, &TypeCatalog{}))
})
}
+101
View File
@@ -0,0 +1,101 @@
package oplog
import (
"errors"
"fmt"
"reflect"
"strings"
"github.com/jinzhu/gorm"
)
// Writer interface for Entries
type Writer interface {
// Create the entry
Create(interface{}) error
// Update the entry using the fieldMaskPaths, which are Paths from field_mask.proto
Update(entry interface{}, fieldMaskPaths []string) error
// Delete the entry
Delete(interface{}) error
}
// GormWriter uses a gorm DB connection for writing
type GormWriter struct {
Tx *gorm.DB
}
// Create an object in storage
func (w *GormWriter) Create(i interface{}) error {
if w.Tx == nil {
return errors.New("create Tx is nil")
}
if i == nil {
return errors.New("create interface is nil")
}
if err := w.Tx.Create(i).Error; err != nil {
return fmt.Errorf("error creating: %w", err)
}
return nil
}
// Update an object in storage, if there's a fieldMask then only the field_mask.proto paths are updated, otherwise
// we will send every field to the DB.
func (w *GormWriter) Update(i interface{}, fieldMaskPaths []string) error {
if w.Tx == nil {
return errors.New("update Tx is nil")
}
if i == nil {
return errors.New("update interface is nil")
}
if len(fieldMaskPaths) == 0 {
if err := w.Tx.Save(i).Error; err != nil {
return fmt.Errorf("error updating: %w", err)
}
}
updateFields := map[string]interface{}{}
val := reflect.Indirect(reflect.ValueOf(i))
structTyp := val.Type()
for _, field := range fieldMaskPaths {
for i := 0; i < structTyp.NumField(); i++ {
// support for an embedded a gorm type
if structTyp.Field(i).Type.Kind() == reflect.Struct {
embType := structTyp.Field(i).Type
// check if the embedded field is exported via CanInterface()
if val.Field(i).CanInterface() {
embVal := reflect.Indirect(reflect.ValueOf(val.Field(i).Interface()))
for embFieldNum := 0; embFieldNum < embType.NumField(); embFieldNum++ {
if strings.EqualFold(embType.Field(embFieldNum).Name, field) {
updateFields[field] = embVal.Field(embFieldNum).Interface()
}
}
continue
}
}
// it's not an embedded type, so check if the field name matches
if strings.EqualFold(structTyp.Field(i).Name, field) {
updateFields[field] = val.Field(i).Interface()
}
}
}
if err := w.Tx.Model(i).Updates(updateFields).Error; err != nil {
return fmt.Errorf("error updating: %w", err)
}
return nil
}
// Deleting an object in storage
func (w *GormWriter) Delete(i interface{}) error {
if w.Tx == nil {
return errors.New("delete Tx is nil")
}
if i == nil {
return errors.New("delete interface is nil")
}
if err := w.Tx.Delete(i).Error; err != nil {
return fmt.Errorf("error deleting: %w", err)
}
return nil
}
+220
View File
@@ -0,0 +1,220 @@
package oplog
import (
"testing"
"github.com/hashicorp/go-uuid"
"github.com/hashicorp/watchtower/internal/oplog/oplog_test"
"github.com/jinzhu/gorm"
"gotest.tools/assert"
)
// Test_GormWriterCreate provides unit tests for GormWriter Create
func Test_GormWriterCreate(t *testing.T) {
t.Parallel()
startTest()
cleanup, url := setup(t)
defer cleanup()
defer completeTest() // must come after the "defer cleanup()"
db, err := test_dbconn(url)
assert.NilError(t, err)
defer db.Close()
t.Run("valid", func(t *testing.T) {
tx := db.Begin()
defer tx.Rollback()
w := GormWriter{tx}
id, err := uuid.GenerateUUID()
assert.NilError(t, err)
user := oplog_test.TestUser{
Name: "foo-" + id,
}
assert.NilError(t, w.Create(&user))
var foundUser oplog_test.TestUser
err = tx.Where("id = ?", user.Id).First(&foundUser).Error
assert.NilError(t, err)
assert.Equal(t, user.Name, foundUser.Name)
})
t.Run("nil tx", func(t *testing.T) {
w := GormWriter{nil}
id, err := uuid.GenerateUUID()
assert.NilError(t, err)
user := oplog_test.TestUser{
Name: "foo-" + id,
}
err = w.Create(&user)
assert.Check(t, err != nil)
assert.Equal(t, err.Error(), "create Tx is nil")
})
t.Run("nil model", func(t *testing.T) {
tx := db.Begin()
defer tx.Rollback()
w := GormWriter{tx}
err = w.Create(nil)
assert.Check(t, err != nil)
assert.Equal(t, err.Error(), "create interface is nil")
})
}
// Test_GormWriterDelete provides unit tests for GormWriter Delete
func Test_GormWriterDelete(t *testing.T) {
t.Parallel()
startTest()
cleanup, url := setup(t)
defer cleanup()
defer completeTest() // must come after the "defer cleanup()"
db, err := test_dbconn(url)
assert.NilError(t, err)
defer db.Close()
t.Run("valid", func(t *testing.T) {
tx := db.Begin()
defer tx.Rollback()
w := GormWriter{tx}
id, err := uuid.GenerateUUID()
assert.NilError(t, err)
user := oplog_test.TestUser{
Name: "foo-" + id,
}
assert.NilError(t, w.Create(&user))
var foundUser oplog_test.TestUser
err = tx.Where("id = ?", user.Id).First(&foundUser).Error
assert.NilError(t, err)
assert.NilError(t, w.Delete(&user))
err = tx.Where("id = ?", user.Id).First(&foundUser).Error
assert.Check(t, err != nil)
assert.Equal(t, err, gorm.ErrRecordNotFound)
})
t.Run("nil tx", func(t *testing.T) {
w := GormWriter{nil}
id, err := uuid.GenerateUUID()
assert.NilError(t, err)
user := oplog_test.TestUser{
Name: "foo-" + id,
}
err = w.Delete(&user)
assert.Check(t, err != nil)
assert.Equal(t, err.Error(), "delete Tx is nil")
})
t.Run("nil model", func(t *testing.T) {
tx := db.Begin()
defer tx.Rollback()
w := GormWriter{tx}
err = w.Delete(nil)
assert.Check(t, err != nil)
assert.Equal(t, err.Error(), "delete interface is nil")
})
}
// Test_GormWriterUpdate provides unit tests for GormWriter Update
func Test_GormWriterUpdate(t *testing.T) {
t.Parallel()
startTest()
cleanup, url := setup(t)
defer cleanup()
defer completeTest() // must come after the "defer cleanup()"
db, err := test_dbconn(url)
assert.NilError(t, err)
defer db.Close()
t.Run("valid no fieldmask", func(t *testing.T) {
tx := db.Begin()
defer tx.Rollback()
w := GormWriter{tx}
id, err := uuid.GenerateUUID()
assert.NilError(t, err)
user := oplog_test.TestUser{
Name: "foo-" + id,
}
assert.NilError(t, w.Create(&user))
var foundUser oplog_test.TestUser
err = tx.Where("id = ?", user.Id).First(&foundUser).Error
assert.NilError(t, err)
foundUser.Name = foundUser.Name + "_updated"
assert.NilError(t, w.Update(&foundUser, nil))
var updatedUser oplog_test.TestUser
err = tx.Where("id = ?", user.Id).First(&updatedUser).Error
assert.NilError(t, err)
assert.Equal(t, foundUser.Name, updatedUser.Name)
})
t.Run("valid with fieldmask", func(t *testing.T) {
tx := db.Begin()
defer tx.Rollback()
w := GormWriter{tx}
id, err := uuid.GenerateUUID()
assert.NilError(t, err)
user := oplog_test.TestUser{
Name: "foo-" + id,
}
assert.NilError(t, w.Create(&user))
var foundUser oplog_test.TestUser
err = tx.Where("id = ?", user.Id).First(&foundUser).Error
assert.NilError(t, err)
foundUser.Name = foundUser.Name + "_updated"
assert.NilError(t, w.Update(&foundUser, []string{"Name"}))
var updatedUser oplog_test.TestUser
err = tx.Where("id = ?", user.Id).First(&updatedUser).Error
assert.NilError(t, err)
assert.Equal(t, foundUser.Name, updatedUser.Name)
})
t.Run("valid with incorrect fieldmask", func(t *testing.T) {
tx := db.Begin()
defer tx.Rollback()
w := GormWriter{tx}
id, err := uuid.GenerateUUID()
assert.NilError(t, err)
user := oplog_test.TestUser{
Name: "foo-" + id,
}
assert.NilError(t, w.Create(&user))
var foundUser oplog_test.TestUser
err = tx.Where("id = ?", user.Id).First(&foundUser).Error
assert.NilError(t, err)
foundUser.Name = foundUser.Name + "_updated"
assert.NilError(t, w.Update(&foundUser, []string{"PhoneNumber"}))
var updatedUser oplog_test.TestUser
err = tx.Where("id = ?", user.Id).First(&updatedUser).Error
assert.NilError(t, err)
assert.Check(t, updatedUser.Name != foundUser.Name+"_updated")
})
t.Run("nil tx", func(t *testing.T) {
w := GormWriter{nil}
id, err := uuid.GenerateUUID()
assert.NilError(t, err)
user := oplog_test.TestUser{
Name: "foo-" + id,
}
err = w.Update(&user, nil)
assert.Check(t, err != nil)
assert.Equal(t, err.Error(), "update Tx is nil")
})
t.Run("nil model", func(t *testing.T) {
tx := db.Begin()
defer tx.Rollback()
w := GormWriter{tx}
err = w.Update(nil, nil)
assert.Check(t, err != nil)
assert.Equal(t, err.Error(), "update interface is nil")
})
}