-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: start/stop A Cloud Guru sandboxes and configure local credentials
- Loading branch information
0 parents
commit a16811b
Showing
3 changed files
with
354 additions
and
0 deletions.
There are no files selected for viewing
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,17 @@ | ||
module acloudguru-sandbox | ||
|
||
go 1.21.6 | ||
|
||
require ( | ||
github.com/go-ini/ini v1.67.0 | ||
github.com/go-rod/rod v0.114.6 | ||
) | ||
|
||
require ( | ||
github.com/stretchr/testify v1.8.4 // indirect | ||
github.com/ysmood/fetchup v0.2.3 // indirect | ||
github.com/ysmood/goob v0.4.0 // indirect | ||
github.com/ysmood/got v0.34.1 // indirect | ||
github.com/ysmood/gson v0.7.3 // indirect | ||
github.com/ysmood/leakless v0.8.0 // indirect | ||
) |
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,26 @@ | ||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= | ||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= | ||
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= | ||
github.com/go-rod/rod v0.114.6 h1:NrutWvLGn6Vea+0ZpLSHQ2cT5UMTqk9DeO+V6xeJBxw= | ||
github.com/go-rod/rod v0.114.6/go.mod h1:aiedSEFg5DwG/fnNbUOTPMTTWX3MRj6vIs/a684Mthw= | ||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | ||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | ||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= | ||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= | ||
github.com/ysmood/fetchup v0.2.3 h1:ulX+SonA0Vma5zUFXtv52Kzip/xe7aj4vqT5AJwQ+ZQ= | ||
github.com/ysmood/fetchup v0.2.3/go.mod h1:xhibcRKziSvol0H1/pj33dnKrYyI2ebIvz5cOOkYGns= | ||
github.com/ysmood/goob v0.4.0 h1:HsxXhyLBeGzWXnqVKtmT9qM7EuVs/XOgkX7T6r1o1AQ= | ||
github.com/ysmood/goob v0.4.0/go.mod h1:u6yx7ZhS4Exf2MwciFr6nIM8knHQIE22lFpWHnfql18= | ||
github.com/ysmood/gop v0.0.2 h1:VuWweTmXK+zedLqYufJdh3PlxDNBOfFHjIZlPT2T5nw= | ||
github.com/ysmood/gop v0.0.2/go.mod h1:rr5z2z27oGEbyB787hpEcx4ab8cCiPnKxn0SUHt6xzk= | ||
github.com/ysmood/got v0.34.1 h1:IrV2uWLs45VXNvZqhJ6g2nIhY+pgIG1CUoOcqfXFl1s= | ||
github.com/ysmood/got v0.34.1/go.mod h1:yddyjq/PmAf08RMLSwDjPyCvHvYed+WjHnQxpH851LM= | ||
github.com/ysmood/gotrace v0.6.0 h1:SyI1d4jclswLhg7SWTL6os3L1WOKeNn/ZtzVQF8QmdY= | ||
github.com/ysmood/gotrace v0.6.0/go.mod h1:TzhIG7nHDry5//eYZDYcTzuJLYQIkykJzCRIo4/dzQM= | ||
github.com/ysmood/gson v0.7.3 h1:QFkWbTH8MxyUTKPkVWAENJhxqdBa4lYTQWqZCiLG6kE= | ||
github.com/ysmood/gson v0.7.3/go.mod h1:3Kzs5zDl21g5F/BlLTNcuAGAYLKt2lV5G8D1zF3RNmg= | ||
github.com/ysmood/leakless v0.8.0 h1:BzLrVoiwxikpgEQR0Lk8NyBN5Cit2b1z+u0mgL4ZJak= | ||
github.com/ysmood/leakless v0.8.0/go.mod h1:R8iAXPRaG97QJwqxs74RdwzcRHT1SWCGTNqY8q0JvMQ= | ||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= | ||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= |
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,311 @@ | ||
package main | ||
|
||
import ( | ||
"fmt" | ||
"log" | ||
"os" | ||
"os/exec" | ||
"path/filepath" | ||
"strings" | ||
|
||
"github.com/go-rod/rod" | ||
"github.com/go-rod/rod/lib/defaults" | ||
"github.com/go-rod/rod/lib/input" | ||
|
||
"github.com/go-ini/ini" | ||
) | ||
|
||
const acgSandbox = "acloudguru-sandbox" | ||
const acgSandboxesUrl = "https://learn.acloud.guru/cloud-playground/cloud-sandboxes" | ||
|
||
func main() { | ||
log.SetPrefix(fmt.Sprintf("%s: ", acgSandbox)) | ||
command := CheckSubCommand(acgSandbox, os.Args) | ||
page := Login(GetGitCredentials(acgSandboxesUrl)) | ||
switch command { | ||
case "current": | ||
command = DetectSandbox(page) | ||
case "stop": | ||
page = StopSandbox(page) | ||
case "aws": | ||
page = StartSandbox(page, "AWS") | ||
case "azure": | ||
page = StartSandbox(page, "Azure") | ||
case "gcloud": | ||
page = StartSandbox(page, "Google Cloud") | ||
} | ||
switch command { | ||
case "stop": | ||
log.Printf("Stopped\n") | ||
case "aws": | ||
ConfigureAwsSandbox(page) | ||
case "azure": | ||
ConfigureAzureSandbox(page) | ||
case "gcloud": | ||
ConfigureGcloudSandbox(page) | ||
} | ||
Logout(page) | ||
} | ||
|
||
func CheckSubCommand(command string, args []string) string { | ||
paramsSyntax := "<current|stop|aws|azure|gcloud> [-rod=...]" | ||
if len(args) < 2 { | ||
log.Fatalf("missing sandbox command:\n\t%s %s\n", command, paramsSyntax) | ||
} | ||
if len(args) > 3 { | ||
log.Fatalf("unexpected arguments: %s\n\t%s %s\n", strings.Join(args[1:], " "), command, paramsSyntax) | ||
} | ||
if len(args) == 3 && !strings.HasPrefix(args[2], "-rod=") { | ||
log.Fatalln("2") | ||
} | ||
switch args[1] { | ||
case "current", "stop", "aws", "azure", "gcloud": | ||
return args[1] | ||
default: | ||
log.Fatalf("unknown sandbox command: %s\n\t%s %s\n", args[1], command, paramsSyntax) | ||
// unreached | ||
return "unknown" | ||
} | ||
} | ||
|
||
// https://pkg.go.dev/golang.org/x/tools/cmd/auth/gitauth | ||
func GetGitCredentials(url string) (username string, password string) { | ||
log.Printf("git credentials for url: %s\n", url) | ||
cmd := exec.Command("git", "credential", "fill") | ||
cmd.Stdin = strings.NewReader(fmt.Sprintf("url=%s\n", url)) | ||
out := RunCmd(cmd, 3) | ||
lines := strings.Split(string(out), "\n") | ||
for _, line := range lines { | ||
frags := strings.SplitN(line, "=", 2) | ||
if len(frags) != 2 { | ||
continue // Ignore unrecognized response lines. | ||
} | ||
switch strings.TrimSpace(frags[0]) { | ||
case "username": | ||
username = frags[1] | ||
case "password": | ||
password = frags[1] | ||
} | ||
} | ||
log.Printf("git username: %s\n", username) | ||
return username, password | ||
} | ||
|
||
func RunCmd(cmd *exec.Cmd, logArgsCount int) []byte { | ||
userHome, err := os.UserHomeDir() | ||
if err != nil { | ||
log.Fatalf("cannot get user HOME: %v\n", err) | ||
} | ||
cmd.Dir = userHome | ||
cmd.Stderr = os.Stderr | ||
var out []byte = nil | ||
|
||
if cmd.Stdout != nil { | ||
err = cmd.Run() | ||
} else { | ||
out, err = cmd.Output() | ||
} | ||
|
||
if err != nil { | ||
logCmd := strings.Join(cmd.Args[:logArgsCount], " ") | ||
log.Fatalf("'%s' failed: %v\n", logCmd, err) | ||
} | ||
return out | ||
} | ||
|
||
func Login(username string, password string) *rod.Page { | ||
browser := rod.New().MustConnect().NoDefaultDevice() | ||
page := browser.MustPage(acgSandboxesUrl) | ||
page.MustElement("input[name='email']").MustInput(username).MustType(input.Enter) | ||
page.MustElement("input[name='password']").MustInput(password).MustType(input.Enter) | ||
if page.MustHas("input[name='captcha']") { | ||
if defaults.Show { | ||
page.MustElement("input[name='captcha']").MustFocus() | ||
} else { | ||
log.Fatalf("Warning: CAPTCHA in login form, use -rod=show option") | ||
} | ||
} | ||
return page | ||
} | ||
|
||
func Logout(page *rod.Page) *rod.Page { | ||
page.MustNavigate("https://learn.acloud.guru/logout") | ||
return page | ||
} | ||
|
||
func DetectSandbox(page *rod.Page) string { | ||
current := "stop" | ||
page.Race(). | ||
ElementR("button", "Start").MustHandle(func(e *rod.Element) { | ||
current = "stop" | ||
}). | ||
ElementR("h3", "AWS Sandbox").MustHandle(func(e *rod.Element) { | ||
current = "aws" | ||
}). | ||
ElementR("h3", "Azure Sandbox").MustHandle(func(e *rod.Element) { | ||
current = "azure" | ||
}). | ||
ElementR("h3", "Google Cloud Sandbox").MustHandle(func(e *rod.Element) { | ||
current = "gcloud" | ||
}). | ||
MustDo() | ||
return current | ||
} | ||
|
||
func StartSandbox(page *rod.Page, target string) *rod.Page { | ||
sandboxHeading := fmt.Sprintf("%s Sandbox", target) | ||
startButtonText := fmt.Sprintf("Start %s Sandbox", target) | ||
for targetReached := false; !targetReached; { | ||
page.Race(). | ||
ElementR("h3", sandboxHeading).MustHandle(func(e *rod.Element) { | ||
targetReached = true | ||
}). | ||
ElementR("button", startButtonText).MustHandle(func(e *rod.Element) { | ||
e.MustClick() | ||
}). | ||
ElementR("button", "Delete Sandbox").MustHandle(func(e *rod.Element) { | ||
DeleteSandbox(e, page) | ||
}). | ||
MustDo() | ||
} | ||
return page | ||
} | ||
|
||
func DeleteSandbox(e *rod.Element, page *rod.Page) { | ||
e.MustClick() | ||
buttons := page.MustElements("button") | ||
buttons.Last().MustClick() | ||
page.MustWaitStable() | ||
} | ||
|
||
func StopSandbox(page *rod.Page) *rod.Page { | ||
for targetReached := false; !targetReached; { | ||
page.Race(). | ||
ElementR("button", "Start").MustHandle(func(e *rod.Element) { | ||
targetReached = true | ||
}). | ||
ElementR("button", "Delete Sandbox").MustHandle(func(e *rod.Element) { | ||
DeleteSandbox(e, page) | ||
}). | ||
MustDo() | ||
} | ||
return page | ||
} | ||
|
||
func ConfigureAwsSandbox(page *rod.Page) { | ||
awsAccessKeyId, awsSecretAccessKey := GetAwsSandboxCredentials(page) | ||
WriteAwsCredentialsFile(acgSandbox, awsAccessKeyId, awsSecretAccessKey) | ||
CheckAwsCredentials(acgSandbox) | ||
} | ||
|
||
func GetAwsSandboxCredentials(page *rod.Page) (string, string) { | ||
elements := page.MustElements("input[aria-label='Copy to clipboard']") | ||
aws_username := elements[0].MustText() | ||
aws_password := elements[1].MustText() | ||
aws_url := elements[2].MustText() | ||
aws_access_key_id := elements[3].MustText() | ||
aws_secret_access_key := elements[4].MustText() | ||
log.Printf("Aws username: %s\n", aws_username) | ||
log.Printf("Aws password: %s\n", aws_password) | ||
log.Printf("Aws URL: %s\n", aws_url) | ||
log.Printf("Aws access key ID: %s\n", aws_access_key_id) | ||
log.Printf("Aws secret access key: %s\n", aws_secret_access_key) | ||
return aws_access_key_id, aws_secret_access_key | ||
} | ||
|
||
func WriteAwsCredentialsFile(profile string, awsAccessKeyId string, awsSecretAccessKey string) { | ||
userHome, err := os.UserHomeDir() | ||
if err != nil { | ||
log.Fatalf("cannot get user HOME: %v\n", err) | ||
} | ||
|
||
awsFolder := filepath.Join(userHome, ".aws") | ||
err = os.MkdirAll(awsFolder, os.ModePerm) | ||
if err != nil { | ||
log.Fatalf("Cannot access AWS config folder: %v\n", err) | ||
} else { | ||
log.Printf("Aws config folder: %s\n", awsFolder) | ||
} | ||
|
||
awsCredentialsFile := filepath.Join(awsFolder, "credentials") | ||
awsCredentials, err := ini.LooseLoad(awsCredentialsFile) | ||
if err != nil { | ||
log.Fatalf("Cannot read AWS credentials file: %v\n", err) | ||
} | ||
log.Printf("Aws proflie: %s\n", profile) | ||
awsCredentials.Section(profile).Key("aws_access_key_id").SetValue(awsAccessKeyId) | ||
awsCredentials.Section(profile).Key("aws_secret_access_key").SetValue(awsSecretAccessKey) | ||
awsCredentials.SaveTo(awsCredentialsFile) | ||
} | ||
|
||
func CheckAwsCredentials(profile string) { | ||
cmd := exec.Command("aws", "iam", "get-user", "--profile", profile) | ||
cmd.Stdout = os.Stdout | ||
_ = RunCmd(cmd, 3) | ||
} | ||
|
||
func ConfigureAzureSandbox(page *rod.Page) { | ||
azure_username, azure_password := GetAzureSandboxCredentials(page) | ||
LoginAzureCli(azure_username, azure_password) | ||
} | ||
|
||
func GetAzureSandboxCredentials(page *rod.Page) (string, string) { | ||
elements := page.MustElements("input[aria-label='Copy to clipboard']") | ||
azure_username := elements[0].MustText() | ||
azure_password := elements[1].MustText() | ||
azure_url := elements[2].MustText() | ||
azure_application_client_id := elements[3].MustText() | ||
azure_secret := elements[4].MustText() | ||
|
||
if true { | ||
log.Printf("Azure username: %s\n", azure_username) | ||
log.Printf("Azure password: %s\n", azure_password) | ||
log.Printf("Azure URL: %s\n", azure_url) | ||
log.Printf("Azure application client ID: %s\n", azure_application_client_id) | ||
log.Printf("Azure secret: %s\n", azure_secret) | ||
} | ||
return azure_username, azure_password | ||
} | ||
|
||
func LoginAzureCli(azure_username string, azure_password string) { | ||
cmd := exec.Command("az", "login", "--user", azure_username, "--password", azure_password) | ||
cmd.Stdout = os.Stdout | ||
_ = RunCmd(cmd, 2) | ||
} | ||
|
||
func ConfigureGcloudSandbox(page *rod.Page) { | ||
gcloud_service_account_credentials := GetGoogleCloudSandboxCredentials(page) | ||
LoginGoogleCloudCli(gcloud_service_account_credentials) | ||
} | ||
|
||
func GetGoogleCloudSandboxCredentials(page *rod.Page) string { | ||
elements := page.MustElements("input[aria-label='Copy to clipboard']") | ||
gcloud_username := elements[0].MustText() | ||
gcloud_password := elements[1].MustText() | ||
gcloud_url := elements[2].MustText() | ||
gcloud_service_account_credentials := elements[3].MustText() | ||
|
||
if true { | ||
log.Printf("Google Cloud username: %s\n", gcloud_username) | ||
log.Printf("Google Cloud password: %s\n", gcloud_password) | ||
log.Printf("Google Cloud URL: %s\n", gcloud_url) | ||
log.Printf("Google Cloud service account credentials: %s\n", gcloud_service_account_credentials) | ||
} | ||
return gcloud_service_account_credentials | ||
} | ||
|
||
func LoginGoogleCloudCli(gcloud_service_account_credentials string) { | ||
tempFile, err := os.CreateTemp("", "sample") | ||
if err != nil { | ||
log.Fatalf("cannot create temporary file: %v\n", err) | ||
} | ||
defer os.Remove(tempFile.Name()) | ||
_, err = tempFile.WriteString(gcloud_service_account_credentials) | ||
if err != nil { | ||
log.Fatalf("cannot write service accout credentials: %v\n", err) | ||
} | ||
keyArgument := fmt.Sprintf("--key=%s", tempFile.Name()) | ||
cmd := exec.Command("gcloud", "auth", "activate-service-account", keyArgument) | ||
cmd.Stdout = os.Stdout | ||
_ = RunCmd(cmd, 3) | ||
} |