github.com/fastly/cli@v1.7.2-0.20240304164155-9d0f1d77c3bf/pkg/commands/logging/s3/s3_integration_test.go (about) 1 package s3_test 2 3 import ( 4 "bytes" 5 "errors" 6 "io" 7 "strings" 8 "testing" 9 10 "github.com/fastly/go-fastly/v9/fastly" 11 12 "github.com/fastly/cli/pkg/app" 13 "github.com/fastly/cli/pkg/global" 14 "github.com/fastly/cli/pkg/mock" 15 "github.com/fastly/cli/pkg/testutil" 16 ) 17 18 func TestS3Create(t *testing.T) { 19 args := testutil.Args 20 scenarios := []struct { 21 args []string 22 api mock.API 23 wantError string 24 wantOutput string 25 }{ 26 { 27 args: args("logging s3 create --service-id 123 --version 1 --name log --bucket log --autoclone"), 28 api: mock.API{ 29 ListVersionsFn: testutil.ListVersions, 30 CloneVersionFn: testutil.CloneVersionResult(4), 31 }, 32 wantError: "error parsing arguments: the --access-key and --secret-key flags or the --iam-role flag must be provided", 33 }, 34 { 35 args: args("logging s3 create --service-id 123 --version 1 --name log --bucket log --secret-key bar --iam-role arn:aws:iam::123456789012:role/S3Access --autoclone"), 36 api: mock.API{ 37 ListVersionsFn: testutil.ListVersions, 38 CloneVersionFn: testutil.CloneVersionResult(4), 39 }, 40 wantError: "error parsing arguments: the --access-key and --secret-key flags are mutually exclusive with the --iam-role flag", 41 }, 42 { 43 args: args("logging s3 create --service-id 123 --version 1 --name log --bucket log --access-key foo --iam-role arn:aws:iam::123456789012:role/S3Access --autoclone"), 44 api: mock.API{ 45 ListVersionsFn: testutil.ListVersions, 46 CloneVersionFn: testutil.CloneVersionResult(4), 47 }, 48 wantError: "error parsing arguments: the --access-key and --secret-key flags are mutually exclusive with the --iam-role flag", 49 }, 50 { 51 args: args("logging s3 create --service-id 123 --version 1 --name log --bucket log --access-key foo --secret-key bar --iam-role arn:aws:iam::123456789012:role/S3Access --autoclone"), 52 api: mock.API{ 53 ListVersionsFn: testutil.ListVersions, 54 CloneVersionFn: testutil.CloneVersionResult(4), 55 }, 56 wantError: "error parsing arguments: the --access-key and --secret-key flags are mutually exclusive with the --iam-role flag", 57 }, 58 { 59 args: args("logging s3 create --service-id 123 --version 1 --name log --bucket log --access-key foo --secret-key bar --autoclone"), 60 api: mock.API{ 61 ListVersionsFn: testutil.ListVersions, 62 CloneVersionFn: testutil.CloneVersionResult(4), 63 CreateS3Fn: createS3OK, 64 }, 65 wantOutput: "Created S3 logging endpoint log (service 123 version 4)", 66 }, 67 { 68 args: args("logging s3 create --service-id 123 --version 1 --name log --bucket log --access-key foo --secret-key bar --autoclone"), 69 api: mock.API{ 70 ListVersionsFn: testutil.ListVersions, 71 CloneVersionFn: testutil.CloneVersionResult(4), 72 CreateS3Fn: createS3Error, 73 }, 74 wantError: errTest.Error(), 75 }, 76 { 77 args: args("logging s3 create --service-id 123 --version 1 --name log2 --bucket log --iam-role arn:aws:iam::123456789012:role/S3Access --autoclone"), 78 api: mock.API{ 79 ListVersionsFn: testutil.ListVersions, 80 CloneVersionFn: testutil.CloneVersionResult(4), 81 CreateS3Fn: createS3OK, 82 }, 83 wantOutput: "Created S3 logging endpoint log2 (service 123 version 4)", 84 }, 85 { 86 args: args("logging s3 create --service-id 123 --version 1 --name log2 --bucket log --iam-role arn:aws:iam::123456789012:role/S3Access --autoclone"), 87 api: mock.API{ 88 ListVersionsFn: testutil.ListVersions, 89 CloneVersionFn: testutil.CloneVersionResult(4), 90 CreateS3Fn: createS3Error, 91 }, 92 wantError: errTest.Error(), 93 }, 94 { 95 args: args("logging s3 create --service-id 123 --version 1 --name log --bucket log --iam-role arn:aws:iam::123456789012:role/S3Access --compression-codec zstd --gzip-level 9 --autoclone"), 96 api: mock.API{ 97 ListVersionsFn: testutil.ListVersions, 98 CloneVersionFn: testutil.CloneVersionResult(4), 99 }, 100 wantError: "error parsing arguments: the --compression-codec flag is mutually exclusive with the --gzip-level flag", 101 }, 102 } 103 for testcaseIdx := range scenarios { 104 testcase := &scenarios[testcaseIdx] 105 t.Run(strings.Join(testcase.args, " "), func(t *testing.T) { 106 var stdout bytes.Buffer 107 app.Init = func(_ []string, _ io.Reader) (*global.Data, error) { 108 opts := testutil.MockGlobalData(testcase.args, &stdout) 109 opts.APIClientFactory = mock.APIClient(testcase.api) 110 return opts, nil 111 } 112 err := app.Run(testcase.args, nil) 113 testutil.AssertErrorContains(t, err, testcase.wantError) 114 testutil.AssertStringContains(t, stdout.String(), testcase.wantOutput) 115 }) 116 } 117 } 118 119 func TestS3List(t *testing.T) { 120 args := testutil.Args 121 scenarios := []struct { 122 args []string 123 api mock.API 124 wantError string 125 wantOutput string 126 }{ 127 { 128 args: args("logging s3 list --service-id 123 --version 1"), 129 api: mock.API{ 130 ListVersionsFn: testutil.ListVersions, 131 ListS3sFn: listS3sOK, 132 }, 133 wantOutput: listS3sShortOutput, 134 }, 135 { 136 args: args("logging s3 list --service-id 123 --version 1 --verbose"), 137 api: mock.API{ 138 ListVersionsFn: testutil.ListVersions, 139 ListS3sFn: listS3sOK, 140 }, 141 wantOutput: listS3sVerboseOutput, 142 }, 143 { 144 args: args("logging s3 list --service-id 123 --version 1 -v"), 145 api: mock.API{ 146 ListVersionsFn: testutil.ListVersions, 147 ListS3sFn: listS3sOK, 148 }, 149 wantOutput: listS3sVerboseOutput, 150 }, 151 { 152 args: args("logging s3 --verbose list --service-id 123 --version 1"), 153 api: mock.API{ 154 ListVersionsFn: testutil.ListVersions, 155 ListS3sFn: listS3sOK, 156 }, 157 wantOutput: listS3sVerboseOutput, 158 }, 159 { 160 args: args("logging -v s3 list --service-id 123 --version 1"), 161 api: mock.API{ 162 ListVersionsFn: testutil.ListVersions, 163 ListS3sFn: listS3sOK, 164 }, 165 wantOutput: listS3sVerboseOutput, 166 }, 167 { 168 args: args("logging s3 list --service-id 123 --version 1"), 169 api: mock.API{ 170 ListVersionsFn: testutil.ListVersions, 171 ListS3sFn: listS3sError, 172 }, 173 wantError: errTest.Error(), 174 }, 175 } 176 for testcaseIdx := range scenarios { 177 testcase := &scenarios[testcaseIdx] 178 t.Run(strings.Join(testcase.args, " "), func(t *testing.T) { 179 var stdout bytes.Buffer 180 app.Init = func(_ []string, _ io.Reader) (*global.Data, error) { 181 opts := testutil.MockGlobalData(testcase.args, &stdout) 182 opts.APIClientFactory = mock.APIClient(testcase.api) 183 return opts, nil 184 } 185 err := app.Run(testcase.args, nil) 186 testutil.AssertErrorContains(t, err, testcase.wantError) 187 testutil.AssertString(t, testcase.wantOutput, stdout.String()) 188 }) 189 } 190 } 191 192 func TestS3Describe(t *testing.T) { 193 args := testutil.Args 194 scenarios := []struct { 195 args []string 196 api mock.API 197 wantError string 198 wantOutput string 199 }{ 200 { 201 args: args("logging s3 describe --service-id 123 --version 1"), 202 wantError: "error parsing arguments: required flag --name not provided", 203 }, 204 { 205 args: args("logging s3 describe --service-id 123 --version 1 --name logs"), 206 api: mock.API{ 207 ListVersionsFn: testutil.ListVersions, 208 GetS3Fn: getS3Error, 209 }, 210 wantError: errTest.Error(), 211 }, 212 { 213 args: args("logging s3 describe --service-id 123 --version 1 --name logs"), 214 api: mock.API{ 215 ListVersionsFn: testutil.ListVersions, 216 GetS3Fn: getS3OK, 217 }, 218 wantOutput: describeS3Output, 219 }, 220 } 221 for testcaseIdx := range scenarios { 222 testcase := &scenarios[testcaseIdx] 223 t.Run(strings.Join(testcase.args, " "), func(t *testing.T) { 224 var stdout bytes.Buffer 225 app.Init = func(_ []string, _ io.Reader) (*global.Data, error) { 226 opts := testutil.MockGlobalData(testcase.args, &stdout) 227 opts.APIClientFactory = mock.APIClient(testcase.api) 228 return opts, nil 229 } 230 err := app.Run(testcase.args, nil) 231 testutil.AssertErrorContains(t, err, testcase.wantError) 232 testutil.AssertString(t, testcase.wantOutput, stdout.String()) 233 }) 234 } 235 } 236 237 func TestS3Update(t *testing.T) { 238 args := testutil.Args 239 scenarios := []struct { 240 args []string 241 api mock.API 242 wantError string 243 wantOutput string 244 }{ 245 { 246 args: args("logging s3 update --service-id 123 --version 1 --new-name log"), 247 wantError: "error parsing arguments: required flag --name not provided", 248 }, 249 { 250 args: args("logging s3 update --service-id 123 --version 1 --name logs --new-name log --autoclone"), 251 api: mock.API{ 252 ListVersionsFn: testutil.ListVersions, 253 CloneVersionFn: testutil.CloneVersionResult(4), 254 UpdateS3Fn: updateS3Error, 255 }, 256 wantError: errTest.Error(), 257 }, 258 { 259 args: args("logging s3 update --service-id 123 --version 1 --name logs --new-name log --autoclone"), 260 api: mock.API{ 261 ListVersionsFn: testutil.ListVersions, 262 CloneVersionFn: testutil.CloneVersionResult(4), 263 UpdateS3Fn: updateS3OK, 264 }, 265 wantOutput: "Updated S3 logging endpoint log (service 123 version 4)", 266 }, 267 { 268 args: args("logging s3 update --service-id 123 --version 1 --name logs --access-key foo --secret-key bar --iam-role --autoclone"), 269 api: mock.API{ 270 ListVersionsFn: testutil.ListVersions, 271 CloneVersionFn: testutil.CloneVersionResult(4), 272 UpdateS3Fn: updateS3OK, 273 }, 274 wantOutput: "Updated S3 logging endpoint log (service 123 version 4)", 275 }, 276 } 277 for testcaseIdx := range scenarios { 278 testcase := &scenarios[testcaseIdx] 279 t.Run(strings.Join(testcase.args, " "), func(t *testing.T) { 280 var stdout bytes.Buffer 281 app.Init = func(_ []string, _ io.Reader) (*global.Data, error) { 282 opts := testutil.MockGlobalData(testcase.args, &stdout) 283 opts.APIClientFactory = mock.APIClient(testcase.api) 284 return opts, nil 285 } 286 err := app.Run(testcase.args, nil) 287 testutil.AssertErrorContains(t, err, testcase.wantError) 288 testutil.AssertStringContains(t, stdout.String(), testcase.wantOutput) 289 }) 290 } 291 } 292 293 func TestS3Delete(t *testing.T) { 294 args := testutil.Args 295 scenarios := []struct { 296 args []string 297 api mock.API 298 wantError string 299 wantOutput string 300 }{ 301 { 302 args: args("logging s3 delete --service-id 123 --version 1"), 303 wantError: "error parsing arguments: required flag --name not provided", 304 }, 305 { 306 args: args("logging s3 delete --service-id 123 --version 1 --name logs --autoclone"), 307 api: mock.API{ 308 ListVersionsFn: testutil.ListVersions, 309 CloneVersionFn: testutil.CloneVersionResult(4), 310 DeleteS3Fn: deleteS3Error, 311 }, 312 wantError: errTest.Error(), 313 }, 314 { 315 args: args("logging s3 delete --service-id 123 --version 1 --name logs --autoclone"), 316 api: mock.API{ 317 ListVersionsFn: testutil.ListVersions, 318 CloneVersionFn: testutil.CloneVersionResult(4), 319 DeleteS3Fn: deleteS3OK, 320 }, 321 wantOutput: "Deleted S3 logging endpoint logs (service 123 version 4)", 322 }, 323 } 324 for testcaseIdx := range scenarios { 325 testcase := &scenarios[testcaseIdx] 326 t.Run(strings.Join(testcase.args, " "), func(t *testing.T) { 327 var stdout bytes.Buffer 328 app.Init = func(_ []string, _ io.Reader) (*global.Data, error) { 329 opts := testutil.MockGlobalData(testcase.args, &stdout) 330 opts.APIClientFactory = mock.APIClient(testcase.api) 331 return opts, nil 332 } 333 err := app.Run(testcase.args, nil) 334 testutil.AssertErrorContains(t, err, testcase.wantError) 335 testutil.AssertStringContains(t, stdout.String(), testcase.wantOutput) 336 }) 337 } 338 } 339 340 var errTest = errors.New("fixture error") 341 342 func createS3OK(i *fastly.CreateS3Input) (*fastly.S3, error) { 343 return &fastly.S3{ 344 ServiceID: fastly.ToPointer(i.ServiceID), 345 ServiceVersion: fastly.ToPointer(i.ServiceVersion), 346 Name: i.Name, 347 CompressionCodec: fastly.ToPointer("zstd"), 348 }, nil 349 } 350 351 func createS3Error(_ *fastly.CreateS3Input) (*fastly.S3, error) { 352 return nil, errTest 353 } 354 355 func listS3sOK(i *fastly.ListS3sInput) ([]*fastly.S3, error) { 356 return []*fastly.S3{ 357 { 358 ServiceID: fastly.ToPointer(i.ServiceID), 359 ServiceVersion: fastly.ToPointer(i.ServiceVersion), 360 Name: fastly.ToPointer("logs"), 361 BucketName: fastly.ToPointer("my-logs"), 362 AccessKey: fastly.ToPointer("1234"), 363 SecretKey: fastly.ToPointer("-----BEGIN RSA PRIVATE KEY-----MIIEogIBAAKCA"), 364 IAMRole: fastly.ToPointer("xyz"), 365 Domain: fastly.ToPointer("https://s3.us-east-1.amazonaws.com"), 366 Path: fastly.ToPointer("logs/"), 367 Period: fastly.ToPointer(3600), 368 Format: fastly.ToPointer(`%h %l %u %t "%r" %>s %b`), 369 FormatVersion: fastly.ToPointer(2), 370 MessageType: fastly.ToPointer("classic"), 371 ResponseCondition: fastly.ToPointer("Prevent default logging"), 372 TimestampFormat: fastly.ToPointer("%Y-%m-%dT%H:%M:%S.000"), 373 Redundancy: fastly.ToPointer(fastly.S3RedundancyStandard), 374 Placement: fastly.ToPointer("none"), 375 PublicKey: fastly.ToPointer(pgpPublicKey()), 376 ServerSideEncryption: fastly.ToPointer(fastly.S3ServerSideEncryptionKMS), 377 ServerSideEncryptionKMSKeyID: fastly.ToPointer("1234"), 378 CompressionCodec: fastly.ToPointer("zstd"), 379 }, 380 { 381 ServiceID: fastly.ToPointer(i.ServiceID), 382 ServiceVersion: fastly.ToPointer(i.ServiceVersion), 383 Name: fastly.ToPointer("analytics"), 384 BucketName: fastly.ToPointer("analytics"), 385 AccessKey: fastly.ToPointer("1234"), 386 SecretKey: fastly.ToPointer("-----BEGIN RSA PRIVATE KEY-----MIIEogIBAAKCA"), 387 Domain: fastly.ToPointer("https://s3.us-east-2.amazonaws.com"), 388 Path: fastly.ToPointer("logs/"), 389 Period: fastly.ToPointer(86400), 390 Format: fastly.ToPointer(`%h %l %u %t "%r" %>s %b`), 391 FormatVersion: fastly.ToPointer(2), 392 MessageType: fastly.ToPointer("classic"), 393 ResponseCondition: fastly.ToPointer("Prevent default logging"), 394 TimestampFormat: fastly.ToPointer("%Y-%m-%dT%H:%M:%S.000"), 395 Redundancy: fastly.ToPointer(fastly.S3RedundancyStandard), 396 Placement: fastly.ToPointer("none"), 397 PublicKey: fastly.ToPointer(pgpPublicKey()), 398 ServerSideEncryption: fastly.ToPointer(fastly.S3ServerSideEncryptionKMS), 399 ServerSideEncryptionKMSKeyID: fastly.ToPointer("1234"), 400 FileMaxBytes: fastly.ToPointer(12345), 401 CompressionCodec: fastly.ToPointer("zstd"), 402 }, 403 }, nil 404 } 405 406 func listS3sError(_ *fastly.ListS3sInput) ([]*fastly.S3, error) { 407 return nil, errTest 408 } 409 410 var listS3sShortOutput = strings.TrimSpace(` 411 SERVICE VERSION NAME 412 123 1 logs 413 123 1 analytics 414 `) + "\n" 415 416 var listS3sVerboseOutput = strings.TrimSpace(` 417 Fastly API endpoint: https://api.fastly.com 418 Fastly API token provided via config file (profile: user) 419 420 Service ID (via --service-id): 123 421 422 Version: 1 423 S3 1/2 424 Service ID: 123 425 Version: 1 426 Name: logs 427 Bucket: my-logs 428 Access key: 1234 429 Secret key: -----BEGIN RSA PRIVATE KEY-----MIIEogIBAAKCA 430 IAM role: xyz 431 Path: logs/ 432 Period: 3600 433 GZip level: 0 434 Format: %h %l %u %t "%r" %>s %b 435 Format version: 2 436 Response condition: Prevent default logging 437 Message type: classic 438 Timestamp format: %Y-%m-%dT%H:%M:%S.000 439 Placement: none 440 Public key: `+pgpPublicKey()+` 441 Redundancy: standard 442 Server-side encryption: aws:kms 443 Server-side encryption KMS key ID: aws:kms 444 File max bytes: 0 445 Compression codec: zstd 446 S3 2/2 447 Service ID: 123 448 Version: 1 449 Name: analytics 450 Bucket: analytics 451 Access key: 1234 452 Secret key: -----BEGIN RSA PRIVATE KEY-----MIIEogIBAAKCA 453 Path: logs/ 454 Period: 86400 455 GZip level: 0 456 Format: %h %l %u %t "%r" %>s %b 457 Format version: 2 458 Response condition: Prevent default logging 459 Message type: classic 460 Timestamp format: %Y-%m-%dT%H:%M:%S.000 461 Placement: none 462 Public key: `+pgpPublicKey()+` 463 Redundancy: standard 464 Server-side encryption: aws:kms 465 Server-side encryption KMS key ID: aws:kms 466 File max bytes: 12345 467 Compression codec: zstd 468 `) + "\n\n" 469 470 func getS3OK(i *fastly.GetS3Input) (*fastly.S3, error) { 471 return &fastly.S3{ 472 ServiceID: fastly.ToPointer(i.ServiceID), 473 ServiceVersion: fastly.ToPointer(i.ServiceVersion), 474 Name: fastly.ToPointer("logs"), 475 BucketName: fastly.ToPointer("my-logs"), 476 AccessKey: fastly.ToPointer("1234"), 477 SecretKey: fastly.ToPointer("-----BEGIN RSA PRIVATE KEY-----MIIEogIBAAKCA"), 478 Domain: fastly.ToPointer("https://s3.us-east-1.amazonaws.com"), 479 Path: fastly.ToPointer("logs/"), 480 Period: fastly.ToPointer(3600), 481 Format: fastly.ToPointer(`%h %l %u %t "%r" %>s %b`), 482 FormatVersion: fastly.ToPointer(2), 483 MessageType: fastly.ToPointer("classic"), 484 ResponseCondition: fastly.ToPointer("Prevent default logging"), 485 TimestampFormat: fastly.ToPointer("%Y-%m-%dT%H:%M:%S.000"), 486 Redundancy: fastly.ToPointer(fastly.S3RedundancyStandard), 487 Placement: fastly.ToPointer("none"), 488 PublicKey: fastly.ToPointer(pgpPublicKey()), 489 ServerSideEncryption: fastly.ToPointer(fastly.S3ServerSideEncryptionKMS), 490 ServerSideEncryptionKMSKeyID: fastly.ToPointer("1234"), 491 CompressionCodec: fastly.ToPointer("zstd"), 492 }, nil 493 } 494 495 func getS3Error(_ *fastly.GetS3Input) (*fastly.S3, error) { 496 return nil, errTest 497 } 498 499 var describeS3Output = "\n" + strings.TrimSpace(` 500 Access key: 1234 501 Bucket: my-logs 502 Compression codec: zstd 503 File max bytes: 0 504 Format: %h %l %u %t "%r" %>s %b 505 Format version: 2 506 GZip level: 0 507 Message type: classic 508 Name: logs 509 Path: logs/ 510 Period: 3600 511 Placement: none 512 Public key: `+pgpPublicKey()+` 513 Redundancy: standard 514 Response condition: Prevent default logging 515 Secret key: -----BEGIN RSA PRIVATE KEY-----MIIEogIBAAKCA 516 Server-side encryption: aws:kms 517 Server-side encryption KMS key ID: aws:kms 518 Service ID: 123 519 Timestamp format: %Y-%m-%dT%H:%M:%S.000 520 Version: 1 521 `) + "\n" 522 523 func updateS3OK(i *fastly.UpdateS3Input) (*fastly.S3, error) { 524 return &fastly.S3{ 525 ServiceID: fastly.ToPointer(i.ServiceID), 526 ServiceVersion: fastly.ToPointer(i.ServiceVersion), 527 Name: fastly.ToPointer("log"), 528 BucketName: fastly.ToPointer("my-logs"), 529 AccessKey: fastly.ToPointer("1234"), 530 SecretKey: fastly.ToPointer("-----BEGIN RSA PRIVATE KEY-----MIIEogIBAAKCA"), 531 Domain: fastly.ToPointer("https://s3.us-east-1.amazonaws.com"), 532 Path: fastly.ToPointer("logs/"), 533 Period: fastly.ToPointer(3600), 534 Format: fastly.ToPointer(`%h %l %u %t "%r" %>s %b`), 535 FormatVersion: fastly.ToPointer(2), 536 MessageType: fastly.ToPointer("classic"), 537 ResponseCondition: fastly.ToPointer("Prevent default logging"), 538 TimestampFormat: fastly.ToPointer("%Y-%m-%dT%H:%M:%S.000"), 539 Redundancy: fastly.ToPointer(fastly.S3RedundancyStandard), 540 Placement: fastly.ToPointer("none"), 541 PublicKey: fastly.ToPointer(pgpPublicKey()), 542 ServerSideEncryption: fastly.ToPointer(fastly.S3ServerSideEncryptionKMS), 543 ServerSideEncryptionKMSKeyID: fastly.ToPointer("1234"), 544 CompressionCodec: fastly.ToPointer("zstd"), 545 }, nil 546 } 547 548 func updateS3Error(_ *fastly.UpdateS3Input) (*fastly.S3, error) { 549 return nil, errTest 550 } 551 552 func deleteS3OK(_ *fastly.DeleteS3Input) error { 553 return nil 554 } 555 556 func deleteS3Error(_ *fastly.DeleteS3Input) error { 557 return errTest 558 } 559 560 // pgpPublicKey returns a PEM encoded PGP public key suitable for testing. 561 func pgpPublicKey() string { 562 return strings.TrimSpace(`-----BEGIN PGP PUBLIC KEY BLOCK----- 563 mQENBFyUD8sBCACyFnB39AuuTygseek+eA4fo0cgwva6/FSjnWq7riouQee8GgQ/ 564 ibXTRyv4iVlwI12GswvMTIy7zNvs1R54i0qvsLr+IZ4GVGJqs6ZJnvQcqe3xPoR4 565 8AnBfw90o32r/LuHf6QCJXi+AEu35koNlNAvLJ2B+KACaNB7N0EeWmqpV/1V2k9p 566 lDYk+th7LcCuaFNGqKS/PrMnnMqR6VDLCjHhNx4KR79b0Twm/2qp6an3hyNRu8Gn 567 dwxpf1/BUu3JWf+LqkN4Y3mbOmSUL3MaJNvyQguUzTfS0P0uGuBDHrJCVkMZCzDB 568 89ag55jCPHyGeHBTd02gHMWzsg3WMBWvCsrzABEBAAG0JXRlcnJhZm9ybSAodGVz 569 dCkgPHRlc3RAdGVycmFmb3JtLmNvbT6JAU4EEwEIADgWIQSHYyc6Kj9l6HzQsau6 570 vFFc9jxV/wUCXJQPywIbAwULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRC6vFFc 571 9jxV/815CAClb32OxV7wG01yF97TzlyTl8TnvjMtoG29Mw4nSyg+mjM3b8N7iXm9 572 OLX59fbDAWtBSldSZE22RXd3CvlFOG/EnKBXSjBtEqfyxYSnyOPkMPBYWGL/ApkX 573 SvPYJ4LKdvipYToKFh3y9kk2gk1DcDBDyaaHvR+3rv1u3aoy7/s2EltAfDS3ZQIq 574 7/cWTLJml/lleeB/Y6rPj8xqeCYhE5ahw9gsV/Mdqatl24V9Tks30iijx0Hhw+Gx 575 kATUikMGr2GDVqoIRga5kXI7CzYff4rkc0Twn47fMHHHe/KY9M2yVnMHUXmAZwbG 576 M1cMI/NH1DjevCKdGBLcRJlhuLPKF/anuQENBFyUD8sBCADIpd7r7GuPd6n/Ikxe 577 u6h7umV6IIPoAm88xCYpTbSZiaK30Svh6Ywra9jfE2KlU9o6Y/art8ip0VJ3m07L 578 4RSfSpnzqgSwdjSq5hNour2Fo/BzYhK7yaz2AzVSbe33R0+RYhb4b/6N+bKbjwGF 579 ftCsqVFMH+PyvYkLbvxyQrHlA9woAZaNThI1ztO5rGSnGUR8xt84eup28WIFKg0K 580 UEGUcTzz+8QGAwAra+0ewPXo/AkO+8BvZjDidP417u6gpBHOJ9qYIcO9FxHeqFyu 581 YrjlrxowEgXn5wO8xuNz6Vu1vhHGDHGDsRbZF8pv1d5O+0F1G7ttZ2GRRgVBZPwi 582 kiyRABEBAAGJATYEGAEIACAWIQSHYyc6Kj9l6HzQsau6vFFc9jxV/wUCXJQPywIb 583 DAAKCRC6vFFc9jxV/9YOCACe8qmOSnKQpQfW+PqYOqo3dt7JyweTs3FkD6NT8Zml 584 dYy/vkstbTjPpX6aTvUZjkb46BVi7AOneVHpD5GBqvRsZ9iVgDYHaehmLCdKiG5L 585 3Tp90NN+QY5WDbsGmsyk6+6ZMYejb4qYfweQeduOj27aavCJdLkCYMoRKfcFYI8c 586 FaNmEfKKy/r1PO20NXEG6t9t05K/frHy6ZG8bCNYdpagfFVot47r9JaQqWlTNtIR 587 5+zkkSq/eG9BEtRij3a6cTdQbktdBzx2KBeI0PYc1vlZR0LpuFKZqY9vlE6vTGLR 588 wMfrTEOvx0NxUM3rpaCgEmuWbB1G1Hu371oyr4srrr+N 589 =28dr 590 -----END PGP PUBLIC KEY BLOCK----- 591 `) 592 }