diff --git a/client/driver/java.go b/client/driver/java.go index 4519a719aac9..366b891e8398 100644 --- a/client/driver/java.go +++ b/client/driver/java.go @@ -18,6 +18,7 @@ import ( "github.com/mitchellh/mapstructure" "github.com/hashicorp/nomad/client/config" + "github.com/hashicorp/nomad/client/driver/env" "github.com/hashicorp/nomad/client/driver/executor" dstructs "github.com/hashicorp/nomad/client/driver/structs" "github.com/hashicorp/nomad/client/fingerprint" @@ -40,9 +41,11 @@ type JavaDriver struct { } type JavaDriverConfig struct { - JarPath string `mapstructure:"jar_path"` - JvmOpts []string `mapstructure:"jvm_options"` - Args []string `mapstructure:"args"` + Class string `mapstructure:"class"` + ClassPath string `mapstructure:"class_path"` + JarPath string `mapstructure:"jar_path"` + JvmOpts []string `mapstructure:"jvm_options"` + Args []string `mapstructure:"args"` } // javaHandle is returned from Start/Open as a handle to the PID @@ -70,9 +73,14 @@ func (d *JavaDriver) Validate(config map[string]interface{}) error { fd := &fields.FieldData{ Raw: config, Schema: map[string]*fields.FieldSchema{ + "class": &fields.FieldSchema{ + Type: fields.TypeString, + }, + "class_path": &fields.FieldSchema{ + Type: fields.TypeString, + }, "jar_path": &fields.FieldSchema{ - Type: fields.TypeString, - Required: true, + Type: fields.TypeString, }, "jvm_options": &fields.FieldSchema{ Type: fields.TypeArray, @@ -96,10 +104,6 @@ func (d *JavaDriver) Abilities() DriverAbilities { } } -func (d *JavaDriver) FSIsolation() cstructs.FSIsolation { - return cstructs.FSIsolationChroot -} - func (d *JavaDriver) Fingerprint(cfg *config.Config, node *structs.Node) (bool, error) { // Get the current status so that we can log any debug messages only if the // state changes @@ -167,25 +171,59 @@ func (d *JavaDriver) Prestart(ctx *ExecContext, task *structs.Task) error { return nil } -func (d *JavaDriver) Start(ctx *ExecContext, task *structs.Task) (DriverHandle, error) { +func NewJavaDriverConfig(task *structs.Task, env *env.TaskEnvironment) (*JavaDriverConfig, error) { var driverConfig JavaDriverConfig if err := mapstructure.WeakDecode(task.Config, &driverConfig); err != nil { return nil, err } - if driverConfig.JarPath == "" { - return nil, fmt.Errorf("jar_path must be specified") + // Interpolate everything + driverConfig.Class = env.ReplaceEnv(driverConfig.Class) + driverConfig.ClassPath = env.ReplaceEnv(driverConfig.ClassPath) + driverConfig.JarPath = env.ReplaceEnv(driverConfig.JarPath) + driverConfig.JvmOpts = env.ParseAndReplace(driverConfig.JvmOpts) + driverConfig.Args = env.ParseAndReplace(driverConfig.Args) + + // Validate + jarSpecified := driverConfig.JarPath != "" + classSpecified := driverConfig.Class != "" + if !jarSpecified && !classSpecified { + return nil, fmt.Errorf("jar_path or class must be specified") + } + + return &driverConfig, nil +} + +func (d *JavaDriver) Start(ctx *ExecContext, task *structs.Task) (DriverHandle, error) { + driverConfig, err := NewJavaDriverConfig(task, d.taskEnv) + if err != nil { + return nil, err } args := []string{} + // Look for jvm options if len(driverConfig.JvmOpts) != 0 { d.logger.Printf("[DEBUG] driver.java: found JVM options: %s", driverConfig.JvmOpts) args = append(args, driverConfig.JvmOpts...) } - // Build the argument list. - args = append(args, "-jar", driverConfig.JarPath) + // Add the classpath + if driverConfig.ClassPath != "" { + args = append(args, "-cp", driverConfig.ClassPath) + } + + // Add the jar + if driverConfig.JarPath != "" { + args = append(args, "-jar", driverConfig.JarPath) + } + + // Add the class + if driverConfig.Class != "" { + args = append(args, driverConfig.Class) + } + + // Add any args if len(driverConfig.Args) != 0 { args = append(args, driverConfig.Args...) } diff --git a/client/driver/java_linux.go b/client/driver/java_linux.go new file mode 100644 index 000000000000..99a8419689bb --- /dev/null +++ b/client/driver/java_linux.go @@ -0,0 +1,7 @@ +package driver + +import cstructs "github.com/hashicorp/nomad/client/structs" + +func (d *JavaDriver) FSIsolation() cstructs.FSIsolation { + return cstructs.FSIsolationChroot +} diff --git a/client/driver/java_test.go b/client/driver/java_test.go index f0821c710b93..8fa5ec26b179 100644 --- a/client/driver/java_test.go +++ b/client/driver/java_test.go @@ -349,3 +349,70 @@ func TestJavaDriverUser(t *testing.T) { t.Fatalf("Expecting '%v' in '%v'", msg, err) } } + +func TestJavaDriver_Start_Wait_Class(t *testing.T) { + if !javaLocated() { + t.Skip("Java not found; skipping") + } + + ctestutils.JavaCompatible(t) + task := &structs.Task{ + Name: "demo-app", + Driver: "java", + Config: map[string]interface{}{ + "class_path": "${NOMAD_TASK_DIR}", + "class": "Hello", + }, + LogConfig: &structs.LogConfig{ + MaxFiles: 10, + MaxFileSizeMB: 10, + }, + Resources: basicResources, + } + + ctx := testDriverContexts(t, task) + //defer ctx.AllocDir.Destroy() + d := NewJavaDriver(ctx.DriverCtx) + + // Copy the test jar into the task's directory + dst := ctx.ExecCtx.TaskDir.LocalDir + copyFile("./test-resources/java/Hello.class", filepath.Join(dst, "Hello.class"), t) + + if err := d.Prestart(ctx.ExecCtx, task); err != nil { + t.Fatalf("prestart err: %v", err) + } + handle, err := d.Start(ctx.ExecCtx, task) + if err != nil { + t.Fatalf("err: %v", err) + } + if handle == nil { + t.Fatalf("missing handle") + } + + // Task should terminate quickly + select { + case res := <-handle.WaitCh(): + if !res.Successful() { + t.Fatalf("err: %v", res) + } + case <-time.After(time.Duration(testutil.TestMultiplier()*5) * time.Second): + // expect the timeout b/c it's a long lived process + break + } + + // Get the stdout of the process and assrt that it's not empty + stdout := filepath.Join(ctx.ExecCtx.TaskDir.LogDir, "demo-app.stdout.0") + fInfo, err := os.Stat(stdout) + if err != nil { + t.Fatalf("failed to get stdout of process: %v", err) + } + if fInfo.Size() == 0 { + t.Fatalf("stdout of process is empty") + } + + // need to kill long lived process + err = handle.Kill() + if err != nil { + t.Fatalf("Error: %s", err) + } +} diff --git a/client/driver/java_universal.go b/client/driver/java_universal.go new file mode 100644 index 000000000000..a75869b9d1b6 --- /dev/null +++ b/client/driver/java_universal.go @@ -0,0 +1,9 @@ +// +build !linux + +package driver + +import cstructs "github.com/hashicorp/nomad/client/structs" + +func (d *JavaDriver) FSIsolation() cstructs.FSIsolation { + return cstructs.FSIsolationNone +} diff --git a/client/driver/test-resources/java/Hello.class b/client/driver/test-resources/java/Hello.class new file mode 100644 index 000000000000..08a9b2e100cd Binary files /dev/null and b/client/driver/test-resources/java/Hello.class differ diff --git a/client/driver/test-resources/java/demoapp.java b/client/driver/test-resources/java/Hello.java similarity index 88% rename from client/driver/test-resources/java/demoapp.java rename to client/driver/test-resources/java/Hello.java index 3f168f959ec6..0279547fb27c 100644 --- a/client/driver/test-resources/java/demoapp.java +++ b/client/driver/test-resources/java/Hello.java @@ -1,7 +1,7 @@ public class Hello { public static void main(String[] args) { while (true) { - System.out.println("Hi"); + System.out.println("Hello"); try { Thread.sleep(1000); //1000 milliseconds is one second. } catch(InterruptedException ex) { diff --git a/website/source/docs/drivers/java.html.md b/website/source/docs/drivers/java.html.md index 9451c35678e1..48b8a5a22676 100644 --- a/website/source/docs/drivers/java.html.md +++ b/website/source/docs/drivers/java.html.md @@ -29,7 +29,14 @@ task "webservice" { The `java` driver supports the following configuration in the job spec: -* `jar_path` - The path to the downloaded Jar. In most cases this will just be +* `class` - (Optional) The name of the class to run. If `jar_path` is specified + and the manifest specifies a main class, this is optional. If shipping classes + rather than a Jar, please specify the class to run and the `class_path`. + +* `class_path` - (Optional) The `class_path` specifies the clath path used by + Java to lookup classes and Jars. + +* `jar_path` - (Optional) The path to the downloaded Jar. In most cases this will just be the name of the Jar. However, if the supplied artifact is an archive that contains the Jar in a subfolder, the path will need to be the relative path (`subdir/from_archive/my.jar`). @@ -67,6 +74,30 @@ task "web" { } ``` +A simple config block to run a Java class: + +```hcl +task "web" { + driver = "java" + + config { + class = "Hello" + class_path = "${NOMAD_TASK_DIR}" + jvm_options = ["-Xmx2048m", "-Xms256m"] + } + + # Specifying an artifact is required with the "java" driver. This is the + # mechanism to ship the Jar to be run. + artifact { + source = "https://internal.file.server/Hello.class" + + options { + checksum = "md5:123445555555555" + } + } +} +``` + ## Client Requirements The `java` driver requires Java to be installed and in your system's `$PATH`. On