Skip to content

Commit f2a2f20

Browse files
Check for nulls for timestamps and logfiles , validate config and event formats (#61)
* Should fix potential Null pointer dereference when parsing timestamps * Fixes situations where logfile might be null * Move logfile null check to more correct location * Move validation to the config object * Adds full config validation for configs * Try/catch Instant.parse instead of nullcheck * Adds validator for appname and hostname while handling messages * appname -> appName refactor * Adds missing exception constructors * Change all ints to integeres in AppConfig objects, check for null where necessary
1 parent e3a47f4 commit f2a2f20

12 files changed

+337
-48
lines changed

example/combined.yaml

+3-2
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ data:
4242
"kubernetes": {
4343
"logdir": "/var/log/containers",
4444
"url": "https://127.0.0.1:8443",
45+
"timezone": "Europe/Helsinki",
4546
"cacheExpireInterval": 300,
4647
"cacheMaxEntries": 4096,
4748
"labels": {
@@ -104,7 +105,7 @@ data:
104105
</Configuration>
105106
kind: ConfigMap
106107
metadata:
107-
name: app-config-2t5785hm6f
108+
name: app-config-58dh7f727h
108109
---
109110
apiVersion: v1
110111
data:
@@ -271,7 +272,7 @@ spec:
271272
terminationGracePeriodSeconds: 0
272273
volumes:
273274
- configMap:
274-
name: app-config-2t5785hm6f
275+
name: app-config-58dh7f727h
275276
name: app-config
276277
- hostPath:
277278
path: /var/log/containers

example/config/k8s_01/config.json

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"kubernetes": {
66
"logdir": "/var/log/containers",
77
"url": "https://127.0.0.1:8443",
8+
"timezone": "Europe/Helsinki",
89
"cacheExpireInterval": 300,
910
"cacheMaxEntries": 4096,
1011
"labels": {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
Kubernetes log forwarder k8s_01
3+
Copyright (C) 2023 Suomen Kanuuna Oy
4+
5+
Licensed under the Apache License, Version 2.0 (the "License");
6+
you may not use this file except in compliance with the License.
7+
You may obtain a copy of the License at
8+
9+
http://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software
12+
distributed under the License is distributed on an "AS IS" BASIS,
13+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
See the License for the specific language governing permissions and
15+
limitations under the License.
16+
*/
17+
18+
package com.teragrep.k8s_01;
19+
20+
public class InvalidConfigurationException extends Exception {
21+
public InvalidConfigurationException() {
22+
super();
23+
}
24+
25+
public InvalidConfigurationException(String message) {
26+
super(message);
27+
}
28+
29+
public InvalidConfigurationException(String message, Throwable throwable) {
30+
super(message, throwable);
31+
}
32+
33+
public InvalidConfigurationException(Throwable throwable) {
34+
super(throwable);
35+
}
36+
}

src/main/java/com/teragrep/k8s_01/K8SConsumer.java

+89-11
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,11 @@
3636
import java.time.ZoneId;
3737
import java.time.ZonedDateTime;
3838
import java.time.format.DateTimeFormatter;
39+
import java.time.format.DateTimeParseException;
3940
import java.util.UUID;
4041
import java.util.concurrent.BlockingQueue;
4142
import java.util.function.Consumer;
43+
import java.util.regex.Pattern;
4244

4345
/**
4446
* Must be thread-safe
@@ -53,6 +55,10 @@ public class K8SConsumer implements Consumer<FileRecord> {
5355
private static final DateTimeFormatter format = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSSSSxxx");
5456
private final ZoneId timezoneId;
5557

58+
// Validators
59+
private static final Pattern hostnamePattern = Pattern.compile("^[a-zA-Z0-9.-]+$"); // Not perfect but filters basically all mistakes
60+
private static final Pattern appNamePattern = Pattern.compile("^[\\x21-\\x7e]+$"); // DEC 33 - DEC 126 as specified in RFC5424
61+
5662
K8SConsumer(
5763
AppConfig appConfig,
5864
KubernetesCachingAPIClient cacheClient,
@@ -131,7 +137,26 @@ public void accept(FileRecord record) {
131137
)
132138
);
133139
}
134-
Instant instant = Instant.parse(log.getTimestamp());
140+
Instant instant;
141+
try {
142+
instant = Instant.parse(log.getTimestamp());
143+
}
144+
catch(DateTimeParseException e) {
145+
throw new RuntimeException(
146+
String.format(
147+
"[%s] Can't parse timestamp <%s> properly for event from pod <%s/%s> on container <%s> in file %s/%s at offset %s: ",
148+
uuid,
149+
log.getTimestamp(),
150+
namespace,
151+
podname,
152+
containerId,
153+
record.getPath(),
154+
record.getFilename(),
155+
record.getStartOffset()
156+
),
157+
e
158+
);
159+
}
135160
ZonedDateTime zdt = instant.atZone(timezoneId);
136161
String timestamp = zdt.format(format);
137162

@@ -152,34 +177,87 @@ public void accept(FileRecord record) {
152177
JsonObject dockerMetadata = new JsonObject();
153178
dockerMetadata.addProperty("container_id", containerId);
154179

155-
// Handle hostname and appname, use fallback values when labels are empty or if label not found
180+
// Handle hostname and appName, use fallback values when labels are empty or if label not found
156181
String hostname;
157-
String appname;
182+
String appName;
158183
if(podMetadataContainer.getLabels() == null) {
159184
LOGGER.warn(
160-
"[{}] Can't resolve metadata and/or labels for container <{}>, using fallback values for hostname and appname",
185+
"[{}] Can't resolve metadata and/or labels for container <{}>, using fallback values for hostname and appName",
161186
uuid,
162187
containerId
163188
);
164189
hostname = appConfig.getKubernetes().getLabels().getHostname().getFallback();
165-
appname = appConfig.getKubernetes().getLabels().getAppname().getFallback();
190+
appName = appConfig.getKubernetes().getLabels().getAppName().getFallback();
166191
}
167192
else {
168193
hostname = podMetadataContainer.getLabels().getOrDefault(
169194
appConfig.getKubernetes().getLabels().getHostname().getLabel(log.getStream()),
170195
appConfig.getKubernetes().getLabels().getHostname().getFallback()
171196
);
172-
appname = podMetadataContainer.getLabels().getOrDefault(
173-
appConfig.getKubernetes().getLabels().getAppname().getLabel(log.getStream()),
174-
appConfig.getKubernetes().getLabels().getAppname().getFallback()
197+
appName = podMetadataContainer.getLabels().getOrDefault(
198+
appConfig.getKubernetes().getLabels().getAppName().getLabel(log.getStream()),
199+
appConfig.getKubernetes().getLabels().getAppName().getFallback()
200+
);
201+
}
202+
hostname = appConfig.getKubernetes().getLabels().getHostname().getPrefix() + hostname;
203+
appName = appConfig.getKubernetes().getLabels().getAppName().getPrefix() + appName;
204+
205+
if(!hostnamePattern.matcher(hostname).matches()) {
206+
throw new RuntimeException(
207+
String.format(
208+
"[%s] Detected hostname <[%s]> from pod <[%s]/[%s]> on container <%s> contains invalid characters, can't continue",
209+
uuid,
210+
hostname,
211+
namespace,
212+
podname,
213+
containerId
214+
)
215+
);
216+
}
217+
218+
if(hostname.length() >= 255) {
219+
throw new RuntimeException(
220+
String.format(
221+
"[%s] Detected hostname <[%s]...> from pod <[%s]/[%s]> on container <%s> is too long, can't continue",
222+
uuid,
223+
hostname.substring(0,30),
224+
namespace,
225+
podname,
226+
containerId
227+
)
228+
);
229+
}
230+
231+
if(!appNamePattern.matcher(appName).matches()) {
232+
throw new RuntimeException(
233+
String.format(
234+
"[%s] Detected appName <[%s]> from pod <[%s]/[%s]> on container <%s> contains invalid characters, can't continue",
235+
uuid,
236+
appName,
237+
namespace,
238+
podname,
239+
containerId
240+
)
241+
);
242+
}
243+
if(appName.length() > 48) {
244+
throw new RuntimeException(
245+
String.format(
246+
"[%s] Detected appName <[%s]...> from pod <[%s]/[%s]> on container <%s> is too long, can't continue",
247+
uuid,
248+
appName.substring(0,30),
249+
namespace,
250+
podname,
251+
containerId
252+
)
175253
);
176254
}
177255

178256
if(LOGGER.isDebugEnabled()) {
179257
LOGGER.debug(
180258
"[{}] Resolved message to be {}@{} from {}/{} generated at {}",
181259
uuid,
182-
appname,
260+
appName,
183261
hostname,
184262
namespace,
185263
podname,
@@ -205,8 +283,8 @@ public void accept(FileRecord record) {
205283
SyslogMessage syslog = new SyslogMessage()
206284
.withTimestamp(timestamp, true)
207285
.withSeverity(Severity.WARNING)
208-
.withHostname(appConfig.getKubernetes().getLabels().getHostname().getPrefix() + hostname)
209-
.withAppName(appConfig.getKubernetes().getLabels().getAppname().getPrefix() + appname)
286+
.withHostname(hostname)
287+
.withAppName(appName)
210288
.withFacility(Facility.USER)
211289
.withSDElement(SDMetadata)
212290
.withMsg(new String(record.getRecord()));

src/main/java/com/teragrep/k8s_01/KubernetesLogReader.java

+11-1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ public static void main(String[] args) throws IOException {
4242
AppConfig appConfig;
4343
try {
4444
appConfig = gson.fromJson(new FileReader("etc/config.json"), AppConfig.class);
45+
appConfig.validate();
4546
}
4647
catch (FileNotFoundException e) {
4748
LOGGER.error(
@@ -57,6 +58,13 @@ public static void main(String[] args) throws IOException {
5758
);
5859
return;
5960
}
61+
catch (InvalidConfigurationException e) {
62+
LOGGER.error(
63+
"Failed to validate config 'etc/config.json':",
64+
e
65+
);
66+
return;
67+
}
6068
catch (Exception e) {
6169
LOGGER.error(
6270
"Unknown exception while handling config:",
@@ -108,23 +116,25 @@ public static void main(String[] args) throws IOException {
108116

109117
// consumer supplier, returns always the same instance
110118
K8SConsumerSupplier consumerSupplier = new K8SConsumerSupplier(appConfig, cacheClient, relpOutputPool);
111-
112119
String[] logfiles = appConfig.getKubernetes().getLogfiles();
113120
LOGGER.debug(
114121
"Monitored logfiles: {}",
115122
Arrays.toString(logfiles)
116123
);
124+
117125
List<Thread> threads = new ArrayList<>();
118126
String statesStore = System.getProperty("user.dir") + "/var";
119127
LOGGER.debug(
120128
"Using {} as statestore",
121129
statesStore
122130
);
131+
123132
// FIXME: VERIFY: SFR is not in try-with-resources block as it will have weird behaviour with threads.
124133
StatefulFileReader statefulFileReader = new StatefulFileReader(
125134
Paths.get(statesStore),
126135
consumerSupplier
127136
);
137+
128138
// Start a new thread for all logfile watchers
129139
for (String logfile : logfiles) {
130140
Thread thread = new Thread(() -> {

src/main/java/com/teragrep/k8s_01/config/AppConfig.java

+23-8
Original file line numberDiff line numberDiff line change
@@ -18,22 +18,19 @@
1818
package com.teragrep.k8s_01.config;
1919

2020
import com.google.gson.Gson;
21+
import com.teragrep.k8s_01.InvalidConfigurationException;
2122

2223
/* POJO representing the main config.json */
23-
public class AppConfig {
24-
private AppConfigKubernetes kubernetes;
25-
24+
public class AppConfig implements BaseConfig {
25+
private AppConfigMetrics metrics;
2626
public AppConfigMetrics getMetrics() {
2727
return metrics;
2828
}
29-
30-
private AppConfigMetrics metrics;
31-
private AppConfigRelp relp;
32-
29+
private AppConfigKubernetes kubernetes;
3330
public AppConfigKubernetes getKubernetes() {
3431
return kubernetes;
3532
}
36-
33+
private AppConfigRelp relp;
3734
public AppConfigRelp getRelp() {
3835
return relp;
3936
}
@@ -42,4 +39,22 @@ public AppConfigRelp getRelp() {
4239
public String toString() {
4340
return new Gson().toJson(this);
4441
}
42+
43+
@Override
44+
public void validate() throws InvalidConfigurationException {
45+
if(metrics == null) {
46+
throw new InvalidConfigurationException("metrics object not found or is null in main config object");
47+
}
48+
getMetrics().validate();
49+
50+
if(kubernetes == null) {
51+
throw new InvalidConfigurationException("kubernetes object not found or is null in main config object");
52+
}
53+
getKubernetes().validate();
54+
55+
if(relp == null) {
56+
throw new InvalidConfigurationException("relp object no found or is null in main config object");
57+
}
58+
getRelp().validate();
59+
}
4560
}

0 commit comments

Comments
 (0)