Last active
July 11, 2025 08:33
-
-
Save aslafy-z/458a80fe62c94099b1540548528307e6 to your computer and use it in GitHub Desktop.
List all Scaleway resources in an organization
This file contains hidden or 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
package main | |
import ( | |
"bytes" | |
"context" | |
"encoding/json" | |
"flag" | |
"fmt" | |
"os" | |
"os/exec" | |
"regexp" | |
"sort" | |
"strings" | |
"sync" | |
"time" | |
) | |
var ( | |
cmdPattern = regexp.MustCompile(`^[a-zA-Z0-9_-]+$`) | |
outputSerial = make(chan string) | |
wg sync.WaitGroup | |
allEverything bool | |
allProjects bool | |
allZones bool | |
currentProjectID string | |
projectIDs []string | |
currentZone string | |
zones []string | |
projectFlag string | |
zoneFlag string | |
outputFormat string | |
onlyPrefix string | |
matchFilter string | |
) | |
func isCommand(s string) bool { | |
return cmdPattern.MatchString(s) && !strings.HasPrefix(s, "-") | |
} | |
func main() { | |
flag.BoolVar(&allProjects, "all-projects", false, "Run list commands for all discovered projects. Cannot be used with -project.") | |
flag.BoolVar(&allZones, "all-zones", false, "Run list commands for all discovered zones. Cannot be used with -zone.") | |
flag.BoolVar(&allEverything, "A", false, "Shortcut for -all-projects and -all-zones.") | |
flag.StringVar(&outputFormat, "output", "", "Set output format (e.g., 'json', 'human=Name,PublicIP'). Defaults to the CLI's standard format if not specified.") | |
flag.StringVar(&outputFormat, "o", "", "Alias for -output.") | |
flag.StringVar(&projectFlag, "project", "", "Comma-separated project ID(s) to use (e.g., 'proj-abc123,proj-def456'). Overrides default project. Cannot be used with -all-projects or -A.") | |
flag.StringVar(&zoneFlag, "zone", "", "Comma-separated zone(s) to use (e.g., 'fr-par-1,nl-ams-1'). Overrides default zone. Cannot be used with -all-zones or -A.") | |
flag.StringVar(&onlyPrefix, "only", "", "Only explore commands under this top-level namespace (e.g., 'instance').") | |
flag.StringVar(&matchFilter, "match", "", "Only run 'list' commands where the full command path includes this substring.") | |
flag.Parse() | |
if allEverything { | |
allProjects = true | |
allZones = true | |
} | |
if projectFlag != "" && allProjects { | |
fmt.Fprintln(os.Stderr, "❌ Cannot use --project with --all-projects or -A") | |
os.Exit(1) | |
} | |
if zoneFlag != "" && allZones { | |
fmt.Fprintln(os.Stderr, "❌ Cannot use --zone with --all-zones or -A") | |
os.Exit(1) | |
} | |
var err error | |
projectIDs, err = getProjectIDs() | |
if err != nil { | |
fmt.Fprintf(os.Stderr, "❌ Failed to list projects: %v\n", err) | |
os.Exit(1) | |
} | |
zones, err = getZonesFromHelp() | |
if err != nil { | |
fmt.Fprintf(os.Stderr, "❌ Failed to get zones: %v\n", err) | |
os.Exit(1) | |
} | |
// Sort zones to ensure consistent ordering | |
sort.Strings(zones) | |
fmt.Fprintf(os.Stderr, "Found zones: %v\n", zones) | |
currentProjectID, currentZone = getDefaultsFromInfo() | |
if projectFlag != "" { | |
projectIDs = strings.Split(projectFlag, ",") | |
allProjects = true | |
} | |
if zoneFlag != "" { | |
zones = strings.Split(zoneFlag, ",") | |
allZones = true | |
} | |
outputDone := make(chan struct{}) | |
go func() { | |
defer close(outputDone) | |
for msg := range outputSerial { | |
fmt.Print(msg) | |
} | |
}() | |
visited := make(map[string]bool) | |
dumpHelpAndRun("scw", []string{}, visited) | |
wg.Wait() | |
close(outputSerial) | |
<-outputDone | |
} | |
func getProjectIDs() ([]string, error) { | |
cmd := exec.Command("scw", "account", "project", "list", "-o", "json") | |
var out bytes.Buffer | |
cmd.Stdout = &out | |
cmd.Stderr = &out | |
if err := cmd.Run(); err != nil { | |
return nil, fmt.Errorf("scw project list failed: %s", out.String()) | |
} | |
var raw []map[string]interface{} | |
if err := json.Unmarshal(out.Bytes(), &raw); err != nil { | |
return nil, fmt.Errorf("failed to parse JSON: %v", err) | |
} | |
var ids []string | |
for _, obj := range raw { | |
if id, ok := obj["id"].(string); ok { | |
ids = append(ids, id) | |
} | |
} | |
if len(ids) == 0 { | |
return nil, fmt.Errorf("no project IDs found") | |
} | |
return ids, nil | |
} | |
func getZonesFromHelp() ([]string, error) { | |
cmd := exec.Command("scw", "config", "set", "--help") | |
var out bytes.Buffer | |
cmd.Stdout = &out | |
cmd.Stderr = &out | |
if err := cmd.Run(); err != nil { | |
return nil, fmt.Errorf("scw config set --help failed: %s", out.String()) | |
} | |
re := regexp.MustCompile(`(?i)default-zone.*?\((.*?)\)`) | |
matches := re.FindStringSubmatch(out.String()) | |
if len(matches) < 2 { | |
return nil, fmt.Errorf("zone pattern not found") | |
} | |
var result []string | |
for _, z := range strings.Split(matches[1], "|") { | |
zone := strings.TrimSpace(z) | |
if zone != "" { | |
result = append(result, zone) | |
} | |
} | |
return result, nil | |
} | |
func getDefaultsFromInfo() (projectID, zone string) { | |
cmd := exec.Command("scw", "info") | |
var out bytes.Buffer | |
cmd.Stdout = &out | |
cmd.Stderr = &out | |
_ = cmd.Run() | |
lines := strings.Split(out.String(), "\n") | |
for _, line := range lines { | |
fields := strings.Fields(line) | |
if len(fields) >= 2 { | |
key, value := fields[0], fields[1] | |
if key == "default_project_id" { | |
projectID = value | |
} | |
if key == "default_zone" { | |
zone = value | |
} | |
} | |
} | |
return | |
} | |
func containsArg(help, name string) (bool, bool) { | |
bracketed := fmt.Sprintf("[%s]", name) | |
bracketedEq := fmt.Sprintf("[%s=", name) | |
if strings.Contains(help, bracketed) || strings.Contains(help, bracketedEq) { | |
return true, false | |
} | |
lines := strings.Split(help, "\n") | |
inArgs := false | |
for _, line := range lines { | |
trim := strings.TrimSpace(line) | |
if strings.HasPrefix(trim, "ARGS:") { | |
inArgs = true | |
continue | |
} | |
if strings.HasPrefix(trim, "FLAGS:") || strings.HasPrefix(trim, "GLOBAL FLAGS:") || trim == "" { | |
inArgs = false | |
} | |
if inArgs { | |
re := regexp.MustCompile(fmt.Sprintf(`^%s(\s|=|$)`, regexp.QuoteMeta(name))) | |
if re.MatchString(trim) { | |
return true, true | |
} | |
} | |
} | |
return false, false | |
} | |
func dumpHelpAndRun(base string, path []string, visited map[string]bool) { | |
if onlyPrefix != "" { | |
onlyParts := strings.Fields(onlyPrefix) | |
if len(path) > len(onlyParts) { | |
for i, part := range onlyParts { | |
if path[i] != part { | |
return | |
} | |
} | |
} else { | |
for i, part := range path { | |
if onlyParts[i] != part { | |
return | |
} | |
} | |
} | |
} | |
key := strings.Join(append([]string{base}, path...), " ") | |
if visited[key] { | |
return | |
} | |
visited[key] = true | |
helpOutput := getHelpOutput(base, path) | |
lines := strings.Split(helpOutput, "\n") | |
var subcommands []string | |
inCommandBlock := false | |
for _, line := range lines { | |
trim := strings.TrimSpace(line) | |
if strings.HasSuffix(trim, "COMMANDS:") { | |
inCommandBlock = true | |
continue | |
} | |
if inCommandBlock && strings.HasPrefix(line, " ") { | |
fields := strings.Fields(line) | |
if len(fields) > 0 && isCommand(fields[0]) { | |
subcommands = append(subcommands, fields[0]) | |
} | |
} | |
if trim == "" || strings.HasPrefix(trim, "FLAGS:") { | |
inCommandBlock = false | |
} | |
} | |
for _, sub := range subcommands { | |
if sub == "list" { | |
fullPath := append(path, sub) | |
listHelp := getHelpOutput(base, fullPath) | |
supportsProjectID, _ := containsArg(listHelp, "project-id") | |
supportsZone, _ := containsArg(listHelp, "zone") | |
if matchFilter != "" && !strings.Contains(strings.Join(path, " "), matchFilter) { | |
continue | |
} | |
projectList := []string{""} | |
if allProjects && supportsProjectID { | |
projectList = make([]string, len(projectIDs)) | |
copy(projectList, projectIDs) | |
} else if supportsProjectID { | |
projectList = []string{currentProjectID} | |
} | |
zoneList := []string{""} | |
if allZones && supportsZone { | |
zoneList = make([]string, len(zones)) | |
copy(zoneList, zones) | |
} else if supportsZone { | |
zoneList = []string{currentZone} | |
} | |
for _, project := range projectList { | |
for _, zone := range zoneList { | |
pathCopy := make([]string, len(path)+1) | |
copy(pathCopy, path) | |
pathCopy[len(path)] = sub | |
runListCommand(pathCopy, project, zone) | |
} | |
} | |
} else { | |
dumpHelpAndRun(base, append(path, sub), visited) | |
} | |
} | |
} | |
func getHelpOutput(base string, path []string) string { | |
cmd := exec.Command(base, append(path, "--help")...) | |
var out bytes.Buffer | |
cmd.Stdout = &out | |
cmd.Stderr = &out | |
_ = cmd.Run() | |
return out.String() | |
} | |
func runListCommand(path []string, projectID, zone string) { | |
wg.Add(1) | |
go func() { | |
defer wg.Done() | |
cmdArgs := path | |
if projectID != "" { | |
cmdArgs = append(cmdArgs, fmt.Sprintf("project-id=%s", projectID)) | |
} | |
if zone != "" { | |
cmdArgs = append(cmdArgs, fmt.Sprintf("zone=%s", zone)) | |
} | |
if outputFormat != "" { | |
cmdArgs = append(cmdArgs, "-o", outputFormat) | |
} | |
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) | |
defer cancel() | |
cmd := exec.CommandContext(ctx, "scw", cmdArgs...) | |
var outBuf, errBuf bytes.Buffer | |
cmd.Stdout = &outBuf | |
cmd.Stderr = &errBuf | |
err := cmd.Run() | |
var b strings.Builder | |
b.WriteString(fmt.Sprintf("\n=== scw %s ===\n\n", strings.Join(cmdArgs, " "))) | |
if err != nil { | |
if ctx.Err() == context.DeadlineExceeded { | |
b.WriteString("[error] Command timed out after 60 seconds.\n") | |
} else { | |
b.WriteString(fmt.Sprintf("[error] Command failed: %v\n", err)) | |
} | |
} | |
if errBuf.Len() > 0 { | |
b.WriteString("[stderr]\n" + errBuf.String()) | |
} | |
if outBuf.Len() > 0 { | |
b.WriteString(outBuf.String()) | |
} | |
outputSerial <- b.String() | |
}() | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment