diff --git a/cmd/frednode/main.go b/cmd/frednode/main.go index 79260c811f933e6b34f5641321a1846a52093ac6..d4d83ecfc4cc3d850b7c72586b18f26f3dfb0db5 100644 --- a/cmd/frednode/main.go +++ b/cmd/frednode/main.go @@ -3,9 +3,11 @@ package main import ( "github.com/BurntSushi/toml" "github.com/alecthomas/kingpin" + "github.com/go-errors/errors" "github.com/mmcloughlin/geohash" "github.com/rs/zerolog" "github.com/rs/zerolog/log" + "gitlab.tu-berlin.de/mcc-fred/fred/pkg/dynamoconnect" "gitlab.tu-berlin.de/mcc-fred/fred/pkg/interconnection" "os" "os/signal" @@ -45,6 +47,12 @@ type fredConfig struct { RemoteStore struct { Host string `toml:"host"` } `toml:"remotestore"` + DynamoDB struct { + Table string `toml:"table"` + Region string `toml:"region"` + PublicKey string `toml:"publickey"` + PrivateKey string `toml:"privatekey"` + } `toml:"dynamodb"` Bdb struct { Path string `toml:"path"` } `toml:"badgerdb"` @@ -60,16 +68,20 @@ var ( wsHost = kingpin.Flag("ws-host", "Host address of webserver.").String() wsSSL = kingpin.Flag("use-tls", "Use TLS/SSL to serve over HTTPS. Works only if host argument is a FQDN.").PlaceHolder("USE-SSL").Bool() zmqHost = kingpin.Flag("zmq-host", "(Publicly reachable) address of this zmq server.").String() - adaptor = kingpin.Flag("adaptor", "Storage adaptor, can be \"remote\", \"badgerdb\", \"memory\".").Enum("leveldb", "remote", "badgerdb", "memory") + adaptor = kingpin.Flag("adaptor", "Storage adaptor, can be \"remote\", \"badgerdb\", \"memory\", \"dynamo\".").Enum("remote", "badgerdb", "memory", "dynamo") logLevel = kingpin.Flag("log-level", "Log level, can be \"debug\", \"info\" ,\"warn\", \"error\", \"fatal\", \"panic\".").Enum("debug", "info", "warn", "errors", "fatal", "panic") handler = kingpin.Flag("handler", "Mode of log handler, can be \"dev\", \"prod\".").Enum("dev", "prod") remoteStorageHost = kingpin.Flag("remote-storage-host", "Host address of GRPC Server.").String() + dynamoTable = kingpin.Flag("dynamo-table", "AWS table for DynamoDB storage backend.").String() + dynamoRegion = kingpin.Flag("dynamo-region", "AWS region for DynamoDB storage backend.").String() + // TODO this should be a list of nodes. One node is enough, but if we want reliability we should accept multiple etcd nodes naseHost = kingpin.Flag("nase-host", "Host where the etcd-server runs").String() bdbPath = kingpin.Flag("badgerdb-path", "Path to the badgerdb database").String() ) func main() { + var err error kingpin.Version(apiversion) kingpin.HelpFlag.Short('h') @@ -121,6 +133,12 @@ func main() { if *remoteStorageHost != "" { fc.RemoteStore.Host = *remoteStorageHost } + if *dynamoTable != "" { + fc.DynamoDB.Table = *dynamoTable + } + if *dynamoRegion != "" { + fc.DynamoDB.Region = *dynamoRegion + } if *naseHost != "" { fc.NaSe.Host = *naseHost } @@ -181,6 +199,11 @@ func main() { store = badgerdb.NewMemory() case "remote": store = storage.NewClient(fc.RemoteStore.Host) + case "dynamo": + store, err = dynamoconnect.New(fc.DynamoDB.Table, fc.DynamoDB.Region) + if err != nil { + log.Fatal().Msgf("could not open a dynamo connection: %s", err.(*errors.Error).ErrorStack()) + } default: log.Fatal().Msg("unknown storage backend") } diff --git a/config.toml b/config.toml index ab58bd5eac5e9c7287d0db565a9579235494b25b..2d2cbecc6b726553b3ca1d3fac08b25946e026bc 100644 --- a/config.toml +++ b/config.toml @@ -17,6 +17,10 @@ adaptor = "badgerdb" [zmq] host = ":5555" +[dynamodb] +table = "fredtable" +region = "eu-central-1" + [remotestore] host = "localhost:1337" diff --git a/go.mod b/go.mod index d5462b4435ee3c0bea96fa60415dec5e74e11882..8e7de19dc8b28f492cbe0f93fa67a7dcd34061fa 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/alecthomas/kingpin v2.2.6+incompatible github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d // indirect + github.com/aws/aws-sdk-go v1.34.13 github.com/coreos/go-semver v0.3.0 // indirect github.com/dgraph-io/badger/v2 v2.0.3 github.com/gin-contrib/logger v0.0.2 @@ -17,9 +18,10 @@ require ( github.com/gogo/protobuf v1.3.1 // indirect github.com/golang/protobuf v1.4.1 github.com/google/uuid v1.1.1 // indirect + github.com/gusaul/go-dynamock v0.0.0-20200325102056-aaeeb0c0e9c1 github.com/mmcloughlin/geohash v0.9.0 github.com/rs/zerolog v1.17.2 - github.com/stretchr/testify v1.4.0 + github.com/stretchr/testify v1.5.1 github.com/syndtr/goleveldb v1.0.0 github.com/zeromq/goczmq v4.1.0+incompatible go.etcd.io/etcd v0.5.0-alpha.5.0.20200306183522-221f0cc107cb diff --git a/go.sum b/go.sum index 3f3913739e84d913d45d0a0a3502fde8deb8ab27..3d2eb76764c7ec3a929d66c3a985ea05e7577239 100644 --- a/go.sum +++ b/go.sum @@ -17,6 +17,8 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRF github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d h1:UQZhZ2O0vMHr2cI+DC1Mbh0TJxzA3RcLoMsFw+aXw7E= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/aws/aws-sdk-go v1.34.13 h1:wwNWSUh4FGJxXVOVVNj2lWI8wTe5hK8sGWlK7ziEcgg= +github.com/aws/aws-sdk-go v1.34.13/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0 h1:HWo1m869IqiPhD389kmkxeTalrjNbbJTC8LXupb+sl0= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= @@ -80,6 +82,7 @@ github.com/go-playground/locales v0.12.1 h1:2FITxuFt/xuCNP1Acdhv62OzaCiviiE4kotf github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM= github.com/go-playground/universal-translator v0.16.0 h1:X++omBR/4cE2MNg91AoC3rmGrCjJ8eAeUP/K/EKx4DM= github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY= +github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= @@ -125,10 +128,14 @@ github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92Bcuy github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.5 h1:UImYN5qQ8tuGpGE16ZmjvcTtTw24zw1QAp/SlnNrZhI= github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/gusaul/go-dynamock v0.0.0-20200325102056-aaeeb0c0e9c1 h1:lPA38imc4ThIPhJ10toIuYU2EQFfH8nlFGbPiydKLwk= +github.com/gusaul/go-dynamock v0.0.0-20200325102056-aaeeb0c0e9c1/go.mod h1:KfoAbLEFfaTk307snIG9ptFwiBHy4DRS4vrEi4Qn4VE= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jmespath/go-jmespath v0.3.0 h1:OS12ieG61fsCg5+qLJ+SsW9NicxNkg3b25OyT2yCeUc= +github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik= github.com/jonboulle/clockwork v0.1.0 h1:VKV+ZcuP6l3yW9doeqz6ziZGgcynBVQO+obU0+0hcPo= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= @@ -178,6 +185,8 @@ github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/9 github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= @@ -222,6 +231,8 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE= github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8 h1:ndzgwNDnKIqyCvHTXaCqh9KlOWKvBry6nuXMJmonVsE= @@ -286,6 +297,7 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200625001655-4c5254603344 h1:vGXIOMxbNfDTk/aXCmfdLgkrSV+Z2tcbze+pEc3v5W4= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= diff --git a/pkg/dynamoconnect/dynamoconnect.go b/pkg/dynamoconnect/dynamoconnect.go new file mode 100644 index 0000000000000000000000000000000000000000..8d028b0365de39fc3a1737cbde113f0bf7a5b733 --- /dev/null +++ b/pkg/dynamoconnect/dynamoconnect.go @@ -0,0 +1,428 @@ +package dynamoconnect + +import ( + "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute" + "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbiface" + "github.com/aws/aws-sdk-go/service/dynamodb/expression" + "github.com/rs/zerolog/log" + "strings" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/dynamodb" + "github.com/go-errors/errors" +) + +const ( + keyName = "Key" +) + +// Storage is a struct that saves all necessary information to access the database, in this case the session for DynamoDB and the table name. +type Storage struct { + dynamotable string + svc dynamodbiface.DynamoDBAPI +} + +// makeKeyName creates the internal DynamoDB key given a keygroup name and an id. +func makeKeyName(kgname string, id string) string { + return kgname + "/" + id +} + +// makeKeygroupKeyName creates the internal DynamoDB key given a keygroup name. +func makeKeygroupKeyName(kgname string) string { + return kgname + "/" +} + +// getKey returns the keygroup and id of a key. +func getKey(key string) (kg, id string) { + s := strings.Split(key, "/") + kg = s[0] + if len(s) > 1 { + id = s[1] + } + return +} + +// New creates a new Session for DynamoDB. +func New(table, region string) (s *Storage, err error) { + log.Debug().Msgf("creating a new dynamodb connection to table %s in region %s", table, region) + + log.Debug().Msg("Checked creds - OK!") + + sess := session.Must(session.NewSession(&aws.Config{ + Region: aws.String(region), + })) + + log.Debug().Msg("Created session - OK!") + + svc := dynamodb.New(sess) + + log.Debug().Msg("Created service - OK!") + + log.Debug().Msgf("Loading table %s...", table) + + // check if the table with that name even exists + // if not: error out + desc, err := svc.DescribeTable(&dynamodb.DescribeTableInput{ + TableName: aws.String(table), + }) + + if err != nil { + return nil, errors.New(err) + } + + log.Debug().Msgf("Checking table %s...", table) + + // check that the table has the correct fields (i.e. a primary hash key with name "key") for our use + if len(desc.Table.KeySchema) != 1 { + return nil, errors.Errorf("expected a single primary range key with name \"%s\" but go %d keys", keyName, len(desc.Table.KeySchema)) + } + + log.Debug().Msg("Checked table fields - OK!") + + if *(desc.Table.KeySchema[0].AttributeName) != keyName && *(desc.Table.KeySchema[0].KeyType) != dynamodb.KeyTypeHash { + return nil, errors.Errorf("expected the primary key to be named \"%s\" and be of type range but got %s with type %s", keyName, *(desc.Table.KeySchema[0].AttributeName), *(desc.Table.KeySchema[0].KeyType)) + } + + log.Debug().Msg("Checked table keys - OK!") + + return &Storage{ + dynamotable: table, + svc: dynamodbiface.DynamoDBAPI(svc), + }, nil +} + +// Close closes the underlying DynamoDB connection (no cleanup needed at the moment). +func (s *Storage) Close() error { + return nil +} + +// Read returns an item with the specified id from the specified keygroup. +func (s *Storage) Read(kg string, id string) (string, error) { + + key := makeKeyName(kg, id) + + result, err := s.svc.GetItem(&dynamodb.GetItemInput{ + Key: map[string]*dynamodb.AttributeValue{ + keyName: { + S: aws.String(key), + }, + }, + TableName: &s.dynamotable, + }) + + if err != nil { + return "", errors.New(err) + } + + if result.Item == nil { + return "", errors.Errorf("could not find item %s in keygroup %s", id, kg) + } + + Item := struct { + Key string + Value string + }{} + + err = dynamodbattribute.UnmarshalMap(result.Item, &Item) + if err != nil { + return "", errors.New(err) + } + + return Item.Value, nil + +} + +// ReadAll returns all items in the specified keygroup. +func (s *Storage) ReadAll(kg string) (map[string]string, error) { + items := make(map[string]string) + + key := makeKeygroupKeyName(kg) + + filt := expression.Name(keyName).BeginsWith(key) + + expr, err := expression.NewBuilder().WithFilter(filt).Build() + if err != nil { + return items, errors.New(err) + } + + params := &dynamodb.ScanInput{ + ExpressionAttributeNames: expr.Names(), + ExpressionAttributeValues: expr.Values(), + FilterExpression: expr.Filter(), + ProjectionExpression: expr.Projection(), + TableName: aws.String(s.dynamotable), + } + + // Make the DynamoDB Query API call + result, err := s.svc.Scan(params) + if err != nil { + return items, errors.New(err) + } + + for _, i := range result.Items { + + item := struct { + Key string + Value string + }{} + + err = dynamodbattribute.UnmarshalMap(i, &item) + + if err != nil { + return items, errors.New(err) + } + + if item.Key == key { + continue + } + + _, id := getKey(item.Key) + + items[id] = item.Value + + } + + return items, nil +} + +// IDs returns the keys of all items in the specified keygroup. +func (s *Storage) IDs(kg string) ([]string, error) { + var ids []string + + key := makeKeygroupKeyName(kg) + + filt := expression.Name(keyName).BeginsWith(key) + + expr, err := expression.NewBuilder().WithFilter(filt).Build() + if err != nil { + return ids, errors.New(err) + } + + params := &dynamodb.ScanInput{ + ExpressionAttributeNames: expr.Names(), + ExpressionAttributeValues: expr.Values(), + FilterExpression: expr.Filter(), + ProjectionExpression: expr.Projection(), + TableName: aws.String(s.dynamotable), + } + + // Make the DynamoDB Query API call + result, err := s.svc.Scan(params) + if err != nil { + return ids, errors.New(err) + } + + for _, i := range result.Items { + + item := struct { + Key string + }{} + + err = dynamodbattribute.UnmarshalMap(i, &item) + + if err != nil { + return ids, errors.New(err) + } + + if item.Key == key { + continue + } + + _, id := getKey(item.Key) + + ids = append(ids, id) + + } + + return ids, nil +} + +// Update updates the item with the specified id in the specified keygroup. +func (s *Storage) Update(kg, id, val string) error { + + key := makeKeyName(kg, id) + + Item := struct { + Key string + Value string + }{ + Key: key, + Value: val, + } + + av, err := dynamodbattribute.MarshalMap(Item) + + if err != nil { + return errors.New(err) + } + + input := &dynamodb.PutItemInput{ + Item: av, + TableName: aws.String(s.dynamotable), + } + + _, err = s.svc.PutItem(input) + if err != nil { + return errors.New(err) + } + + return nil +} + +// Delete deletes the item with the specified id from the specified keygroup. +func (s *Storage) Delete(kg string, id string) error { + key := makeKeyName(kg, id) + + input := &dynamodb.DeleteItemInput{ + TableName: aws.String(s.dynamotable), + Key: map[string]*dynamodb.AttributeValue{ + keyName: { + S: aws.String(key), + }, + }, + } + + _, err := s.svc.DeleteItem(input) + if err != nil { + return errors.New(err) + } + + return nil +} + +// Exists checks if the given data item exists in the dynamodb database. +func (s *Storage) Exists(kg string, id string) bool { + key := makeKeyName(kg, id) + + result, err := s.svc.GetItem(&dynamodb.GetItemInput{ + Key: map[string]*dynamodb.AttributeValue{ + keyName: { + S: aws.String(key), + }, + }, + TableName: &s.dynamotable, + }) + + if err != nil { + return false + } + + if result.Item == nil { + return false + } + + return true +} + +// ExistsKeygroup checks if the given keygroup exists in the DynamoDB database. +func (s *Storage) ExistsKeygroup(kg string) bool { + key := makeKeygroupKeyName(kg) + + result, err := s.svc.GetItem(&dynamodb.GetItemInput{ + Key: map[string]*dynamodb.AttributeValue{ + keyName: { + S: aws.String(key), + }, + }, + TableName: &s.dynamotable, + }) + + if err != nil { + return false + } + + if result.Item == nil { + return false + } + + return true +} + +// CreateKeygroup creates the given keygroup in the DynamoDB database. +func (s *Storage) CreateKeygroup(kg string) error { + key := makeKeygroupKeyName(kg) + + Item := struct { + Key string + Value string + }{ + Key: key, + Value: key, + } + + av, err := dynamodbattribute.MarshalMap(Item) + + if err != nil { + return errors.New(err) + } + + input := &dynamodb.PutItemInput{ + Item: av, + TableName: aws.String(s.dynamotable), + } + + _, err = s.svc.PutItem(input) + if err != nil { + return errors.New(err) + } + + return nil +} + +// DeleteKeygroup deletes the given keygroup from the DynamoDB database. +func (s *Storage) DeleteKeygroup(kg string) error { + + key := makeKeygroupKeyName(kg) + + filt := expression.Name(keyName).BeginsWith(key) + + expr, err := expression.NewBuilder().WithFilter(filt).Build() + if err != nil { + return errors.New(err) + } + + params := &dynamodb.ScanInput{ + ExpressionAttributeNames: expr.Names(), + ExpressionAttributeValues: expr.Values(), + FilterExpression: expr.Filter(), + ProjectionExpression: expr.Projection(), + TableName: aws.String(s.dynamotable), + } + + // Make the DynamoDB Query API call + result, err := s.svc.Scan(params) + if err != nil { + return errors.New(err) + } + + for _, i := range result.Items { + + item := struct { + Key string + }{} + + err = dynamodbattribute.UnmarshalMap(i, &item) + + if err != nil { + return errors.New(err) + } + + input := &dynamodb.DeleteItemInput{ + + Key: map[string]*dynamodb.AttributeValue{ + keyName: { + S: aws.String(item.Key), + }, + }, + TableName: aws.String(s.dynamotable), + } + + _, err := s.svc.DeleteItem(input) + if err != nil { + return errors.New(err) + } + } + + return nil +}