Skip to content

Instantly share code, notes, and snippets.

@sandromello
Last active July 4, 2024 19:43
Show Gist options
  • Save sandromello/1d6f6aa0209c63d5a118004d328c4f25 to your computer and use it in GitHub Desktop.
Save sandromello/1d6f6aa0209c63d5a118004d328c4f25 to your computer and use it in GitHub Desktop.
Hoop Target to Connection
package main
import (
"bytes"
"encoding/json"
"flag"
"fmt"
"math/rand"
"os"
"strings"
"text/template"
"time"
)
func PrintErrorAndExit(format string, args ...any) {
fmt.Println(fmt.Sprintf(format, args...))
os.Exit(1)
}
var createConnTmpl = `hoop admin create connection {{ .name }} --agent {{ .tags }} \
--type {{ .type }} \
{{- range $key, $val := .plugins }}
--plugin '{{ $key }}{{ $val }}' \
{{- end }}
{{- range $key, $val := .secret }}
--env {{ $key }}={{ $val }} \
{{- end }}
--overwrite {{- with .command }} -- {{ . }} {{- end -}}
`
func parseTemplate(tgt *RunopsTarget) string {
cmdInfo, err := parseEnvVarByType(tgt)
if err != nil {
PrintErrorAndExit(err.Error())
}
cmdType := cmdInfo.Type
if cmdInfo.Subtype != "" {
cmdType = strings.Join([]string{cmdInfo.Type, cmdInfo.Subtype}, "/")
}
connectionBody := map[string]any{
"name": tgt.Name,
"type": cmdType,
"secret": cmdInfo.EnvVars,
"tags": tgt.Tags,
}
plugins := map[string]string{"audit": ""}
if tgt.getSlackChannel() != "" {
plugins["slack:"] = tgt.getSlackChannel()
}
if len(tgt.Groups) > 0 {
plugins["access_control:"] = strings.Join(tgt.Groups, ";")
}
if tgt.enableReview() && tgt.ReviewGroups != "" {
plugins["review:"] = strings.ReplaceAll(tgt.ReviewGroups, ",", ";")
}
if tgt.enableDLP() {
plugins["dlp"] = ""
}
connectionBody["plugins"] = plugins
connectionBody["command"] = strings.Join(cmdInfo.CmdList, " ")
return execGoTemplate(createConnTmpl, connectionBody)
}
type ConnectionCmd struct {
Type string
Subtype string
CmdList []string
EnvVars map[string]string
}
func parseEnvVarByType(t *RunopsTarget) (*ConnectionCmd, error) {
cmd := &ConnectionCmd{Type: "database"}
// if t.CustomCommand != nil {
// return nil, fmt.Errorf("target has custom command [%v], not implemented, target=%v", *t.CustomCommand, t.Name)
// }
envVar := map[string]string{}
secretKeyFn := func(key string) string {
switch t.SecretProvider {
case "aws":
return fmt.Sprintf("_aws:%s:%s", t.SecretPath, t.secretKey(key))
case "env-var":
return fmt.Sprintf("_envjson:%s:%s", t.SecretPath, t.secretKey(key))
case "runops":
secretVal := t.secretKey(key)
if secretVal == "" {
PrintErrorAndExit("provider=runops - secret value is empty for %q, target=%v", key, t.Name)
}
return secretVal
case "":
// https://github.com/runopsio/agent/blob/213a2dab6de316a850a641f16836eecad0d74575/src/agent/secrets.clj#L46
return ""
}
PrintErrorAndExit("secret provider %q not implemented", t.SecretProvider)
return ""
}
switch t.Type {
case "mysql", "mysql-csv":
cmd.Subtype = "mysql"
envVar["HOST"] = secretKeyFn("MYSQL_HOST")
envVar["USER"] = secretKeyFn("MYSQL_USER")
envVar["PASS"] = secretKeyFn("MYSQL_PASS")
envVar["DB"] = secretKeyFn("MYSQL_DB")
envVar["PORT"] = "3306"
case "postgres", "postgres-csv":
// TODO: add ssl options
cmd.Subtype = "postgres"
envVar["HOST"] = secretKeyFn("PG_HOST")
envVar["USER"] = secretKeyFn("PG_USER")
envVar["PASS"] = secretKeyFn("PG_PASS")
envVar["DB"] = secretKeyFn("PG_DB")
envVar["PORT"] = "5432"
case "sql-server":
cmd.Subtype = "mssql"
envVar["HOST"] = secretKeyFn("MSSQL_CONNECTION_URI")
envVar["USER"] = secretKeyFn("MSSQL_USER")
envVar["PASS"] = secretKeyFn("MSSQL_PASS")
envVar["PORT"] = "1433"
envVar["DB"] = secretKeyFn("MSSQL_DB")
case "mongodb":
cmd.Type = "custom"
envVar["MONGO_CONNECTION_URI"] = secretKeyFn("MONGO_CONNECTION_URI")
cmd.CmdList = []string{"mongo", "--quiet", "'$MONGO_CONNECTION_URI'"}
case "python":
cmd.Type = "custom"
cmd.CmdList = []string{"python3"}
case "node":
cmd.Type = "custom"
cmd.CmdList = []string{"node"}
case "k8s":
envVar["b64-filesystem:KUBECONFIG"] = secretKeyFn("KUBE_CONFIG_DATA")
cmd.Type = "custom"
cmd.CmdList = []string{"kubectl"}
case "bash":
cmd.Type = "custom"
cmd.CmdList = []string{"bash"}
if t.CustomCommand != nil {
cc := *t.CustomCommand
cc = strings.ReplaceAll(strings.ReplaceAll(cc, "[[", "'${"), "]]", "}'")
cmd.CmdList = strings.Split(cc, " ")
}
default:
return nil, fmt.Errorf("target %q not supported", t.Type)
}
cmd.EnvVars = envVar
return cmd, nil
}
type RunopsTarget struct {
Name string `json:"name"`
Status string `json:"status"`
Type string `json:"type"`
Tags string `json:"tags"`
SecretProvider string `json:"secret_provider"`
SecretPath string `json:"secret_path"`
Config map[string]string `json:"config"`
SecretMapping map[string]string `json:"secret_mapping"`
Redact string `json:"redact"`
ReviewType string `json:"review_type"`
ReviewGroups string `json:"reviewers"`
SlackChannel *string `json:"channel_name"`
Groups []string `json:"groups"`
// TODO
CustomCommand *string `json:"custom_command"`
secretMapping map[string]string
}
func (t *RunopsTarget) getSlackChannel() string {
if t.SlackChannel != nil {
return *t.SlackChannel
}
return ""
}
func (t *RunopsTarget) enableReview() bool { return t.ReviewType != "none" }
func (t *RunopsTarget) enableDLP() bool { return t.Redact == "all" }
func (t *RunopsTarget) agentName() string {
if t.Tags == "" {
return "main"
}
return t.Tags
}
// secretKey do a best effort to fetch the secret from the mapping, if it doesn't
// exits returns the same key
func (t *RunopsTarget) secretKey(key string) string {
if t.SecretProvider == "runops" {
return t.Config[key]
}
secretKeyVal, ok := t.secretMapping[key]
if !ok {
secretKeyVal = key
}
return secretKeyVal
}
func parseRunopsTargets(filePath string) []RunopsTarget {
targetJsonData, err := os.ReadFile(filePath)
if err != nil {
PrintErrorAndExit("failed fetching targets from file, reason=%v", err)
}
items := []RunopsTarget{}
if err := json.Unmarshal(targetJsonData, &items); err != nil {
PrintErrorAndExit("failed decoding targets, reason=%v", err)
}
return items
}
func execGoTemplate(tmpl string, data any) string {
t, err := template.New("").Parse(tmpl)
if err != nil {
PrintErrorAndExit("failed parsing template, err=%v", err)
}
buf := bytes.NewBufferString("")
if err := t.Execute(buf, data); err != nil {
PrintErrorAndExit("failed executing template: %v", err)
}
return buf.String()
}
func main() {
hosts := []string{"host01:27017", "host02:27017", "host03:27017"}
for i := 0; i < 3; i++ {
randS := rand.NewSource(time.Now().Unix())
index := rand.New(randS).Intn(len(hosts))
fmt.Println("GOT", hosts[index])
}
os.Exit(0)
var statusFlag string
flag.StringVar(&statusFlag, "status", "active", "parse depending on the status: active|inactive")
flag.Parse()
if len(flag.Args()) == 0 {
PrintErrorAndExit("missing targets file argument, e.g.: ./target-to-connection /tmp/targets.json")
}
args := flag.Args()
targetList := parseRunopsTargets(args[0])
for _, tgt := range targetList {
if statusFlag != tgt.Status {
continue
}
tmpl := parseTemplate(&tgt)
fmt.Println(tmpl)
fmt.Println("")
}
}
[
{
"name": "myapp-dev",
"tags": "main",
"type": "postgres",
"config": null,
"groups": [
"dev"
],
"redact": "all",
"status": "active",
"reviewers": null,
"review_type": "none",
"secret_path": "my-secret-path",
"channel_name": null,
"custom_command": null,
"secret_mapping": {
"PG_HOST": "host",
"PG_USER": "username",
"PG_PASS": "password",
"PG_DB": "dbname",
"PG_PORT": "port"
},
"secret_provider": "aws"
}
]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment