github.com/qiniu/go-sdk/v7@v7.27.0/examples/sandbox_resources/main.go (about) 1 package main 2 3 import ( 4 "bufio" 5 "context" 6 "errors" 7 "fmt" 8 "log" 9 "os" 10 "path" 11 "strings" 12 "time" 13 14 "github.com/qiniu/go-sdk/v7/auth" 15 "github.com/qiniu/go-sdk/v7/sandbox" 16 ) 17 18 func main() { 19 loadEnvFileIfExists("examples/sandbox_resources/.env", ".env") 20 21 // 确保设置了环境变量 QINIU_API_KEY 22 apiKey := os.Getenv("QINIU_API_KEY") 23 if apiKey == "" { 24 log.Fatal("请设置 QINIU_API_KEY 环境变量") 25 } 26 27 apiURL := strings.TrimSpace(os.Getenv("QINIU_SANDBOX_API_URL")) 28 29 ctx := context.Background() 30 gitConfig := loadGitResourceConfig() 31 kodoConfig, err := loadKodoResourceConfig() 32 if err != nil { 33 log.Fatal(err) 34 } 35 36 if !gitConfig.Enabled() && !kodoConfig.Enabled() { 37 log.Fatal("请设置 GITHUB_TOKEN 和 QINIU_SANDBOX_GIT_REPO_URL,或设置 QINIU_SANDBOX_KODO_BUCKET,以启用至少一种资源") 38 } 39 40 var failures []error 41 if gitConfig.Enabled() { 42 client, err := sandbox.NewClient(&sandbox.Config{ 43 APIKey: apiKey, 44 Endpoint: apiURL, 45 }) 46 if err != nil { 47 failures = append(failures, fmt.Errorf("create Git resource client: %w", err)) 48 } else if err := runGitResourceExample(ctx, client, gitConfig); err != nil { 49 failures = append(failures, fmt.Errorf("Git resource example: %w", err)) 50 log.Printf("Git resource example failed: %v", err) 51 } 52 } 53 54 if kodoConfig.Enabled() { 55 if kodoConfig.AccessKey == "" || kodoConfig.SecretKey == "" { 56 failures = append(failures, errors.New("启用 Kodo 资源时请设置 QINIU_ACCESS_KEY 和 QINIU_SECRET_KEY 环境变量")) 57 } else { 58 client, err := sandbox.NewClient(&sandbox.Config{ 59 APIKey: apiKey, 60 Credentials: auth.New(kodoConfig.AccessKey, kodoConfig.SecretKey), 61 Endpoint: apiURL, 62 }) 63 if err != nil { 64 failures = append(failures, fmt.Errorf("create Kodo resource client: %w", err)) 65 } else if err := runKodoResourceExample(ctx, client, kodoConfig); err != nil { 66 failures = append(failures, fmt.Errorf("Kodo resource example: %w", err)) 67 log.Printf("Kodo resource example failed: %v", err) 68 } 69 } 70 } 71 72 if len(failures) > 0 { 73 log.Fatal(errors.Join(failures...)) 74 } 75 } 76 77 type gitResourceConfig struct { 78 RepoURL string 79 Token string 80 MountPath string 81 PushTest bool 82 TestFile string 83 CommitName string 84 CommitEmail string 85 CommitMessage string 86 PushBranch string 87 } 88 89 func loadGitResourceConfig() gitResourceConfig { 90 return gitResourceConfig{ 91 RepoURL: os.Getenv("QINIU_SANDBOX_GIT_REPO_URL"), 92 Token: os.Getenv("GITHUB_TOKEN"), 93 MountPath: "/workspace/repo", 94 PushTest: envBool("QINIU_SANDBOX_GIT_PUSH_TEST", false), 95 TestFile: envDefault("QINIU_SANDBOX_GIT_TEST_FILE", "sandbox-resource-write-test.txt"), 96 CommitName: envDefault("QINIU_SANDBOX_GIT_COMMIT_NAME", "Sandbox Resource Demo"), 97 CommitEmail: envDefault("QINIU_SANDBOX_GIT_COMMIT_EMAIL", "sandbox-resource-demo@example.com"), 98 CommitMessage: envDefault("QINIU_SANDBOX_GIT_COMMIT_MESSAGE", "test: update sandbox resource write file"), 99 PushBranch: os.Getenv("QINIU_SANDBOX_GIT_PUSH_BRANCH"), 100 } 101 } 102 103 func (c gitResourceConfig) Enabled() bool { 104 return c.Token != "" && c.RepoURL != "" 105 } 106 107 func runGitResourceExample(ctx context.Context, client *sandbox.Client, cfg gitResourceConfig) error { 108 resource := sandbox.GitRepositoryResource{ 109 Type: sandbox.GitRepositoryTypeGithub, 110 URL: cfg.RepoURL, 111 MountPath: cfg.MountPath, 112 AuthorizationToken: &cfg.Token, 113 } 114 resources := []sandbox.SandboxResourceSpec{{GitRepository: &resource}} 115 116 log.Println("Creating sandbox with Git repository resource...") 117 sb, info, err := client.CreateAndWait(ctx, sandbox.CreateParams{ 118 TemplateID: "base", 119 Resources: &resources, 120 }) 121 if err != nil { 122 return err 123 } 124 defer func() { 125 log.Println("Killing Git resource sandbox...") 126 cleanupCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 127 defer cancel() 128 _ = sb.Kill(cleanupCtx) 129 }() 130 131 log.Printf("Git resource sandbox created successfully! ID: %s, State: %s\n", sb.ID(), info.State) 132 if err := runAndLog(ctx, sb, "ls -la "+shellQuote(cfg.MountPath)+" | head -20"); err != nil { 133 return err 134 } 135 136 var pushURL string 137 if cfg.PushTest { 138 pushURL, err = githubTokenPushURL(cfg.RepoURL) 139 if err != nil { 140 return err 141 } 142 if err := runAndLog(ctx, sb, gitSyncBranchCommand(cfg.MountPath, cfg.PushBranch, pushURL), sandbox.WithTimeout(2*time.Minute), sandbox.WithEnvs(map[string]string{ 143 "GITHUB_TOKEN": cfg.Token, 144 "GIT_TERMINAL_PROMPT": "0", 145 })); err != nil { 146 return err 147 } 148 } 149 150 writeContent := "sandbox resource write test\n" + 151 "time: " + time.Now().UTC().Format(time.RFC3339Nano) + "\n" + 152 "sandbox: " + sb.ID() + "\n" 153 gitFilePath := path.Join(cfg.MountPath, cfg.TestFile) 154 if err := runAndLog(ctx, sb, writeFileCommand(gitFilePath, writeContent+"resource: git\n")); err != nil { 155 return err 156 } 157 if err := runAndLog(ctx, sb, "git -C "+shellQuote(cfg.MountPath)+" status --short "+shellQuote(cfg.TestFile)); err != nil { 158 return err 159 } 160 161 if cfg.PushTest { 162 if err := runAndLog(ctx, sb, gitCommitAndPushCommand(cfg.MountPath, cfg.TestFile, cfg.CommitName, cfg.CommitEmail, cfg.CommitMessage, cfg.PushBranch), sandbox.WithTimeout(2*time.Minute), sandbox.WithEnvs(map[string]string{ 163 "GITHUB_TOKEN": cfg.Token, 164 "GIT_TERMINAL_PROMPT": "0", 165 })); err != nil { 166 return err 167 } 168 } 169 return nil 170 } 171 172 type kodoResourceConfig struct { 173 Bucket string 174 MountPath string 175 Prefix string 176 ReadOnly *bool 177 AccessKey string 178 SecretKey string 179 WriteTest bool 180 TestFile string 181 } 182 183 func loadKodoResourceConfig() (kodoResourceConfig, error) { 184 readOnly, err := optionalEnvBool("QINIU_SANDBOX_KODO_READ_ONLY") 185 if err != nil { 186 return kodoResourceConfig{}, err 187 } 188 return kodoResourceConfig{ 189 Bucket: os.Getenv("QINIU_SANDBOX_KODO_BUCKET"), 190 MountPath: envDefault("QINIU_SANDBOX_KODO_MOUNT_PATH", "/workspace/kodo"), 191 Prefix: os.Getenv("QINIU_SANDBOX_KODO_PREFIX"), 192 ReadOnly: readOnly, 193 AccessKey: os.Getenv("QINIU_ACCESS_KEY"), 194 SecretKey: os.Getenv("QINIU_SECRET_KEY"), 195 WriteTest: envBool("QINIU_SANDBOX_KODO_WRITE_TEST", true), 196 TestFile: envDefault("QINIU_SANDBOX_KODO_TEST_FILE", "sandbox-resource-write-test.txt"), 197 }, nil 198 } 199 200 func (c kodoResourceConfig) Enabled() bool { 201 return c.Bucket != "" 202 } 203 204 func runKodoResourceExample(ctx context.Context, client *sandbox.Client, cfg kodoResourceConfig) error { 205 resource := sandbox.KodoResource{ 206 Bucket: cfg.Bucket, 207 MountPath: cfg.MountPath, 208 } 209 if cfg.Prefix != "" { 210 resource.Prefix = &cfg.Prefix 211 } 212 if cfg.ReadOnly != nil { 213 resource.ReadOnly = cfg.ReadOnly 214 } 215 resources := []sandbox.SandboxResourceSpec{{Kodo: &resource}} 216 217 log.Println("Creating sandbox with Kodo bucket resource...") 218 sb, info, err := client.CreateAndWait(ctx, sandbox.CreateParams{ 219 TemplateID: "base", 220 Resources: &resources, 221 }) 222 if err != nil { 223 return err 224 } 225 defer func() { 226 log.Println("Killing Kodo resource sandbox...") 227 cleanupCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 228 defer cancel() 229 _ = sb.Kill(cleanupCtx) 230 }() 231 232 log.Printf("Kodo resource sandbox created successfully! ID: %s, State: %s\n", sb.ID(), info.State) 233 if err := runAndLog(ctx, sb, "ls -la "+shellQuote(cfg.MountPath)+" | head -20"); err != nil { 234 return err 235 } 236 237 if cfg.WriteTest { 238 if cfg.ReadOnlyEnabled() { 239 log.Println("Skipping Kodo write test because QINIU_SANDBOX_KODO_READ_ONLY=true") 240 return nil 241 } 242 writeContent := "sandbox resource write test\n" + 243 "time: " + time.Now().UTC().Format(time.RFC3339Nano) + "\n" + 244 "sandbox: " + sb.ID() + "\n" + 245 "resource: kodo\n" 246 if err := runAndLog(ctx, sb, writeFileCommand(path.Join(cfg.MountPath, cfg.TestFile), writeContent)); err != nil { 247 return err 248 } 249 } 250 return nil 251 } 252 253 func (c kodoResourceConfig) ReadOnlyEnabled() bool { 254 return c.ReadOnly != nil && *c.ReadOnly 255 } 256 257 func loadEnvFileIfExists(paths ...string) { 258 for _, p := range paths { 259 file, err := os.Open(p) 260 if err != nil { 261 continue 262 } 263 264 scanner := bufio.NewScanner(file) 265 for scanner.Scan() { 266 line := strings.TrimSpace(scanner.Text()) 267 if line == "" || strings.HasPrefix(line, "#") { 268 continue 269 } 270 key, value, ok := strings.Cut(line, "=") 271 if !ok { 272 continue 273 } 274 key = strings.TrimSpace(key) 275 if key == "" { 276 continue 277 } 278 value = strings.TrimSpace(value) 279 value = strings.Trim(value, `"'`) 280 value = strings.TrimSpace(value) 281 if _, exists := os.LookupEnv(key); !exists { 282 _ = os.Setenv(key, value) 283 } 284 } 285 if err := file.Close(); err != nil { 286 log.Fatalf("Failed to close .env file %s: %v", p, err) 287 } 288 if err := scanner.Err(); err != nil { 289 log.Fatalf("Failed to read .env file %s: %v", p, err) 290 } 291 log.Printf("Loaded environment from %s", p) 292 return 293 } 294 } 295 296 func envDefault(key, fallback string) string { 297 value := os.Getenv(key) 298 if value == "" { 299 return fallback 300 } 301 return value 302 } 303 304 func envBool(key string, fallback bool) bool { 305 value := strings.ToLower(strings.TrimSpace(os.Getenv(key))) 306 switch value { 307 case "": 308 return fallback 309 case "1", "true", "yes", "on": 310 return true 311 default: 312 return false 313 } 314 } 315 316 func optionalEnvBool(key string) (*bool, error) { 317 value := strings.ToLower(strings.TrimSpace(os.Getenv(key))) 318 switch value { 319 case "": 320 return nil, nil 321 case "1", "true", "yes", "on": 322 v := true 323 return &v, nil 324 case "0", "false", "no", "off": 325 v := false 326 return &v, nil 327 default: 328 return nil, fmt.Errorf("%s must be one of 1, true, yes, on, 0, false, no, or off", key) 329 } 330 } 331 332 func runAndLog(ctx context.Context, sb *sandbox.Sandbox, cmd string, opts ...sandbox.CommandOption) error { 333 log.Printf("Executing command in sandbox:\n$ %s\n", cmd) 334 335 result, err := sb.Commands().Run(ctx, cmd, opts...) 336 if err != nil { 337 return fmt.Errorf("run command: %w", err) 338 } 339 340 log.Printf("ExitCode: %d\n", result.ExitCode) 341 if result.Stdout != "" { 342 log.Printf("Stdout:\n%s\n", result.Stdout) 343 } 344 if result.Stderr != "" { 345 log.Printf("Stderr:\n%s\n", result.Stderr) 346 } 347 if result.ExitCode != 0 { 348 return fmt.Errorf("command failed with exit code %d", result.ExitCode) 349 } 350 return nil 351 } 352 353 func githubTokenPushURL(repoURL string) (string, error) { 354 repoURL = strings.TrimSpace(repoURL) 355 repoURL = strings.TrimSuffix(repoURL, "/") 356 switch { 357 case strings.HasPrefix(repoURL, "https://"): 358 return "https://x-access-token:${GITHUB_TOKEN}@" + strings.TrimPrefix(repoURL, "https://"), nil 359 case strings.HasPrefix(repoURL, "http://"): 360 return "https://x-access-token:${GITHUB_TOKEN}@" + strings.TrimPrefix(repoURL, "http://"), nil 361 case strings.HasPrefix(repoURL, "git@github.com:"): 362 return "https://x-access-token:${GITHUB_TOKEN}@github.com/" + strings.TrimPrefix(repoURL, "git@github.com:"), nil 363 default: 364 return "", fmt.Errorf("unsupported GitHub repository URL: %s", repoURL) 365 } 366 } 367 368 func gitSyncBranchCommand(repoPath, branch, pushURL string) string { 369 branchExpr := "branch=" + shellQuote(branch) 370 if branch == "" { 371 branchExpr = "branch=$(git -C " + shellQuote(repoPath) + " rev-parse --abbrev-ref HEAD)" 372 } 373 return strings.Join([]string{ 374 "set -e", 375 branchExpr, 376 "test \"$branch\" != HEAD", 377 "git -C " + shellQuote(repoPath) + " remote set-url origin " + shellDoubleQuote(pushURL), 378 "git -C " + shellQuote(repoPath) + " fetch origin \"$branch\"", 379 "git -C " + shellQuote(repoPath) + " checkout \"$branch\"", 380 "git -C " + shellQuote(repoPath) + " reset --hard \"origin/$branch\"", 381 }, " && ") 382 } 383 384 func gitCommitAndPushCommand(repoPath, filePath, name, email, message, branch string) string { 385 branchExpr := "branch=" + shellQuote(branch) 386 if branch == "" { 387 branchExpr = "branch=$(git -C " + shellQuote(repoPath) + " rev-parse --abbrev-ref HEAD)" 388 } 389 return strings.Join([]string{ 390 "set -e", 391 branchExpr, 392 "test \"$branch\" != HEAD", 393 "git -C " + shellQuote(repoPath) + " config user.name " + shellQuote(name), 394 "git -C " + shellQuote(repoPath) + " config user.email " + shellQuote(email), 395 "git -C " + shellQuote(repoPath) + " add " + shellQuote(filePath), 396 "git -C " + shellQuote(repoPath) + " commit -m " + shellQuote(message), 397 "for attempt in 1 2 3; do if git -C " + shellQuote(repoPath) + " push origin HEAD:\"$branch\"; then break; fi; if [ \"$attempt\" = 3 ]; then exit 1; fi; sleep $((attempt * 2)); done", 398 }, " && ") 399 } 400 401 func writeFileCommand(filePath, content string) string { 402 dir := path.Dir(filePath) 403 return strings.Join([]string{ 404 "mkdir -p " + shellQuote(dir), 405 "printf %s " + shellQuote(content) + " > " + shellQuote(filePath), 406 "ls -la " + shellQuote(filePath), 407 "sed -n '1,20p' " + shellQuote(filePath), 408 }, " && ") 409 } 410 411 func shellQuote(s string) string { 412 return "'" + strings.ReplaceAll(s, "'", "'\\''") + "'" 413 } 414 415 func shellDoubleQuote(s string) string { 416 const tokenPlaceholder = "__GITHUB_TOKEN_PLACEHOLDER__" 417 s = strings.ReplaceAll(s, "${GITHUB_TOKEN}", tokenPlaceholder) 418 replacer := strings.NewReplacer(`\`, `\\`, `"`, `\"`, `$`, `\$`, "`", "\\`") 419 return `"` + strings.ReplaceAll(replacer.Replace(s), tokenPlaceholder, "${GITHUB_TOKEN}") + `"` 420 }