-
Notifications
You must be signed in to change notification settings - Fork 2.6k
Expand file tree
/
Copy pathjava_opts.go
More file actions
334 lines (292 loc) · 9.66 KB
/
java_opts.go
File metadata and controls
334 lines (292 loc) · 9.66 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
package frameworks
import (
"fmt"
"github.com/cloudfoundry/java-buildpack/src/java/common"
"os"
"strings"
"unicode"
)
// JavaOptsFramework implements custom JAVA_OPTS configuration
type JavaOptsFramework struct {
context *common.Context
}
// JavaOptsConfig represents the java_opts.yml configuration
type JavaOptsConfig struct {
FromEnvironment bool `yaml:"from_environment"`
JavaOpts []string `yaml:"java_opts"`
}
// NewJavaOptsFramework creates a new Java Opts framework instance
func NewJavaOptsFramework(ctx *common.Context) *JavaOptsFramework {
return &JavaOptsFramework{context: ctx}
}
// Detect returns a positive result if loadConfig() finds settings (universal framework for JAVA_OPTS configuration)
func (j *JavaOptsFramework) Detect() (string, error) {
// Check if there's any configuration to apply
config, err := j.loadConfig()
if err != nil {
// if detect "fails" Finalize() is not called so log parse failures as warning
j.context.Log.Warning("Failed to load java_opts config: %s", err.Error())
return "", nil
}
// Detect if there are any custom java_opts or if from_environment is enabled
if len(config.JavaOpts) > 0 || config.FromEnvironment {
return "Java Opts", nil
}
return "", nil
}
// Supply does nothing (no dependencies to install)
func (j *JavaOptsFramework) Supply() error {
// Java Opts framework only configures environment in finalize phase
return nil
}
// Finalize applies the JAVA_OPTS configuration
func (j *JavaOptsFramework) Finalize() error {
j.context.Log.BeginStep("Configuring Java Opts")
// Load configuration
config, err := j.loadConfig()
if err != nil {
j.context.Log.Warning("Failed to load java_opts config: %s", err.Error())
return nil // Don't fail the build
}
var configuredOpts []string
// Add configured java_opts from config file
if len(config.JavaOpts) > 0 {
j.context.Log.Info("Adding configured JAVA_OPTS: %v", config.JavaOpts)
configuredOpts = append(configuredOpts, config.JavaOpts...)
}
// Build the configured JAVA_OPTS value
// Escape each opt using Ruby buildpack's strategy: backslash-escape special characters
// This allows values with spaces to be preserved when passed through shell evaluation
var escapedOpts []string
for _, opt := range configuredOpts {
escapedOpts = append(escapedOpts, rubyStyleEscape(opt))
}
optsString := strings.Join(escapedOpts, " ")
// Write user-defined JAVA_OPTS to .opts file with priority 99 (Ruby buildpack line 82)
// This ensures user opts run LAST, allowing them to override framework defaults
//
// Handle from_environment setting (matching Ruby buildpack order):
// - If true: configured opts FIRST, then append $JAVA_OPTS (allows environment to override config)
// - If false: only use configured opts (ignore environment JAVA_OPTS)
//
// Ruby buildpack order (lines 39-44):
// configured.shellsplit.map {...}.each { |java_opt| @droplet.java_opts << java_opt }
// @droplet.java_opts << '$JAVA_OPTS' if from_environment?
var finalOpts string
if config.FromEnvironment {
// Add configured opts first, then environment JAVA_OPTS (Ruby order)
if optsString != "" {
finalOpts = fmt.Sprintf("%s $JAVA_OPTS", optsString)
} else {
// No configured opts, use only environment JAVA_OPTS
finalOpts = "$JAVA_OPTS"
}
} else {
// Ignore environment JAVA_OPTS, use only configured opts
finalOpts = optsString
}
// Write to .opts file (priority 99 = always last)
if finalOpts != "" {
if err := writeJavaOptsFile(j.context, 99, "user_java_opts", finalOpts); err != nil {
return fmt.Errorf("failed to write java_opts file: %w", err)
}
}
j.context.Log.Info("Configured user JAVA_OPTS for runtime (priority 99)")
return nil
}
// shellSplit splits a string like a shell would, respecting quotes
// Similar to Ruby's Shellwords.shellsplit
func shellSplit(input string) ([]string, error) {
var tokens []string
var current strings.Builder
var inSingleQuote, inDoubleQuote bool
var escaped bool
for _, r := range input {
// Handle escape sequences
if escaped {
current.WriteRune(r)
escaped = false
continue
}
if r == '\\' {
escaped = true
continue
}
// Handle quotes
if r == '\'' && !inDoubleQuote {
inSingleQuote = !inSingleQuote
continue
}
if r == '"' && !inSingleQuote {
inDoubleQuote = !inDoubleQuote
continue
}
// Handle spaces (word separators when not quoted)
if unicode.IsSpace(r) && !inSingleQuote && !inDoubleQuote {
if current.Len() > 0 {
tokens = append(tokens, current.String())
current.Reset()
}
continue
}
// Regular character
current.WriteRune(r)
}
// Add last token if exists
if current.Len() > 0 {
tokens = append(tokens, current.String())
}
// Check for unclosed quotes
if inSingleQuote || inDoubleQuote {
return nil, fmt.Errorf("unclosed quote in string: %s", input)
}
return tokens, nil
}
// rubyStyleEscape escapes a Java option exactly like the Ruby buildpack
//
// Ruby source: lib/java_buildpack/framework/java_opts.rb:40-41
//
// .map { |java_opt| /(?<key>.+?)=(?<value>.+)/ =~ java_opt ? "#{key}=#{escape_value(value)}" : java_opt }
//
// Strategy: Split on first '=' and escape only the VALUE part
//
// Examples:
//
// "-Xmx512M" → "-Xmx512M"
// "-Dkey=value with spaces" → "-Dkey=value\\ with\\ spaces"
// "-XX:OnOutOfMemoryError=kill -9 %p" → "-XX:OnOutOfMemoryError=kill\\ -9\\ \\%p"
func rubyStyleEscape(javaOpt string) string {
idx := strings.IndexByte(javaOpt, '=')
if idx == -1 || idx == len(javaOpt)-1 {
return javaOpt // No '=' or ends with '='
}
key := javaOpt[:idx]
value := javaOpt[idx+1:]
return key + "=" + escapeValue(value)
}
// escapeValue escapes a string for shell safety using Ruby's escape_value method
//
// Ruby source: lib/java_buildpack/framework/java_opts.rb:61-67
//
// str.gsub(%r{([^A-Za-z0-9_\-.,:/@\n$\\])}, '\\\\\\1').gsub(/\n/, "'\n'")
//
// Safe chars (not escaped): A-Za-z0-9_-.,:/@$\
// All other chars are backslash-escaped, including: = ( ) [ ] { } ; & | space % etc.
func escapeValue(value string) string {
if value == "" {
return "''"
}
var result strings.Builder
for _, ch := range value {
if ch == '\n' {
result.WriteString("'\n'") // Special newline handling
continue
}
if !isRubySafeChar(ch) {
result.WriteRune('\\')
}
result.WriteRune(ch)
}
return result.String()
}
// isRubySafeChar checks if a character is in Ruby's safe set: A-Za-z0-9_-.,:/@\n$\
// Note: '=' is NOT safe and will be escaped
func isRubySafeChar(ch rune) bool {
return (ch >= 'A' && ch <= 'Z') ||
(ch >= 'a' && ch <= 'z') ||
(ch >= '0' && ch <= '9') ||
ch == '_' ||
ch == '-' ||
ch == '.' ||
ch == ',' ||
ch == ':' ||
ch == '/' ||
ch == '@' ||
ch == '\n' ||
ch == '$' ||
ch == '\\'
}
// loadConfig loads the java_opts.yml configuration
func (j *JavaOptsFramework) loadConfig() (*JavaOptsConfig, error) {
config := &JavaOptsConfig{
FromEnvironment: true, // Default to true (matches config file)
JavaOpts: []string{},
}
yamlHandler := common.YamlHandler{}
// Check for JBP_CONFIG_JAVA_OPTS override
configOverride := os.Getenv("JBP_CONFIG_JAVA_OPTS")
if configOverride != "" {
// First, parse the outer YAML string (handles single-quoted format like '{...}')
var yamlContent interface{}
if err := yamlHandler.Unmarshal([]byte(configOverride), &yamlContent); err != nil {
return nil, fmt.Errorf("failed to parse JBP_CONFIG_JAVA_OPTS: %w", err)
}
// Handle different YAML formats for backward compatibility
var configData []byte
switch v := yamlContent.(type) {
case string:
// It's a YAML string literal - parse the content
configData = []byte(v)
case map[string]interface{}:
// It's already a parsed YAML structure - marshal it back to bytes
var err error
configData, err = yamlHandler.Marshal(v)
if err != nil {
return nil, fmt.Errorf("failed to marshal config map: %w", err)
}
case []interface{}:
// Handle legacy format: [from_environment: false, java_opts: ...]
// This parses as an array of maps, so we need to merge them
mergedMap := make(map[string]interface{})
for _, item := range v {
if m, ok := item.(map[string]interface{}); ok {
for k, val := range m {
mergedMap[k] = val
}
}
}
var err error
configData, err = yamlHandler.Marshal(mergedMap)
if err != nil {
return nil, fmt.Errorf("failed to marshal merged config map: %w", err)
}
default:
return nil, fmt.Errorf("unexpected YAML type: %T", v)
}
// Parse into a generic map first to handle both string and array formats for java_opts
var rawConfig map[string]interface{}
if err := yamlHandler.Unmarshal(configData, &rawConfig); err != nil {
return nil, fmt.Errorf("failed to parse JBP_CONFIG_JAVA_OPTS structure: %w", err)
}
// Handle from_environment field
if fromEnv, ok := rawConfig["from_environment"].(bool); ok {
config.FromEnvironment = fromEnv
}
// Handle java_opts field - support both string and array formats
if javaOptsRaw, ok := rawConfig["java_opts"]; ok {
switch opts := javaOptsRaw.(type) {
case []interface{}:
// Already an array
for _, opt := range opts {
if optStr, ok := opt.(string); ok {
config.JavaOpts = append(config.JavaOpts, optStr)
}
}
case string:
// Legacy format: space-separated string
// Split on spaces but preserve quoted strings (like Ruby's shellsplit)
if opts != "" {
tokens, err := shellSplit(opts)
if err != nil {
return nil, fmt.Errorf("failed to parse java_opts string: %w", err)
}
config.JavaOpts = tokens
}
}
}
return config, nil
}
// No config file - use built-in defaults
// (The Ruby buildpack's config/java_opts.yml only contained these same defaults)
return config, nil
}