Skip to content

Commit 748be3f

Browse files
authored
feat: add AsGoGetterURL() to connection (#303)
* feat: add AsGoGetterURL() to connection * feat: return file content on AsEnv() * added tests as well * feat: env prep * make envprep context aware * chore: use lowercase for connection types * fix: use new CmdEnv * chore: fix test
1 parent 1e214bd commit 748be3f

File tree

2 files changed

+324
-1
lines changed

2 files changed

+324
-1
lines changed

models/connections.go

+218-1
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,67 @@
11
package models
22

33
import (
4+
"bytes"
5+
"context"
6+
"encoding/base64"
7+
"fmt"
8+
"math/rand"
49
"net/url"
10+
"os"
11+
"os/exec"
12+
"path/filepath"
513
"regexp"
14+
"strings"
615
"time"
716

817
"github.com/flanksource/duty/types"
918
"github.com/google/uuid"
1019
)
1120

21+
// List of all connection types
22+
const (
23+
ConnectionTypeAWS = "aws"
24+
ConnectionTypeAzure = "azure"
25+
ConnectionTypeAzureDevops = "azure_devops"
26+
ConnectionTypeDiscord = "discord"
27+
ConnectionTypeDynatrace = "dynatrace"
28+
ConnectionTypeElasticSearch = "elasticsearch"
29+
ConnectionTypeEmail = "email"
30+
ConnectionTypeGCP = "google_cloud"
31+
ConnectionTypeGenericWebhook = "generic_webhook"
32+
ConnectionTypeGit = "git"
33+
ConnectionTypeGithub = "github"
34+
ConnectionTypeGoogleChat = "google_chat"
35+
ConnectionTypeHTTP = "http"
36+
ConnectionTypeIFTTT = "ifttt"
37+
ConnectionTypeJMeter = "jmeter"
38+
ConnectionTypeKubernetes = "kubernetes"
39+
ConnectionTypeLDAP = "ldap"
40+
ConnectionTypeMatrix = "matrix"
41+
ConnectionTypeMattermost = "mattermost"
42+
ConnectionTypeMongo = "mongo"
43+
ConnectionTypeMySQL = "mysql"
44+
ConnectionTypeNtfy = "ntfy"
45+
ConnectionTypeOpsGenie = "opsgenie"
46+
ConnectionTypePostgres = "postgres"
47+
ConnectionTypePrometheus = "prometheus"
48+
ConnectionTypePushbullet = "pushbullet"
49+
ConnectionTypePushover = "pushover"
50+
ConnectionTypeRedis = "redis"
51+
ConnectionTypeRestic = "restic"
52+
ConnectionTypeRocketchat = "rocketchat"
53+
ConnectionTypeSFTP = "sftp"
54+
ConnectionTypeSlack = "slack"
55+
ConnectionTypeSlackWebhook = "slackwebhook"
56+
ConnectionTypeSMB = "smb"
57+
ConnectionTypeSQLServer = "sql_server"
58+
ConnectionTypeTeams = "teams"
59+
ConnectionTypeTelegram = "telegram"
60+
ConnectionTypeWebhook = "webhook"
61+
ConnectionTypeWindows = "windows"
62+
ConnectionTypeZulipChat = "zulip_chat"
63+
)
64+
1265
type Connection struct {
1366
ID uuid.UUID `gorm:"primaryKey;unique_index;not null;column:id" json:"id" faker:"uuid_hyphenated" `
1467
Name string `gorm:"column:name" json:"name" faker:"name" `
@@ -25,9 +78,10 @@ type Connection struct {
2578
}
2679

2780
func (c Connection) String() string {
28-
if c.Type == "aws" {
81+
if strings.ToLower(c.Type) == ConnectionTypeAWS {
2982
return "AWS::" + c.Username
3083
}
84+
3185
var connection string
3286
// Obfuscate passwords of the form ' password=xxxxx ' from connectionString since
3387
// connectionStrings are used as metric labels and we don't want to leak passwords
@@ -48,3 +102,166 @@ func (c Connection) String() string {
48102
func (c Connection) AsMap(removeFields ...string) map[string]any {
49103
return asMap(c, removeFields...)
50104
}
105+
106+
// AsGoGetterURL returns the connection as a url that's supported by https://github.com/hashicorp/go-getter
107+
// Connection details are added to the url as query params
108+
func (c Connection) AsGoGetterURL() (string, error) {
109+
parsedURL, err := url.Parse(c.URL)
110+
if err != nil {
111+
return "", err
112+
}
113+
114+
var output string
115+
switch strings.ReplaceAll(strings.ToLower(c.Type), " ", "_") {
116+
case ConnectionTypeHTTP:
117+
if c.Username != "" || c.Password != "" {
118+
parsedURL.User = url.UserPassword(c.Username, c.Password)
119+
}
120+
121+
output = parsedURL.String()
122+
123+
case ConnectionTypeGit:
124+
q := parsedURL.Query()
125+
126+
if c.Certificate != "" {
127+
q.Set("sshkey", base64.URLEncoding.EncodeToString([]byte(c.Certificate)))
128+
}
129+
130+
if v, ok := c.Properties["ref"]; ok {
131+
q.Set("ref", v)
132+
}
133+
134+
if v, ok := c.Properties["depth"]; ok {
135+
q.Set("depth", v)
136+
}
137+
138+
parsedURL.RawQuery = q.Encode()
139+
output = parsedURL.String()
140+
141+
case ConnectionTypeAWS:
142+
q := parsedURL.Query()
143+
q.Set("aws_access_key_id", c.Username)
144+
q.Set("aws_access_key_secret", c.Password)
145+
146+
if v, ok := c.Properties["profile"]; ok {
147+
q.Set("aws_profile", v)
148+
}
149+
150+
if v, ok := c.Properties["region"]; ok {
151+
q.Set("region", v)
152+
}
153+
154+
// For S3
155+
if v, ok := c.Properties["version"]; ok {
156+
q.Set("version", v)
157+
}
158+
159+
parsedURL.RawQuery = q.Encode()
160+
output = parsedURL.String()
161+
}
162+
163+
return output, nil
164+
}
165+
166+
// AsEnv generates environment variables and a configuration file content based on the connection type.
167+
func (c Connection) AsEnv(ctx context.Context) EnvPrep {
168+
var envPrep = EnvPrep{
169+
Files: make(map[string]bytes.Buffer),
170+
}
171+
172+
switch strings.ReplaceAll(strings.ToLower(c.Type), " ", "_") {
173+
case ConnectionTypeAWS:
174+
envPrep.Env = append(envPrep.Env, fmt.Sprintf("AWS_ACCESS_KEY_ID=%s", c.Username))
175+
envPrep.Env = append(envPrep.Env, fmt.Sprintf("AWS_SECRET_ACCESS_KEY=%s", c.Password))
176+
177+
// credentialFilePath :="$HOME/.aws/credentials"
178+
credentialFilePath := filepath.Join(".creds", "aws", fmt.Sprintf("cred-%d", rand.Intn(100000000)))
179+
180+
var credentialFile bytes.Buffer
181+
credentialFile.WriteString("[default]\n")
182+
credentialFile.WriteString(fmt.Sprintf("aws_access_key_id = %s\n", c.Username))
183+
credentialFile.WriteString(fmt.Sprintf("aws_secret_access_key = %s\n", c.Password))
184+
185+
if v, ok := c.Properties["profile"]; ok {
186+
envPrep.Env = append(envPrep.Env, fmt.Sprintf("AWS_DEFAULT_PROFILE=%s", v))
187+
}
188+
189+
if v, ok := c.Properties["region"]; ok {
190+
envPrep.Env = append(envPrep.Env, fmt.Sprintf("AWS_DEFAULT_REGION=%s", v))
191+
192+
credentialFile.WriteString(fmt.Sprintf("region = %s\n", v))
193+
194+
envPrep.CmdEnvs = append(envPrep.CmdEnvs, fmt.Sprintf("AWS_DEFAULT_REGION=%s", v))
195+
}
196+
197+
envPrep.Files[credentialFilePath] = credentialFile
198+
199+
envPrep.CmdEnvs = append(envPrep.CmdEnvs, "AWS_EC2_METADATA_DISABLED=true") // https://github.com/aws/aws-cli/issues/5262#issuecomment-705832151
200+
envPrep.CmdEnvs = append(envPrep.CmdEnvs, fmt.Sprintf("AWS_SHARED_CREDENTIALS_FILE=%s", credentialFilePath))
201+
202+
case ConnectionTypeAzure:
203+
args := []string{"login", "--service-principal", "--username", c.Username, "--password", c.Password}
204+
if v, ok := c.Properties["tenant"]; ok {
205+
args = append(args, "--tenant")
206+
args = append(args, v)
207+
}
208+
209+
// login with service principal
210+
envPrep.PreRuns = append(envPrep.PreRuns, exec.CommandContext(ctx, "az", args...))
211+
212+
case ConnectionTypeGCP:
213+
var credentialFile bytes.Buffer
214+
credentialFile.WriteString(c.Certificate)
215+
216+
// credentialFilePath := "$HOME/.config/gcloud/credentials"
217+
credentialFilePath := filepath.Join(".creds", "gcp", fmt.Sprintf("cred-%d", rand.Intn(100000000)))
218+
219+
// to configure gcloud CLI to use the service account specified in GOOGLE_APPLICATION_CREDENTIALS,
220+
// we need to explicitly activate it
221+
envPrep.PreRuns = append(envPrep.PreRuns, exec.CommandContext(ctx, "gcloud", "auth", "activate-service-account", "--key-file", credentialFilePath))
222+
envPrep.Files[credentialFilePath] = credentialFile
223+
224+
envPrep.CmdEnvs = append(envPrep.CmdEnvs, fmt.Sprintf("GOOGLE_APPLICATION_CREDENTIALS=%s", credentialFilePath))
225+
}
226+
227+
return envPrep
228+
}
229+
230+
type EnvPrep struct {
231+
// Env is the connection credentials in environment variables
232+
Env []string
233+
234+
// CmdEnvs is a list of env vars that will be passed to the command
235+
CmdEnvs []string
236+
237+
// List of commands that need to be run before the actual command.
238+
// These commands will setup the connection.
239+
PreRuns []*exec.Cmd
240+
241+
// File contains the content of the configuration file based on the connection
242+
Files map[string]bytes.Buffer
243+
}
244+
245+
// Inject creates the config file & injects the necessary environment variable into the command
246+
func (c *EnvPrep) Inject(ctx context.Context, cmd *exec.Cmd) ([]*exec.Cmd, error) {
247+
for path, file := range c.Files {
248+
if err := saveConfig(file.Bytes(), path); err != nil {
249+
return nil, fmt.Errorf("error saving config to %s: %w", path, err)
250+
}
251+
}
252+
253+
cmd.Env = append(cmd.Env, c.CmdEnvs...)
254+
255+
return c.PreRuns, nil
256+
}
257+
258+
func saveConfig(content []byte, absPath string) error {
259+
file, err := os.Create(absPath)
260+
if err != nil {
261+
return err
262+
}
263+
defer file.Close()
264+
265+
_, err = file.Write(content)
266+
return err
267+
}

models/connections_test.go

+106
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package models
2+
3+
import (
4+
"context"
5+
"testing"
6+
)
7+
8+
func Test_Connection_AsGoGetterURL(t *testing.T) {
9+
testCases := []struct {
10+
name string
11+
connection Connection
12+
expectedURL string
13+
expectedError error
14+
}{
15+
{
16+
name: "HTTP Connection",
17+
connection: Connection{
18+
Type: ConnectionTypeHTTP,
19+
URL: "http://example.com",
20+
Username: "testuser",
21+
Password: "testpassword",
22+
},
23+
expectedURL: "http://testuser:[email protected]",
24+
expectedError: nil,
25+
},
26+
{
27+
name: "Git Connection",
28+
connection: Connection{
29+
Type: ConnectionTypeGit,
30+
URL: "https://github.com/repo.git",
31+
Certificate: "cert123",
32+
Properties: map[string]string{"ref": "main"},
33+
},
34+
expectedURL: "https://github.com/repo.git?ref=main&sshkey=Y2VydDEyMw%3D%3D",
35+
expectedError: nil,
36+
},
37+
}
38+
39+
for _, tc := range testCases {
40+
t.Run(tc.name, func(t *testing.T) {
41+
resultURL, err := tc.connection.AsGoGetterURL()
42+
43+
if resultURL != tc.expectedURL {
44+
t.Errorf("Expected URL: %s, but got: %s", tc.expectedURL, resultURL)
45+
}
46+
47+
if err != tc.expectedError {
48+
t.Errorf("Expected error: %v, but got: %v", tc.expectedError, err)
49+
}
50+
})
51+
}
52+
}
53+
54+
func Test_Connection_AsEnv(t *testing.T) {
55+
testCases := []struct {
56+
name string
57+
connection Connection
58+
expectedEnv []string
59+
expectedFileContent string
60+
}{
61+
{
62+
name: "AWS Connection",
63+
connection: Connection{
64+
Type: ConnectionTypeAWS,
65+
Username: "awsuser",
66+
Password: "awssecret",
67+
Properties: map[string]string{"profile": "awsprofile", "region": "us-east-1"},
68+
},
69+
expectedEnv: []string{
70+
"AWS_ACCESS_KEY_ID=awsuser",
71+
"AWS_SECRET_ACCESS_KEY=awssecret",
72+
"AWS_DEFAULT_PROFILE=awsprofile",
73+
"AWS_DEFAULT_REGION=us-east-1",
74+
},
75+
expectedFileContent: "[default]\naws_access_key_id = awsuser\naws_secret_access_key = awssecret\nregion = us-east-1\n",
76+
},
77+
{
78+
name: "GCP Connection",
79+
connection: Connection{
80+
Type: ConnectionTypeGCP,
81+
Username: "gcpuser",
82+
Certificate: `{"account": "gcpuser"}`,
83+
},
84+
expectedEnv: []string{},
85+
expectedFileContent: `{"account": "gcpuser"}`,
86+
},
87+
}
88+
89+
for _, tc := range testCases {
90+
t.Run(tc.name, func(t *testing.T) {
91+
envPrep := tc.connection.AsEnv(context.Background())
92+
93+
for i, expected := range tc.expectedEnv {
94+
if envPrep.Env[i] != expected {
95+
t.Errorf("Expected environment variable: %s, but got: %s", expected, envPrep.Env[i])
96+
}
97+
}
98+
99+
for _, content := range envPrep.Files {
100+
if content.String() != tc.expectedFileContent {
101+
t.Errorf("Expected file content: %s, but got: %s", tc.expectedFileContent, content.String())
102+
}
103+
}
104+
})
105+
}
106+
}

0 commit comments

Comments
 (0)