-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Refactor POC into actual package (#1)
* Add internal envparser * Update documentation * Add GCP Secret Manager provider * Add initial client api * Add comment token as a const * Flip if condition to be more readable * Remove cmd folder * Update property name * Move serum.go into its own folder
- Loading branch information
Showing
5 changed files
with
194 additions
and
129 deletions.
There are no files selected for viewing
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
package envparser | ||
|
||
import ( | ||
"bufio" | ||
"fmt" | ||
"os" | ||
"regexp" | ||
"strings" | ||
) | ||
|
||
const ( | ||
commentToken = "#" | ||
kvSeparator = "=" | ||
secretRegex = `^${(?P<secretval>.+)}$` | ||
) | ||
|
||
var secretRe *regexp.Regexp | ||
|
||
func init() { | ||
secretRe = regexp.MustCompile(secretRegex) | ||
} | ||
|
||
//EnvVars contains the plain text key value mappings as well as the encrypted secret key value mappings | ||
//parsed from an env file | ||
type EnvVars struct { | ||
Plain map[string]string | ||
Secrets map[string]string | ||
} | ||
|
||
//ParseFile parses a .env file at path and returns the key value | ||
//mappings for plain text variables and secret variables | ||
func ParseFile(path string) (*EnvVars, error) { | ||
f, err := os.Open(path) | ||
if err != nil { | ||
return nil, fmt.Errorf("error opening file %s: %s", path, err) | ||
} | ||
defer f.Close() | ||
|
||
envVars := &EnvVars{ | ||
Plain: make(map[string]string), | ||
Secrets: make(map[string]string), | ||
} | ||
|
||
scanner := bufio.NewScanner(f) | ||
for scanner.Scan() { | ||
line := scanner.Text() | ||
if err := parseLine(envVars, line); err != nil { | ||
return nil, fmt.Errorf("error parsing line: %s: %s", line, err) | ||
} | ||
} | ||
if err := scanner.Err(); err != nil { | ||
return nil, fmt.Errorf("error parsing file: %s", err) | ||
} | ||
|
||
return envVars, nil | ||
} | ||
|
||
func parseLine(envVars *EnvVars, l string) error { | ||
//ignore commented line | ||
//TODO: remove inline comments | ||
if strings.HasPrefix(l, commentToken) { | ||
return nil | ||
} | ||
|
||
//split line into two pieces (k,v) based on key value seperator | ||
splits := strings.SplitN(l, kvSeparator, 2) | ||
if len(splits) != 2 { | ||
return fmt.Errorf("invalid format %s", l) | ||
} | ||
|
||
//key is first index, value is second | ||
k := strings.TrimSpace(splits[0]) | ||
v := strings.TrimSpace(splits[1]) | ||
|
||
//check if value is encrypted secret | ||
if secretRe.MatchString(v) { | ||
//fill in secret value - replace template value with capture group "secretval" | ||
envVars.Secrets[k] = secretRe.ReplaceAllString(v, "$secretval") | ||
return nil | ||
} | ||
|
||
//not a secret, fill in plain text value | ||
envVars.Plain[k] = v | ||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
package secretprovider | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
|
||
secretmanager "cloud.google.com/go/secretmanager/apiv1" | ||
secretmanagerpb "google.golang.org/genproto/googleapis/cloud/secretmanager/v1" | ||
) | ||
|
||
//GSManager is a secret provider that communicates with Google Cloud Platform's Secret Manager | ||
//to decrypt secrets. Internally it uses the Google Cloud SDK. | ||
type GSManager struct { | ||
smClient *secretmanager.Client | ||
} | ||
|
||
//NewGSManager return's an initialized GSManager using a new secret manager client. | ||
func NewGSManager() (*GSManager, error) { | ||
c, err := secretmanager.NewClient(context.Background()) | ||
if err != nil { | ||
return nil, fmt.Errorf("gsmanager: failed to initialize client: %w", err) | ||
} | ||
|
||
return &GSManager{smClient: c}, nil | ||
} | ||
|
||
//Decrypt will access the secret on GCP Secret Manager and return the plain text string. | ||
func (g *GSManager) Decrypt(secret string) (string, error) { | ||
req := &secretmanagerpb.AccessSecretVersionRequest{ | ||
Name: secret, | ||
} | ||
|
||
result, err := g.smClient.AccessSecretVersion(context.Background(), req) | ||
if err != nil { | ||
return "", fmt.Errorf("gsmanager: failed to access secret version: %w", err) | ||
} | ||
|
||
return result.Payload.String(), nil | ||
} | ||
|
||
//Close closes the connection to the secret manager API. | ||
func (g *GSManager) Close() error { | ||
return g.smClient.Close() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
package secretprovider | ||
|
||
//SecretProvider is an interface that wraps the decrypt and close methods. | ||
//Close should be called when the secret provier is no longer needed. | ||
//It may be a no-op in cases where there's no underlying connection to be closed. | ||
type SecretProvider interface { | ||
Decrypt(secret string) (string, error) | ||
Close() error | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
package serum | ||
|
||
import ( | ||
"fmt" | ||
"os" | ||
"wingocard/serum/internal/envparser" | ||
"wingocard/serum/secretprovider" | ||
) | ||
|
||
//Injector injects environment variables into the current running process. Key/value pairs can | ||
//be read in from a .env file using the load method. | ||
type Injector struct { | ||
SecretProvider secretprovider.SecretProvider | ||
envVars *envparser.EnvVars | ||
} | ||
|
||
//Inject will inject the loaded environment variables into the current running process' environment. | ||
//Any secret values found will attempt to be decrypted using the provided secret provider. | ||
//The presence of secrets with a nil SecretProvider will return an error. | ||
func (in *Injector) Inject() error { | ||
if len(in.envVars.Secrets) > 0 && in.SecretProvider == nil { | ||
return fmt.Errorf("serum: error injecting env vars: secrets were loaded but the SecretProvider is nil") | ||
} | ||
|
||
//inject secrets | ||
for k, v := range in.envVars.Secrets { | ||
decrypted, err := in.SecretProvider.Decrypt(v) | ||
if err != nil { | ||
return fmt.Errorf("serum: error decrypting secret %s: %s", v, err) | ||
} | ||
|
||
if err := os.Setenv(k, decrypted); err != nil { | ||
return fmt.Errorf("serum: error setting env var %s: %s", k, err) | ||
} | ||
} | ||
|
||
//inject plain text vars | ||
for k, v := range in.envVars.Plain { | ||
if err := os.Setenv(k, v); err != nil { | ||
return fmt.Errorf("serum: error setting env var %s: %s", k, err) | ||
} | ||
} | ||
return nil | ||
} | ||
|
||
//Load will parse a .env file for key/value pairs and prepair them to be injected using the | ||
//Inject method. | ||
func (in *Injector) Load(path string) error { | ||
envVars, err := envparser.ParseFile(path) | ||
if err != nil { | ||
return fmt.Errorf("serum: error loading env vars: %s", err) | ||
} | ||
|
||
in.envVars = envVars | ||
return nil | ||
} |