Skip to content

Commit

Permalink
Add FDB config service (#127)
Browse files Browse the repository at this point in the history
* add: fdb_conf service

* update: do not include fdb_conf service by default

* update: add license header

* update: add license header

* fix: ignore directories without tests

* update: move services/fdb_conf under services/fdb

* fix: remove all references to fdb_conf

* fix: lincese + go.sum
  • Loading branch information
sfc-gh-ssudakovich authored Jun 3, 2022
1 parent ddc75c1 commit b8a2cd4
Show file tree
Hide file tree
Showing 10 changed files with 654 additions and 100 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ require (
google.golang.org/grpc v1.46.2
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.2.0
google.golang.org/protobuf v1.28.0
gopkg.in/ini.v1 v1.66.4
)

require (
Expand Down
70 changes: 3 additions & 67 deletions go.sum

Large diffs are not rendered by default.

213 changes: 213 additions & 0 deletions services/fdb/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ func processLog(state *util.ExecuteState, index int, log *pb.Log, openFiles map[
func setup(f *flag.FlagSet) *subcommands.Commander {
c := client.SetupSubpackage(subPackage, f)
c.Register(&fdbCLICmd{}, "")
c.Register(&fdbConfCmd{}, "")

return c
}
Expand Down Expand Up @@ -2453,3 +2454,215 @@ func (r *fdbCLITssqCmd) Execute(ctx context.Context, f *flag.FlagSet, args ...in

return runFDBCLI(ctx, c, state, req, "tssq")
}

const fdbConfCLIPackage = "conf"

func setupConfCLI(f *flag.FlagSet) *subcommands.Commander {
c := client.SetupSubpackage(fdbConfCLIPackage, f)
c.Register(&fdbConfReadCmd{}, "")
c.Register(&fdbConfWriteCmd{}, "")
c.Register(&fdbConfDeleteCmd{}, "")

return c
}

type fdbConfCmd struct{}

func (*fdbConfCmd) Name() string { return fdbConfCLIPackage }
func (*fdbConfCmd) SetFlags(_ *flag.FlagSet) {}
func (*fdbConfCmd) Synopsis() string {
return "Read or update values in foundationdb conf file.\n" + client.GenerateSynopsis(setupConfCLI(flag.NewFlagSet("", flag.ContinueOnError)), 4)
}
func (p *fdbConfCmd) Usage() string {
return client.GenerateUsage(fdbConfCLIPackage, p.Synopsis())
}

func (p *fdbConfCmd) Execute(ctx context.Context, f *flag.FlagSet, args ...interface{}) subcommands.ExitStatus {
c := setupConfCLI(f)
return c.Execute(ctx, args...)
}

type fdbConfReadCmd struct {
path string
}

func (*fdbConfReadCmd) Name() string { return "read" }
func (*fdbConfReadCmd) Synopsis() string {
return "Read value from a section for a given key and return a response."
}
func (*fdbConfReadCmd) Usage() string {
return `read <section> <key> [--path <config>]:
Read a key from a section specified in a FDB config file.
Default location for config file is /etc/foundationdb/foundationdb.conf.
`
}

func (r *fdbConfReadCmd) SetFlags(f *flag.FlagSet) {
f.StringVar(&r.path, "path", "/etc/foundationdb/foundationdb.conf", "The absolute path to FDB config.")
}

func (r *fdbConfReadCmd) Execute(ctx context.Context, f *flag.FlagSet, args ...interface{}) subcommands.ExitStatus {
state := args[0].(*util.ExecuteState)
c := pb.NewConfClientProxy(state.Conn)

if len(f.Args()) != 2 {
fmt.Fprint(os.Stderr, "invalid number of parameters: specify section and key only")
return subcommands.ExitFailure
}

section := f.Args()[0]
key := f.Args()[1]

resp, err := c.ReadOneMany(ctx, &pb.ReadRequest{Location: &pb.Location{Section: section, Key: key, File: r.path}})
if err != nil {
// Emit this to every error file as it's not specific to a given target.
for _, e := range state.Err {
fmt.Fprintf(e, "fdb config read error: %v\n", err)
}

return subcommands.ExitFailure
}

retCode := subcommands.ExitSuccess
for r := range resp {
if r.Error != nil {
fmt.Fprintf(state.Err[r.Index], "fdb config read error: %v\n", r.Error)
retCode = subcommands.ExitFailure
}
}

return retCode
}

type fdbConfWriteCmd struct {
path string
}

func (*fdbConfWriteCmd) Name() string { return "write" }
func (*fdbConfWriteCmd) Synopsis() string {
return "Write a key-value pair to a section of an FDB config."
}

func (*fdbConfWriteCmd) Usage() string {
return `write <section> <key> <value> [--path <config>]:
Write a key-value pair to a section specified in a FDB config file.
Default location for config file is /etc/foundationdb/foundationdb.conf.
`
}

func (w *fdbConfWriteCmd) SetFlags(f *flag.FlagSet) {
f.StringVar(&w.path, "path", "/etc/foundationdb/foundationdb.conf", "The absolute path to FDB config.")
}

func (w *fdbConfWriteCmd) Execute(ctx context.Context, f *flag.FlagSet, args ...interface{}) subcommands.ExitStatus {
state := args[0].(*util.ExecuteState)
c := pb.NewConfClientProxy(state.Conn)

if len(f.Args()) != 3 {
fmt.Fprint(os.Stderr, "invalid number of parameters: specify section, key and value only")
return subcommands.ExitFailure
}

section, key, value := f.Args()[0], f.Args()[1], f.Args()[2]

resp, err := c.WriteOneMany(ctx, &pb.WriteRequest{
Location: &pb.Location{Section: section, Key: key, File: w.path},
Value: value,
})
if err != nil {
// Emit this to every error file as it's not specific to a given target.
for _, e := range state.Err {
fmt.Fprintf(e, "fdb config write error: %v\n", err)
}

return subcommands.ExitFailure
}

retCode := subcommands.ExitSuccess
for r := range resp {
if r.Error != nil {
fmt.Fprintf(state.Err[r.Index], "fdb config write error: %v\n", r.Error)
retCode = subcommands.ExitFailure
}
}

return retCode
}

type fdbConfDeleteCmd struct {
path string
deleteSection bool
}

func (*fdbConfDeleteCmd) Name() string { return "delete" }
func (*fdbConfDeleteCmd) Synopsis() string {
return "Delete key from a section or entire section"
}

func (*fdbConfDeleteCmd) Usage() string {
return `delete [--delete-section] <section> [<key>]:
Delete key from a section or entire section.
When entire section is deleted, '--delete-section' flag is mandatory but 'key' is optional.
Default location for config file is /etc/foundationdb/foundationdb.conf.
`
}

func (d *fdbConfDeleteCmd) SetFlags(f *flag.FlagSet) {
f.StringVar(&d.path, "path", "/etc/foundationdb/foundationdb.conf", "The absolute path to FDB config.")
f.BoolVar(&d.deleteSection, "delete-section", false, "Delete section safeguard.")
}

func (d *fdbConfDeleteCmd) Execute(ctx context.Context, f *flag.FlagSet, args ...interface{}) subcommands.ExitStatus {
state := args[0].(*util.ExecuteState)
c := pb.NewConfClientProxy(state.Conn)

if len(f.Args()) < 1 {
fmt.Fprint(os.Stderr, "invalid number of parameters: specify section name.")
return subcommands.ExitFailure
}

if len(f.Args()) > 2 {
fmt.Fprint(os.Stderr, "invalid number of parameters: specify section name and optional key only.")
return subcommands.ExitFailure
}

section := f.Args()[0]

var key string
if len(f.Args()) == 2 {
key = f.Args()[1]
}

if key == "" && !d.deleteSection {
fmt.Fprint(os.Stderr, "invalid parameters: `delete-section` must be set if `key` is empty.")
return subcommands.ExitFailure
}

if key != "" && d.deleteSection {
fmt.Fprint(os.Stderr, "invalid parameters: `delete-section` and `key` are mutually exclusive.")
return subcommands.ExitFailure
}

resp, err := c.DeleteOneMany(ctx, &pb.DeleteRequest{Location: &pb.Location{Section: section, Key: key, File: d.path}})
if err != nil {
// Emit this to every error file as it's not specific to a given target.
for _, e := range state.Err {
fmt.Fprintf(e, "fdb config delete error: %v\n", err)
}

return subcommands.ExitFailure
}

retCode := subcommands.ExitSuccess
for r := range resp {
if r.Error != nil {
fmt.Fprintf(state.Err[r.Index], "fdb config delete error: %v\n", r.Error)
retCode = subcommands.ExitFailure
}
}

return retCode
}
4 changes: 2 additions & 2 deletions services/fdb/fdb.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions services/fdb/fdb_grpc.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

131 changes: 131 additions & 0 deletions services/fdb/server/conf.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/* Copyright (c) 2019 Snowflake Inc. All rights reserved.
Licensed under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
*/

package server

import (
"context"
"io/ioutil"
"os"

"github.com/Snowflake-Labs/sansshell/services"
pb "github.com/Snowflake-Labs/sansshell/services/fdb"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"
"gopkg.in/ini.v1"
)

// TODO add section name validator https://apple.github.io/foundationdb/configuration.html#foundationdb-conf

type cserver struct {
}

func (s *cserver) Read(_ context.Context, req *pb.ReadRequest) (*pb.FdbConfResponse, error) {
cfg, err := ini.Load(req.Location.File)
if err != nil {
return nil, status.Errorf(codes.Internal, "could not load config file %s: %v", req.Location.File, err)
}

value := cfg.Section(req.Location.Section).Key(req.Location.Key).String()

return &pb.FdbConfResponse{Value: value}, nil
}

func (s *cserver) Write(_ context.Context, req *pb.WriteRequest) (*emptypb.Empty, error) {
section := req.Location.Section
if section == "" {
return nil, status.Error(codes.InvalidArgument, "section name can not be empty")
}

sectionKey := req.Location.Key
if sectionKey == "" {
return nil, status.Error(codes.InvalidArgument, "key name can not be empty")
}

sectionKeyVal := req.Value
// For now, disallow empty values since there are not usecases for that.
// But maybe, in the future, key = "" will be valid config, just not now
if sectionKeyVal == "" {
return nil, status.Error(codes.InvalidArgument, "key value can not be empty")
}

cfg, err := ini.Load(req.Location.File)
if err != nil {
return nil, status.Errorf(codes.Internal, "could not load config file %s: %v", req.Location.File, err)
}

cfg.Section(section).Key(sectionKey).SetValue(sectionKeyVal)

if err = atomicSaveTo(cfg, req.Location.File); err != nil {
return nil, status.Errorf(codes.Internal, "could not save config file %s: %v", req.Location.File, err)
}

return &emptypb.Empty{}, nil
}

func (s *cserver) Delete(_ context.Context, req *pb.DeleteRequest) (*emptypb.Empty, error) {
section := req.Location.Section
if section == "" {
return nil, status.Error(codes.InvalidArgument, "section name can not be empty")
}

cfg, err := ini.Load(req.Location.File)
if err != nil {
return nil, status.Errorf(codes.Internal, "could not load config file %s: %v", req.Location.File, err)
}

sectionKey := req.Location.Key
if sectionKey != "" {
cfg.Section(section).DeleteKey(sectionKey)
}
// TODO maybe use a sentinel for section deletion to avoid accidents?
if sectionKey == "" {
cfg.DeleteSection(section)
}

if err = atomicSaveTo(cfg, req.Location.File); err != nil {
return nil, status.Errorf(codes.Internal, "could not save config file %s: %v", req.Location.File, err)
}

return &emptypb.Empty{}, nil
}

func (s *cserver) Register(gs *grpc.Server) {
pb.RegisterConfServer(gs, s)
}

func init() {
services.RegisterSansShellService(&server{})
}

func atomicSaveTo(f *ini.File, filename string) error {
tf, err := ioutil.TempFile("", "fdb_conf")
if err != nil {
return err
}
defer tf.Close()

tfilename := tf.Name()
defer os.Remove(tfilename)

if err = f.SaveTo(tfilename); err != nil {
return err
}

return os.Rename(tfilename, filename)
}
Loading

0 comments on commit b8a2cd4

Please sign in to comment.