|
14 | 14 | import java.io.ByteArrayInputStream; |
15 | 15 | import java.io.ByteArrayOutputStream; |
16 | 16 | import java.io.UnsupportedEncodingException; |
| 17 | +import java.text.ParseException; |
| 18 | +import java.text.SimpleDateFormat; |
| 19 | +import java.time.Instant; |
| 20 | +import java.time.format.DateTimeParseException; |
| 21 | +import java.util.Date; |
17 | 22 | import java.util.HashMap; |
| 23 | +import java.util.Locale; |
18 | 24 | import java.util.Map; |
| 25 | +import java.util.TimeZone; |
| 26 | + |
| 27 | +import org.slf4j.Logger; |
| 28 | +import org.slf4j.LoggerFactory; |
19 | 29 |
|
20 | 30 | public class VaultKey extends KeyObject { |
21 | 31 |
|
| 32 | + private static final Logger LOG = LoggerFactory.getLogger(VaultKey.class); |
| 33 | + private static final int RFC3339_DATETIME_PREFIX_LENGTH = 19; // Length of "yyyy-MM-dd'T'HH:mm:ss" |
| 34 | + |
22 | 35 | KeyObject parent; |
23 | 36 |
|
24 | 37 | public VaultKey(LogicalResponse response, Path path) { |
@@ -54,6 +67,70 @@ public VaultKey(final Path path, final String item, final Object value) { |
54 | 67 |
|
55 | 68 | } |
56 | 69 |
|
| 70 | + /** |
| 71 | + * Parses a timestamp string with multiple fallback strategies to handle various formats. |
| 72 | + * This method is designed to be resilient to future format changes from Vault. |
| 73 | + * |
| 74 | + * @param timestamp The timestamp string to parse |
| 75 | + * @param fieldName The field name (for logging purposes) |
| 76 | + * @return A Date object, or null if parsing fails with all strategies |
| 77 | + */ |
| 78 | + private Date parseTimestampWithFallback(String timestamp, String fieldName) { |
| 79 | + if (timestamp == null || timestamp.isEmpty()) { |
| 80 | + return null; |
| 81 | + } |
| 82 | + |
| 83 | + // Try parsing as RFC3339/ISO 8601 using Java 8+ Instant (most robust) |
| 84 | + // This handles formats like: "2025-03-13T16:25:00.123456Z", "2025-03-13T16:25:00Z", etc. |
| 85 | + try { |
| 86 | + Instant instant = Instant.parse(timestamp); |
| 87 | + LOG.debug("Successfully parsed {} using Instant.parse()", fieldName); |
| 88 | + return Date.from(instant); |
| 89 | + } catch (DateTimeParseException e) { |
| 90 | + LOG.debug("Failed to parse {} '{}' with Instant.parse(), trying fallback strategies", fieldName, timestamp); |
| 91 | + } |
| 92 | + |
| 93 | + // Try parsing with SimpleDateFormat including fractional seconds |
| 94 | + // Handles: "2025-03-13T16:25:00.123456Z" |
| 95 | + try { |
| 96 | + SimpleDateFormat formatWithFractional = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'", Locale.ENGLISH); |
| 97 | + formatWithFractional.setTimeZone(TimeZone.getTimeZone("UTC")); |
| 98 | + LOG.debug("Successfully parsed {} using SimpleDateFormat with fractional seconds", fieldName); |
| 99 | + return formatWithFractional.parse(timestamp); |
| 100 | + } catch (ParseException e) { |
| 101 | + LOG.debug("Failed to parse {} with fractional seconds pattern", fieldName); |
| 102 | + } |
| 103 | + |
| 104 | + // Try parsing with SimpleDateFormat for milliseconds |
| 105 | + // Handles: "2025-03-13T16:25:00.123Z" |
| 106 | + try { |
| 107 | + SimpleDateFormat formatWithMillis = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ENGLISH); |
| 108 | + formatWithMillis.setTimeZone(TimeZone.getTimeZone("UTC")); |
| 109 | + LOG.debug("Successfully parsed {} using SimpleDateFormat with milliseconds", fieldName); |
| 110 | + return formatWithMillis.parse(timestamp); |
| 111 | + } catch (ParseException e) { |
| 112 | + LOG.debug("Failed to parse {} with milliseconds pattern", fieldName); |
| 113 | + } |
| 114 | + |
| 115 | + // Try substring approach (original implementation) |
| 116 | + // Handles: Any format with at least "yyyy-MM-ddTHH:mm:ss" prefix |
| 117 | + if (timestamp.length() >= RFC3339_DATETIME_PREFIX_LENGTH) { |
| 118 | + try { |
| 119 | + SimpleDateFormat vaultDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.ENGLISH); |
| 120 | + vaultDateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); |
| 121 | + Date parsed = vaultDateFormat.parse(timestamp.substring(0, RFC3339_DATETIME_PREFIX_LENGTH)); |
| 122 | + LOG.debug("Successfully parsed {} using substring strategy", fieldName); |
| 123 | + return parsed; |
| 124 | + } catch (ParseException e) { |
| 125 | + LOG.debug("Failed to parse {} with substring strategy", fieldName); |
| 126 | + } |
| 127 | + } |
| 128 | + |
| 129 | + // All strategies failed |
| 130 | + LOG.warn("Failed to parse {} '{}' with all available strategies", fieldName, timestamp); |
| 131 | + return null; |
| 132 | + } |
| 133 | + |
57 | 134 | public Map<String, Object> saveResource(ResourceMeta content, String event, ByteArrayOutputStream baoStream){ |
58 | 135 |
|
59 | 136 | Path path=this.getPath(); |
@@ -148,6 +225,28 @@ ResourceBase loadResource(){ |
148 | 225 | builder.setMeta(VaultStoragePlugin.RUNDECK_DATA_TYPE, "password"); |
149 | 226 | } |
150 | 227 |
|
| 228 | + // Parse and set timestamps from Vault metadata (KV v2) |
| 229 | + if (this.vaultMetadata != null && !this.vaultMetadata.isEmpty()) { |
| 230 | + String createdTime = this.vaultMetadata.get("created_time"); |
| 231 | + String updatedTime = this.vaultMetadata.get("updated_time"); |
| 232 | + |
| 233 | + // Parse creation time with fallback strategies |
| 234 | + Date creationDate = parseTimestampWithFallback(createdTime, "created_time"); |
| 235 | + |
| 236 | + if (creationDate != null) { |
| 237 | + builder.setCreationTime(creationDate); |
| 238 | + |
| 239 | + // Use updated_time for modification time if available, otherwise use created_time |
| 240 | + Date modificationDate = parseTimestampWithFallback(updatedTime, "updated_time"); |
| 241 | + if (modificationDate != null) { |
| 242 | + builder.setModificationTime(modificationDate); |
| 243 | + } else { |
| 244 | + // Fall back to creation time if updated_time is missing or unparseable |
| 245 | + builder.setModificationTime(creationDate); |
| 246 | + } |
| 247 | + } |
| 248 | + } |
| 249 | + |
151 | 250 | ByteArrayInputStream baiStream = new ByteArrayInputStream(value.getBytes()); |
152 | 251 |
|
153 | 252 | return new ResourceBase<>( |
|
0 commit comments