Repository navigation
Expand file tree
/
Copy pathCmdSendMetadata.go
More file actions
450 lines (390 loc) · 15.1 KB
/
Copy pathCmdSendMetadata.go
File metadata and controls
450 lines (390 loc) · 15.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
// Copyright 2025 IBM Corp
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package main provides the send-metadata command implementation.
//
// This file implements the send-metadata command which sends cluster metadata
// to a remote server for creation or deletion. The command supports two mutually
// exclusive operations:
//
// - Create: Sends metadata to the server to create cluster resources
// - Delete: Sends metadata to the server to delete cluster resources
//
// The command accepts the following flags:
// - createMetadata: Path to metadata file for creation (mutually exclusive with deleteMetadata)
// - deleteMetadata: Path to metadata file for deletion (mutually exclusive with createMetadata)
// - serverIP: IP address of the remote server (required)
// - shouldDebug: Enable debug output (true/false, default: false)
// - timeout: Timeout for send operation (e.g., 5m, 10m, 30s, default: 5m)
//
// Example usage:
// # Create metadata with default timeout
// ./tool send-metadata --createMetadata metadata.json --serverIP 192.168.1.100
//
// # Delete metadata with custom timeout
// ./tool send-metadata --deleteMetadata metadata.json --serverIP 192.168.1.100 --timeout 10m
package main
import (
"context"
"encoding/json"
"flag"
"fmt"
"os"
"strings"
"time"
)
const (
// Flag names for send-metadata command
flagSendCreateMetadata = "createMetadata"
flagSendDeleteMetadata = "deleteMetadata"
flagSendServerIP = "serverIP"
flagSendShouldDebug = "shouldDebug"
flagSendTimeout = "timeout"
// Flag default values
defaultSendCreateMetadata = ""
defaultSendDeleteMetadata = ""
defaultSendServerIP = ""
defaultSendShouldDebug = "false"
defaultSendTimeout = "1m"
// Usage messages
usageSendCreateMetadata = "Create the metadata from this file"
usageSendDeleteMetadata = "Delete the metadata from this file"
usageSendServerIP = "The IP address of the server to send the command to"
usageSendShouldDebug = "Enable debug output (true/false)"
usageSendTimeout = "Timeout for send operation (e.g., 5m, 10m, 30s)"
// Operation names
operationCreate = "create"
operationDelete = "delete"
operationCreated = "created"
operationDeleted = "deleted"
// Error message prefix
errPrefixSend = "Error: "
)
// operationType represents the type of metadata operation to perform
type operationType int
const (
// operationTypeCreate indicates a create operation
operationTypeCreate operationType = iota
// operationTypeDelete indicates a delete operation
operationTypeDelete
)
// String returns the string representation of the operation type
func (o operationType) String() string {
switch o {
case operationTypeCreate:
return operationCreate
case operationTypeDelete:
return operationDelete
default:
return "unknown"
}
}
// pastTense returns the past tense form of the operation
func (o operationType) pastTense() string {
switch o {
case operationTypeCreate:
return operationCreated
case operationTypeDelete:
return operationDeleted
default:
return "unknown"
}
}
// sendMetadataError represents a structured error for send-metadata operations
type sendMetadataError struct {
operation string
phase string
cause error
}
// Error implements the error interface
func (e *sendMetadataError) Error() string {
if e.cause != nil {
return fmt.Sprintf("%s%s failed during %s: %v", errPrefixSend, e.operation, e.phase, e.cause)
}
return fmt.Sprintf("%s%s failed during %s", errPrefixSend, e.operation, e.phase)
}
// Unwrap returns the underlying error
func (e *sendMetadataError) Unwrap() error {
return e.cause
}
// newSendMetadataError creates a new structured error
func newSendMetadataError(operation, phase string, cause error) error {
return &sendMetadataError{
operation: operation,
phase: phase,
cause: cause,
}
}
// validateMetadataContent validates the JSON structure and required fields of a metadata file.
// This function reads and parses the metadata file to ensure it contains valid JSON with
// all required fields before attempting to send it to the server.
//
// Parameters:
// - filePath: Path to the metadata JSON file
//
// Returns:
// - error: Any error encountered during reading, parsing, or validation
//
// The function validates:
// - File can be read
// - Content is valid JSON
// - Required fields are present and non-empty (ClusterName, InfraID)
// - At least one platform metadata is present (OpenStack or PowerVC)
func validateMetadataContent(filePath string) error {
// Read the file
content, err := os.ReadFile(filePath)
if err != nil {
return fmt.Errorf("failed to read metadata file: %w", err)
}
// Parse JSON structure
var metadata CreateMetadata
if err := json.Unmarshal(content, &metadata); err != nil {
return fmt.Errorf("invalid JSON format: %w", err)
}
// Validate required fields
if strings.TrimSpace(metadata.ClusterName) == "" {
return fmt.Errorf("required field 'clusterName' is missing or empty")
}
if strings.TrimSpace(metadata.InfraID) == "" {
return fmt.Errorf("required field 'infraID' is missing or empty")
}
// Validate that at least one platform metadata exists
hasOpenStack := metadata.OpenStack != nil && strings.TrimSpace(metadata.OpenStack.Cloud) != ""
hasPowerVC := metadata.PowerVC != nil && strings.TrimSpace(metadata.PowerVC.Cloud) != ""
if !hasOpenStack && !hasPowerVC {
return fmt.Errorf("metadata must contain either 'openstack' or 'powervc' platform configuration")
}
return nil
}
// sendMetadataWithRetry attempts to send metadata with exponential backoff retry logic.
// It will retry transient failures up to maxRetries times with increasing delays between attempts.
//
// Parameters:
// - ctx: Context for cancellation and timeout
// - metadataFile: Path to the metadata file
// - serverIP: IP address or hostname of the server
// - shouldCreate: true for create operation, false for delete
//
// Returns:
// - error: Any error encountered, or nil on success
func sendMetadataWithRetry(ctx context.Context, metadataFile, serverIP string, shouldCreate bool) error {
var lastErr error
backoff := initialRetryDelay
for attempt := 0; attempt <= maxRetries; attempt++ {
// Wait before retry (skip on first attempt)
if attempt > 0 {
log.Printf("[INFO] Retry attempt %d/%d after %v delay", attempt, maxRetries, backoff)
// Wait with context cancellation support
select {
case <-time.After(backoff):
// Continue with retry
case <-ctx.Done():
return fmt.Errorf("operation cancelled during retry backoff: %w", ctx.Err())
}
// Increase backoff for next retry (exponential backoff)
backoff = time.Duration(float64(backoff) * retryMultiplier)
if backoff > maxRetryDelay {
backoff = maxRetryDelay
}
}
// Attempt to send metadata
log.Debugf("Attempting to send metadata (attempt %d/%d)", attempt+1, maxRetries+1)
err := sendMetadata(ctx, metadataFile, serverIP, shouldCreate)
// Success - return immediately
if err == nil {
if attempt > 0 {
log.Printf("[INFO] Metadata sent successfully after %d retries", attempt)
}
return nil
}
// Check if error is retryable
if !isRetryableError(err) {
log.Printf("[INFO] Non-retryable error encountered: %v", err)
return err
}
// Store error for potential final return
lastErr = err
log.Printf("[WARN] Retryable error encountered: %v", err)
// Check if we've exhausted retries
if attempt == maxRetries {
break
}
}
// All retries exhausted
return fmt.Errorf("failed after %d retries: %w", maxRetries, lastErr)
}
// sendMetadataCommand is the top-level handler for the send-metadata command.
// It delegates to innerSendMetadataCommand and, on failure, prints the error
// and displays flag usage before returning the error.
func sendMetadataCommand(sendMetadataFlags *flag.FlagSet, args []string) error {
err := innerSendMetadataCommand(sendMetadataFlags, args)
if err != nil {
fmt.Printf("%+v\n", err)
if sendMetadataFlags != nil {
sendMetadataFlags.Usage()
}
}
return err
}
// innerSendMetadataCommand executes the send-metadata command with the given flags and arguments.
//
// This function handles both metadata creation and deletion operations. It validates
// that exactly one operation is specified, validates the metadata file and server IP,
// and sends the metadata to the remote server with timeout support.
//
// Parameters:
// - sendMetadataFlags: The FlagSet containing command-line flags (must not be nil)
// - args: Command-line arguments to parse
//
// Returns:
// - error: Any error encountered during flag parsing, validation, or operation execution
//
// The function executes the following steps:
// 1. Validates input parameters
// 2. Displays program version information
// 3. Defines and parses command-line flags
// 4. Validates mutual exclusivity of create/delete operations
// 5. Validates metadata file existence and readability
// 6. Validates server IP address format
// 7. Initializes logger based on debug flag
// 8. Creates context with timeout for operation
// 9. Sends metadata to remote server
// 10. Provides user feedback on success
func innerSendMetadataCommand(sendMetadataFlags *flag.FlagSet, args []string) error {
// Validate input parameters
if sendMetadataFlags == nil {
return newSendMetadataError("send-metadata", "initialization", fmt.Errorf("flag set cannot be nil"))
}
// Display version information
fmt.Fprintf(os.Stderr, "Program version is %v, release = %v\n", version, release)
// Define command-line flags
ptrCreateMetadata := sendMetadataFlags.String(flagSendCreateMetadata, defaultSendCreateMetadata, usageSendCreateMetadata)
ptrDeleteMetadata := sendMetadataFlags.String(flagSendDeleteMetadata, defaultSendDeleteMetadata, usageSendDeleteMetadata)
ptrServerIP := sendMetadataFlags.String(flagSendServerIP, defaultSendServerIP, usageSendServerIP)
ptrShouldDebug := sendMetadataFlags.String(flagSendShouldDebug, defaultSendShouldDebug, usageSendShouldDebug)
ptrTimeout := sendMetadataFlags.String(flagSendTimeout, defaultSendTimeout, usageSendTimeout)
// Parse flags
if err := sendMetadataFlags.Parse(args); err != nil {
return newSendMetadataError("send-metadata", "flag parsing", err)
}
// Parse and validate operation flags
createFile := strings.TrimSpace(*ptrCreateMetadata)
deleteFile := strings.TrimSpace(*ptrDeleteMetadata)
shouldCreateMetadata := createFile != ""
shouldDeleteMetadata := deleteFile != ""
// Ensure exactly one operation is specified
if shouldCreateMetadata && shouldDeleteMetadata {
return newSendMetadataError("send-metadata", "flag validation",
fmt.Errorf("cannot specify both --%s and --%s", flagSendCreateMetadata, flagSendDeleteMetadata))
}
if !shouldCreateMetadata && !shouldDeleteMetadata {
return newSendMetadataError("send-metadata", "flag validation",
fmt.Errorf("required flag --%s or --%s must be specified", flagSendCreateMetadata, flagSendDeleteMetadata))
}
// Parse debug flag
shouldDebug, err := parseBoolFlag(*ptrShouldDebug, flagSendShouldDebug)
if err != nil {
return newSendMetadataError("send-metadata", "debug flag parsing", err)
}
// Parse timeout flag
timeout, err := time.ParseDuration(strings.TrimSpace(*ptrTimeout))
if err != nil {
return newSendMetadataError("send-metadata", "timeout parsing",
fmt.Errorf("invalid timeout value %q: %w", *ptrTimeout, err))
}
if timeout <= 0 {
return newSendMetadataError("send-metadata", "timeout validation",
fmt.Errorf("timeout must be positive, got %v", timeout))
}
// Initialize logger
log = initLogger(shouldDebug)
if shouldDebug {
log.Debugf("Debug mode enabled")
}
// Determine which file to use and operation type
var metadataFile string
var opType operationType
if shouldCreateMetadata {
metadataFile = createFile
opType = operationTypeCreate
} else {
metadataFile = deleteFile
opType = operationTypeDelete
}
log.Printf("[INFO] Starting send-metadata command")
log.Printf("[INFO] Operation: %s", opType.String())
log.Printf("[INFO] Metadata file: %s", metadataFile)
log.Printf("[INFO] Timeout: %v", timeout)
// Create context with timeout for the operation
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
// Validate metadata file exists and is readable
log.Printf("[INFO] Validating metadata file...")
if err := validateFileExists(metadataFile); err != nil {
return newSendMetadataError(opType.String(), "file validation", err)
}
log.Printf("[INFO] Metadata file validated successfully")
// Check if context was cancelled after file validation
if err := ctx.Err(); err != nil {
return newSendMetadataError(opType.String(), "operation cancelled", err)
}
// Validate metadata content (JSON structure and required fields)
log.Printf("[INFO] Validating metadata content...")
if err := validateMetadataContent(metadataFile); err != nil {
return newSendMetadataError(opType.String(), "metadata content validation", err)
}
log.Printf("[INFO] Metadata content validated successfully")
// Check if context was cancelled after content validation
if err := ctx.Err(); err != nil {
return newSendMetadataError(opType.String(), "operation cancelled", err)
}
// Validate required server IP flag
serverIP := strings.TrimSpace(*ptrServerIP)
if serverIP == "" {
return newSendMetadataError(opType.String(), "server IP validation",
fmt.Errorf("required flag --%s not specified", flagSendServerIP))
}
log.Printf("[INFO] Server IP: %s", serverIP)
// Validate server IP format
log.Printf("[INFO] Validating server IP format...")
if err := validateServerIP(serverIP); err != nil {
return newSendMetadataError(opType.String(), "server IP validation", err)
}
log.Printf("[INFO] Server IP validated successfully")
// Check if context was cancelled after IP validation
if err := ctx.Err(); err != nil {
return newSendMetadataError(opType.String(), "operation cancelled", err)
}
log.Debugf("sendMetadataCommand: operation=%s, file=%s, server=%s",
opType.String(),
metadataFile,
serverIP)
deadline, ok := ctx.Deadline()
if ok {
log.Printf("[INFO] Sending metadata to server (timeout: %v, deadline: %v)...",
timeout, deadline.Format(time.RFC3339))
} else {
log.Printf("[INFO] Sending metadata to server (timeout: %v)...", timeout)
}
startTime := time.Now()
// Send metadata command to server with context and retry logic
if err := sendMetadataWithRetry(ctx, metadataFile, serverIP, shouldCreateMetadata); err != nil {
return newSendMetadataError(opType.String(), "metadata transmission", err)
}
duration := time.Since(startTime)
log.Printf("[INFO] Metadata sent successfully (took %v)", duration)
// Provide user feedback
fmt.Printf("Metadata %s successfully (source file: %s)\n", opType.pastTense(), metadataFile)
log.Printf("[INFO] Send-metadata command completed successfully")
return nil
}