Skip to content

Commit 55dc0ee

Browse files
authored
Merge pull request #65 from rundeck-plugins/RUN-3900-hashi-corp-vault-integration-modification-time-issue
RUN-3900: Hashi corp vault integration modification time issue
2 parents 0d7e393 + c03e42d commit 55dc0ee

File tree

4 files changed

+629
-1
lines changed

4 files changed

+629
-1
lines changed

src/main/java/io/github/valfadeev/rundeck/plugin/vault/KeyObject.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ public abstract class KeyObject {
1515
protected Map<String, String> payload;
1616
protected Map<String, Object> keys;
1717
protected Path path;
18+
protected Map<String, String> vaultMetadata;
1819

1920
protected boolean error;
2021
protected String errorMessage;
@@ -84,6 +85,14 @@ public void setError(final boolean error) {
8485
this.error = error;
8586
}
8687

88+
public Map<String, String> getVaultMetadata() {
89+
return vaultMetadata;
90+
}
91+
92+
public void setVaultMetadata(final Map<String, String> vaultMetadata) {
93+
this.vaultMetadata = vaultMetadata;
94+
}
95+
8796
@Override
8897
public String toString() {
8998
return "KeyObject{" +
@@ -92,6 +101,7 @@ public String toString() {
92101
", payload=" + payload +
93102
", keys=" + keys +
94103
", path=" + path +
104+
", vaultMetadata=" + vaultMetadata +
95105
", error=" + error +
96106
", errorMessage='" + errorMessage + '\'' +
97107
'}';

src/main/java/io/github/valfadeev/rundeck/plugin/vault/KeyObjectBuilder.java

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
import io.github.jopenlibs.vault.VaultException;
44
import io.github.jopenlibs.vault.api.Logical;
55
import io.github.jopenlibs.vault.response.LogicalResponse;
6+
import io.github.jopenlibs.vault.response.DataMetadata;
67
import org.rundeck.storage.api.Path;
78
import org.rundeck.storage.api.PathUtil;
9+
import java.util.Map;
810

911
public class KeyObjectBuilder {
1012

@@ -45,10 +47,20 @@ KeyObject build(){
4547
response = vault.read(VaultStoragePlugin.getVaultPath(path.getPath(),vaultSecretBackend,vaultPrefix));
4648
String data = response.getData().get(VaultStoragePlugin.VAULT_STORAGE_KEY);
4749

50+
// Extract metadata from response for KV v2
51+
Map<String, String> metadata = null;
52+
DataMetadata dataMetadata = response.getDataMetadata();
53+
if (dataMetadata != null && !dataMetadata.isEmpty()) {
54+
metadata = dataMetadata.getMetadataMap();
55+
}
56+
4857
if(data !=null) {
58+
// RundeckKey stores timestamps in its own payload format, doesn't use Vault metadata
4959
object = new RundeckKey(response,path);
5060
}else{
61+
// VaultKey uses Vault metadata for timestamps
5162
object = new VaultKey(response,path);
63+
object.setVaultMetadata(metadata);
5264
}
5365

5466
if(response.getRestResponse().getStatus()!=200){
@@ -97,8 +109,14 @@ public KeyObject getVaultParentObject(Path path){
97109
}
98110

99111
parentObject=new VaultKey(response, parentPath);
100-
} catch (VaultException e) {
101112

113+
// Extract metadata for parent object too
114+
DataMetadata dataMetadata = response.getDataMetadata();
115+
if (dataMetadata != null && !dataMetadata.isEmpty()) {
116+
parentObject.setVaultMetadata(dataMetadata.getMetadataMap());
117+
}
118+
} catch (VaultException e) {
119+
// Parent object doesn't exist, return null - this is expected in some cases
102120
}
103121

104122
return parentObject;

src/main/java/io/github/valfadeev/rundeck/plugin/vault/VaultKey.java

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,24 @@
1414
import java.io.ByteArrayInputStream;
1515
import java.io.ByteArrayOutputStream;
1616
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;
1722
import java.util.HashMap;
23+
import java.util.Locale;
1824
import java.util.Map;
25+
import java.util.TimeZone;
26+
27+
import org.slf4j.Logger;
28+
import org.slf4j.LoggerFactory;
1929

2030
public class VaultKey extends KeyObject {
2131

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+
2235
KeyObject parent;
2336

2437
public VaultKey(LogicalResponse response, Path path) {
@@ -54,6 +67,70 @@ public VaultKey(final Path path, final String item, final Object value) {
5467

5568
}
5669

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+
57134
public Map<String, Object> saveResource(ResourceMeta content, String event, ByteArrayOutputStream baoStream){
58135

59136
Path path=this.getPath();
@@ -148,6 +225,28 @@ ResourceBase loadResource(){
148225
builder.setMeta(VaultStoragePlugin.RUNDECK_DATA_TYPE, "password");
149226
}
150227

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+
151250
ByteArrayInputStream baiStream = new ByteArrayInputStream(value.getBytes());
152251

153252
return new ResourceBase<>(

0 commit comments

Comments
 (0)