Skip to content

Commit

Permalink
adding commands for configuration management and init
Browse files Browse the repository at this point in the history
  • Loading branch information
devdinu committed Jun 28, 2023
1 parent 8bf5860 commit 07576bf
Show file tree
Hide file tree
Showing 4 changed files with 387 additions and 0 deletions.
168 changes: 168 additions & 0 deletions cmd/dolores/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
package main

import (
"context"
"fmt"
"os"

"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/scalescape/dolores/client"
"github.com/scalescape/dolores/secrets"
"github.com/urfave/cli/v2"
)

type ConfigCommand struct {
*cli.Command
rcli func(context.Context) *client.Client
log zerolog.Logger
}

func NewConfig(client func(context.Context) *client.Client) *ConfigCommand {
log := log.With().Str("cmd", "config").Logger()
cmd := &cli.Command{
Name: "config",
Usage: "secrets management",
Flags: []cli.Flag{},
}
cfg := &ConfigCommand{
Command: cmd,
log: log,
rcli: client,
}
cfg.Subcommands = append(cfg.Subcommands, EncryptCommand(cfg.encryptAction))
cfg.Subcommands = append(cfg.Subcommands, DecryptCommand(cfg.decryptAction))
cfg.Subcommands = append(cfg.Subcommands, EditCommand(cfg.editAction))
return cfg
}

func (c *ConfigCommand) editAction(ctx *cli.Context) error {
env := ctx.String("environment")
log := c.log.With().Str("cmd", "config.edit").Str("environment", env).Logger()
dcfg, err := parseDecryptConfig(ctx)
if err != nil {
return err
}
sec := secrets.NewSecertsManager(log, c.rcli(ctx.Context))
cfg := secrets.EditConfig{DecryptConfig: dcfg}
if err := sec.Edit(cfg); err != nil {
log.Error().Msgf("error editing file: %v", err)
return err
}
return nil
}

func (c *ConfigCommand) encryptAction(ctx *cli.Context) error {
env := ctx.String("environment")
file := ctx.String("file")
name := ctx.String("name")
log := c.log.With().Str("cmd", "config.encrypt").Str("environment", env).Logger()
secMan := secrets.NewSecertsManager(log, c.rcli(ctx.Context))
req := secrets.EncryptConfig{Environment: env, FileName: file, Name: name}
if err := secMan.Encrypt(req); err != nil {
return err
}
return nil
}

func parseDecryptConfig(ctx *cli.Context) (secrets.DecryptConfig, error) {
env := ctx.String("environment")
name := ctx.String("name")
req := secrets.DecryptConfig{
Environment: env,
Name: name,
Out: os.Stdout,
}
parseKeyConfig(ctx, &req)
if err := req.Valid(); err != nil {
return secrets.DecryptConfig{}, fmt.Errorf("pass appropriate key or key-file to decrypt: %w", err)
}

return req, nil
}

func parseKeyConfig(ctx *cli.Context, cfg *secrets.DecryptConfig) {
log.Trace().Msgf("parsing configuration required to decrypt config")
key := ctx.String("key")
keyFile := ctx.String("key-file")
if keyFile == "" {
keyFile = os.Getenv("DOLORES_SECRETS_KEY_FILE")
}
if key == "" {
key = os.Getenv("DOLORES_SECRETS_KEY")
}
cfg.KeyFile = keyFile
cfg.Key = key
}

func (c *ConfigCommand) decryptAction(ctx *cli.Context) error {
req, err := parseDecryptConfig(ctx)
if err != nil {
return err
}
log := c.log.With().Str("cmd", "config.dencrypt").Str("environment", req.Environment).Logger()
log.Trace().Str("cmd", "config.decrypt").Msgf("running decryption")
sec := secrets.NewSecertsManager(log, c.rcli(ctx.Context))
return sec.Decrypt(req)
}

func EncryptCommand(action cli.ActionFunc) *cli.Command {
return &cli.Command{
Name: "encrypt",
Usage: "encrypt the passed file",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "file",
Aliases: []string{"f"},
Required: true,
},
&cli.StringFlag{
Name: "name",
Required: true,
},
},
Action: action,
}
}

func DecryptCommand(action cli.ActionFunc) *cli.Command {
return &cli.Command{
Name: "decrypt",
Usage: "decrypt the remote configuration",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "name",
Required: true,
},
&cli.StringFlag{
Name: "key",
Aliases: []string{"k"},
},
&cli.StringFlag{
Name: "key-file",
},
},
Action: action,
}
}

func EditCommand(action cli.ActionFunc) *cli.Command {
return &cli.Command{
Name: "edit",
Usage: "edit the secrets configuration",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "name",
Required: true,
},
&cli.StringFlag{
Name: "key",
Aliases: []string{"k"},
},
&cli.StringFlag{
Name: "key-file",
},
},
Action: action,
}
}
110 changes: 110 additions & 0 deletions cmd/dolores/init.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package main

import (
"context"
"os"

"github.com/AlecAivazis/survey/v2"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/scalescape/dolores/client"
"github.com/scalescape/dolores/config"
"github.com/urfave/cli/v2"
)

type InitCommand struct {
*cli.Command
log zerolog.Logger
rcli func(context.Context) *client.Client
}

type GetClient func(context.Context) *client.Client

func NewInitCommand(newCli GetClient) *cli.Command {
ic := &InitCommand{
log: log.With().Str("cmd", "init").Logger(),
rcli: newCli,
}
cmd := &cli.Command{
Name: "init",
Usage: "bootstrap with settings",
Action: ic.initialize,
}
return cmd
}

type Input struct {
Bucket string
Environment string
Location string
ApplicationCredentials string `survey:"google_creds"`
}

func (c *InitCommand) getData() (*Input, error) {
credFile := os.Getenv("GOOGLE_APPLICATION_CREDENTIALS")
qs := []*survey.Question{
{
Name: "environment",
Prompt: &survey.Select{
Message: "Choose an environment:",
Options: []string{"production", "staging"},
Default: "production",
},
},
{
Name: "bucket",
Prompt: &survey.Input{
Message: "Enter the GCS bucket name:",
},
Validate: survey.Required,
},
{
Name: "location",
Validate: survey.Required,
Prompt: &survey.Input{
Message: "Enter the object location to store the secrets",
},
},
}
if credFile != "" {
qs = append(qs, &survey.Question{
Name: "google_creds",
Validate: survey.Required,
Prompt: &survey.Select{
Message: "Confirm using GOOGLE_APPLICATION_CREDENTIALS env as credentials file",
Options: []string{credFile, ""},
},
})
} else {
qs = append(qs, &survey.Question{
Name: "google_creds",
Prompt: &survey.Input{
Message: "Enter google service account file path",
},
Validate: survey.Required,
})
}
res := new(Input)
if err := survey.Ask(qs, res); err != nil {
return nil, err
}
return res, nil
}

func (c *InitCommand) initialize(ctx *cli.Context) error {
inp, err := c.getData()
if err != nil {
return err
}
md := config.Metadata{
Bucket: inp.Bucket,
Locations: map[string]string{
inp.Environment: inp.Location,
},
}
if err := c.rcli(ctx.Context).SaveMetadata(ctx.Context, inp.Bucket, md); err != nil {
c.log.Error().Msgf("error writing metadta: %v", err)
return err
}
return nil
}
60 changes: 60 additions & 0 deletions cmd/dolores/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package main

import (
"context"
"fmt"
"os"

"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/scalescape/dolores/client"
"github.com/scalescape/dolores/config"
"github.com/urfave/cli/v2"
)

var (
version = "0.0.1"
sha = "undefined"
)

func main() {
log.Logger = log.Output(zerolog.NewConsoleWriter())
cli.VersionPrinter = VersionDisplay

app := &cli.App{
Name: "Dolores",
Usage: "service configuration management with your own cloud platform",
Version: version,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "environment", Aliases: []string{"env"},
Usage: "environment where you want to manage [staging|production]",
},
},
Commands: []*cli.Command{
NewConfig(newClient).Command,
NewInitCommand(newClient),
},
}
if err := app.Run(os.Args); err != nil {
log.Fatal().Msgf("error: %v", err)
}
}

func VersionDisplay(cc *cli.Context) {
fmt.Printf("rom %s (%s)", version, sha) //nolint
}

func newClient(ctx context.Context) *client.Client {
cfg, err := config.LoadClient()
if err != nil {
log.Fatal().Msgf("error loading config: %v", err)
return nil
}
client, err := client.New(ctx, cfg)
if err != nil {
log.Fatal().Msgf("error building client: %v", err)
return nil
}
return client
}
49 changes: 49 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package config

import (
"errors"
"fmt"
"time"

"github.com/kelseyhightower/envconfig"
)

var ErrInvalidGoogleCreds = errors.New("invalid google application credentials")

type Google struct {
ApplicationCredentials string `split_words:"true"`
StorageBucket string `split_words:"true" required:"true"`
}

type Metadata struct {
Bucket string `json:"bucket"`
Locations map[string]string `json:"locations"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}

type Client struct {
Google
}

func (c Client) BucketName() string {
return c.Google.StorageBucket
}

func (c Client) Valid() error {
if c.Google.ApplicationCredentials == "" {
return ErrInvalidGoogleCreds
}
return nil
}

func LoadClient() (Client, error) {
var cfg Client
if err := envconfig.Process("GOOGLE", &cfg.Google); err != nil {
return Client{}, fmt.Errorf("processing config: %w", err)
}
if err := cfg.Valid(); err != nil {
return Client{}, err
}
return cfg, nil
}

0 comments on commit 07576bf

Please sign in to comment.