Merge pull request #381 from hashicorp/f-getter-refactor

client/drivers: Refactor to use Getter wrapper
This commit is contained in:
Clint 2015-11-05 13:47:42 -06:00
commit e494a24335
14 changed files with 260 additions and 113 deletions

View file

@ -2,17 +2,15 @@ package driver
import (
"fmt"
"log"
"path"
"path/filepath"
"runtime"
"syscall"
"time"
"github.com/hashicorp/go-getter"
"github.com/hashicorp/nomad/client/allocdir"
"github.com/hashicorp/nomad/client/config"
"github.com/hashicorp/nomad/client/driver/executor"
"github.com/hashicorp/nomad/client/getter"
"github.com/hashicorp/nomad/nomad/structs"
)
@ -55,29 +53,24 @@ func (d *ExecDriver) Start(ctx *ExecContext, task *structs.Task) (DriverHandle,
return nil, fmt.Errorf("missing command for exec driver")
}
// Check if an artificat is specified and attempt to download it
source, ok := task.Config["artifact_source"]
if ok && source != "" {
// Proceed to download an artifact to be executed.
// We use go-getter to support a variety of protocols, but need to change
// file permissions of the resulted download to be executable
// Create a location to download the artifact.
taskDir, ok := ctx.AllocDir.TaskDirs[d.DriverContext.taskName]
if !ok {
return nil, fmt.Errorf("Could not find task directory for task: %v", d.DriverContext.taskName)
}
destDir := filepath.Join(taskDir, allocdir.TaskLocal)
artifactName := path.Base(source)
artifactFile := filepath.Join(destDir, artifactName)
if err := getter.GetFile(artifactFile, source); err != nil {
return nil, fmt.Errorf("Error downloading artifact for Exec driver: %s", err)
}
// Add execution permissions to the newly downloaded artifact
if err := syscall.Chmod(artifactFile, 0755); err != nil {
log.Printf("[ERR] driver.exec: Error making artifact executable: %s", err)
// Check if an artificat is specified and attempt to download it
source, ok := task.Config["artifact_source"]
if ok && source != "" {
// Proceed to download an artifact to be executed.
_, err := getter.GetArtifact(
filepath.Join(taskDir, allocdir.TaskLocal),
task.Config["artifact_source"],
task.Config["checksum"],
d.logger,
)
if err != nil {
return nil, err
}
}

View file

@ -123,11 +123,12 @@ func TestExecDriver_Start_Wait(t *testing.T) {
func TestExecDriver_Start_Artifact_basic(t *testing.T) {
ctestutils.ExecCompatible(t)
file := "hi_linux_amd64"
checksum := "sha256:6f99b4c5184726e601ecb062500aeb9537862434dfe1898dbe5c68d9f50c179c"
task := &structs.Task{
Name: "sleep",
Config: map[string]string{
"artifact_source": fmt.Sprintf("https://dl.dropboxusercontent.com/u/47675/jar_thing/%s", file),
"artifact_source": fmt.Sprintf("https://dl.dropboxusercontent.com/u/47675/jar_thing/%s?checksum=%s", file, checksum),
"command": filepath.Join("$NOMAD_TASK_DIR", file),
},
Resources: basicResources,

View file

@ -4,17 +4,16 @@ import (
"bytes"
"fmt"
"os/exec"
"path"
"path/filepath"
"runtime"
"strings"
"syscall"
"time"
"github.com/hashicorp/go-getter"
"github.com/hashicorp/nomad/client/allocdir"
"github.com/hashicorp/nomad/client/config"
"github.com/hashicorp/nomad/client/driver/executor"
"github.com/hashicorp/nomad/client/getter"
"github.com/hashicorp/nomad/nomad/structs"
)
@ -89,26 +88,24 @@ func (d *JavaDriver) Fingerprint(cfg *config.Config, node *structs.Node) (bool,
}
func (d *JavaDriver) Start(ctx *ExecContext, task *structs.Task) (DriverHandle, error) {
// Get the jar source
source, ok := task.Config["jar_source"]
if !ok || source == "" {
return nil, fmt.Errorf("missing jar source for Java Jar driver")
}
taskDir, ok := ctx.AllocDir.TaskDirs[d.DriverContext.taskName]
if !ok {
return nil, fmt.Errorf("Could not find task directory for task: %v", d.DriverContext.taskName)
}
destDir := filepath.Join(taskDir, allocdir.TaskLocal)
// Create a location to download the binary.
jarName := path.Base(source)
jarPath := filepath.Join(destDir, jarName)
if err := getter.GetFile(jarPath, source); err != nil {
return nil, fmt.Errorf("Error downloading source for Java driver: %s", err)
// Proceed to download an artifact to be executed.
path, err := getter.GetArtifact(
filepath.Join(taskDir, allocdir.TaskLocal),
task.Config["artifact_source"],
task.Config["checksum"],
d.logger,
)
if err != nil {
return nil, err
}
jarName := filepath.Base(path)
// Get the environment variables.
envVars := TaskEnvironmentVariables(ctx, task)

View file

@ -97,10 +97,9 @@ func TestJavaDriver_Start_Wait(t *testing.T) {
task := &structs.Task{
Name: "demo-app",
Config: map[string]string{
"jar_source": "https://dl.dropboxusercontent.com/u/47675/jar_thing/demoapp.jar",
// "jar_source": "https://s3-us-west-2.amazonaws.com/java-jar-thing/demoapp.jar",
// "args": "-d64",
"artifact_source": "https://dl.dropboxusercontent.com/u/47675/jar_thing/demoapp.jar",
"jvm_options": "-Xmx2048m -Xms256m",
"checksum": "sha256:58d6e8130308d32e197c5108edd4f56ddf1417408f743097c2e662df0f0b17c8",
},
Resources: basicResources,
}
@ -145,9 +144,7 @@ func TestJavaDriver_Start_Kill_Wait(t *testing.T) {
task := &structs.Task{
Name: "demo-app",
Config: map[string]string{
"jar_source": "https://dl.dropboxusercontent.com/u/47675/jar_thing/demoapp.jar",
// "jar_source": "https://s3-us-west-2.amazonaws.com/java-jar-thing/demoapp.jar",
// "args": "-d64",
"artifact_source": "https://dl.dropboxusercontent.com/u/47675/jar_thing/demoapp.jar",
},
Resources: basicResources,
}

View file

@ -2,11 +2,8 @@ package driver
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"log"
"os"
"os/exec"
@ -17,9 +14,9 @@ import (
"strings"
"time"
"github.com/hashicorp/go-getter"
"github.com/hashicorp/nomad/client/allocdir"
"github.com/hashicorp/nomad/client/config"
"github.com/hashicorp/nomad/client/getter"
"github.com/hashicorp/nomad/nomad/structs"
)
@ -82,7 +79,7 @@ func (d *QemuDriver) Fingerprint(cfg *config.Config, node *structs.Node) (bool,
// image and save it to the Drivers Allocation Dir
func (d *QemuDriver) Start(ctx *ExecContext, task *structs.Task) (DriverHandle, error) {
// Get the image source
source, ok := task.Config["image_source"]
source, ok := task.Config["artifact_source"]
if !ok || source == "" {
return nil, fmt.Errorf("Missing source image Qemu driver")
}
@ -99,34 +96,18 @@ func (d *QemuDriver) Start(ctx *ExecContext, task *structs.Task) (DriverHandle,
return nil, fmt.Errorf("Could not find task directory for task: %v", d.DriverContext.taskName)
}
// Create a location to download the binary.
destDir := filepath.Join(taskDir, allocdir.TaskLocal)
vmID := fmt.Sprintf("qemu-vm-%s-%s", structs.GenerateUUID(), filepath.Base(source))
vmPath := filepath.Join(destDir, vmID)
if err := getter.GetFile(vmPath, source); err != nil {
return nil, fmt.Errorf("Error downloading artifact for Qemu driver: %s", err)
}
// compute and check checksum
if check, ok := task.Config["checksum"]; ok {
d.logger.Printf("[DEBUG] Running checksum on (%s)", vmID)
hasher := sha256.New()
file, err := os.Open(vmPath)
// Proceed to download an artifact to be executed.
vmPath, err := getter.GetArtifact(
filepath.Join(taskDir, allocdir.TaskLocal),
task.Config["artifact_source"],
task.Config["checksum"],
d.logger,
)
if err != nil {
return nil, fmt.Errorf("Failed to open file for checksum")
return nil, err
}
defer file.Close()
io.Copy(hasher, file)
sum := hex.EncodeToString(hasher.Sum(nil))
if sum != check {
return nil, fmt.Errorf(
"Error in Qemu: checksums did not match.\nExpected (%s), got (%s)",
check,
sum)
}
}
vmID := filepath.Base(vmPath)
// Parse configuration arguments
// Create the base arguments

View file

@ -54,8 +54,8 @@ func TestQemuDriver_Start(t *testing.T) {
task := &structs.Task{
Name: "linux",
Config: map[string]string{
"image_source": "https://dl.dropboxusercontent.com/u/47675/jar_thing/linux-0.2.img",
"checksum": "a5e836985934c3392cbbd9b26db55a7d35a8d7ae1deb7ca559dd9c0159572544",
"artifact_source": "https://dl.dropboxusercontent.com/u/47675/jar_thing/linux-0.2.img",
"checksum": "sha256:a5e836985934c3392cbbd9b26db55a7d35a8d7ae1deb7ca559dd9c0159572544",
"accelerator": "tcg",
"guest_ports": "22,8080",
},
@ -103,11 +103,11 @@ func TestQemuDriver_RequiresMemory(t *testing.T) {
task := &structs.Task{
Name: "linux",
Config: map[string]string{
"image_source": "https://dl.dropboxusercontent.com/u/47675/jar_thing/linux-0.2.img",
"artifact_source": "https://dl.dropboxusercontent.com/u/47675/jar_thing/linux-0.2.img",
"accelerator": "tcg",
"host_port": "8080",
"guest_port": "8081",
"checksum": "a5e836985934c3392cbbd9b26db55a7d35a8d7ae1deb7ca559dd9c0159572544",
"checksum": "sha256:a5e836985934c3392cbbd9b26db55a7d35a8d7ae1deb7ca559dd9c0159572544",
// ssh u/p would be here
},
}

View file

@ -2,21 +2,18 @@ package driver
import (
"fmt"
"log"
"os"
"os/exec"
"path"
"path/filepath"
"runtime"
"strconv"
"strings"
"syscall"
"time"
"github.com/hashicorp/go-getter"
"github.com/hashicorp/nomad/client/allocdir"
"github.com/hashicorp/nomad/client/config"
"github.com/hashicorp/nomad/client/driver/args"
"github.com/hashicorp/nomad/client/getter"
"github.com/hashicorp/nomad/nomad/structs"
)
@ -83,23 +80,14 @@ func (d *RawExecDriver) Start(ctx *ExecContext, task *structs.Task) (DriverHandl
source, ok := task.Config["artifact_source"]
if ok && source != "" {
// Proceed to download an artifact to be executed.
// We use go-getter to support a variety of protocols, but need to change
// file permissions of the resulted download to be executable
// Create a location to download the artifact.
destDir := filepath.Join(taskDir, allocdir.TaskLocal)
artifactName := path.Base(source)
artifactFile := filepath.Join(destDir, artifactName)
if err := getter.GetFile(artifactFile, source); err != nil {
return nil, fmt.Errorf("Error downloading artifact for Raw Exec driver: %s", err)
}
// Add execution permissions to the newly downloaded artifact
if runtime.GOOS != "windows" {
if err := syscall.Chmod(artifactFile, 0755); err != nil {
log.Printf("[ERR] driver.raw_exec: Error making artifact executable: %s", err)
}
_, err := getter.GetArtifact(
filepath.Join(taskDir, allocdir.TaskLocal),
task.Config["artifact_source"],
task.Config["checksum"],
d.logger,
)
if err != nil {
return nil, err
}
}

View file

@ -94,12 +94,14 @@ func TestRawExecDriver_StartOpen_Wait(t *testing.T) {
}
func TestRawExecDriver_Start_Artifact_basic(t *testing.T) {
var file string
var file, checksum string
switch runtime.GOOS {
case "darwin":
file = "hi_darwin_amd64"
checksum = "md5:d7f2fdb13b36dcb7407721d78926b335"
default:
file = "hi_linux_amd64"
checksum = "md5:a9b14903a8942748e4f8474e11f795d3"
}
task := &structs.Task{
@ -107,6 +109,7 @@ func TestRawExecDriver_Start_Artifact_basic(t *testing.T) {
Config: map[string]string{
"artifact_source": fmt.Sprintf("https://dl.dropboxusercontent.com/u/47675/jar_thing/%s", file),
"command": filepath.Join("$NOMAD_TASK_DIR", file),
"checksum": checksum,
},
}
driverCtx := testDriverContext(task.Name)

43
client/getter/getter.go Normal file
View file

@ -0,0 +1,43 @@
package getter
import (
"fmt"
"log"
"net/url"
"path"
"path/filepath"
"runtime"
"strings"
"syscall"
gg "github.com/hashicorp/go-getter"
)
func GetArtifact(destDir, source, checksum string, logger *log.Logger) (string, error) {
if source == "" {
return "", fmt.Errorf("Source url is empty in Artifact Getter")
}
u, err := url.Parse(source)
if err != nil {
return "", err
}
// if checksum is seperate, apply to source
if checksum != "" {
source = strings.Join([]string{source, fmt.Sprintf("checksum=%s", checksum)}, "?")
logger.Printf("[DEBUG] client.getter: Applying checksum to Artifact Source URL, new url: %s", source)
}
artifactFile := filepath.Join(destDir, path.Base(u.Path))
if err := gg.GetFile(artifactFile, source); err != nil {
return "", fmt.Errorf("Error downloading artifact: %s", err)
}
// Add execution permissions to the newly downloaded artifact
if runtime.GOOS != "windows" {
if err := syscall.Chmod(artifactFile, 0755); err != nil {
logger.Printf("[ERR] driver.raw_exec: Error making artifact executable: %s", err)
}
}
return artifactFile, nil
}

View file

@ -0,0 +1,111 @@
package getter
import (
"fmt"
"io/ioutil"
"log"
"os"
"strings"
"testing"
)
func TestGetArtifact_basic(t *testing.T) {
logger := log.New(os.Stderr, "", log.LstdFlags)
// TODO: Use http.TestServer to serve these files from fixtures dir
passing := []struct {
Source, Checksum string
}{
{
"https://dl.dropboxusercontent.com/u/47675/jar_thing/hi_darwin_amd64",
"sha256:66aa0f05fc0cfcf1e5ed8cc5307b5df51e33871d6b295a60e0f9f6dd573da977",
},
{
"https://dl.dropboxusercontent.com/u/47675/jar_thing/hi_linux_amd64",
"sha256:6f99b4c5184726e601ecb062500aeb9537862434dfe1898dbe5c68d9f50c179c",
},
{
"https://dl.dropboxusercontent.com/u/47675/jar_thing/hi_linux_amd64",
"md5:a9b14903a8942748e4f8474e11f795d3",
},
{
"https://dl.dropboxusercontent.com/u/47675/jar_thing/hi_linux_amd64?checksum=sha256:6f99b4c5184726e601ecb062500aeb9537862434dfe1898dbe5c68d9f50c179c",
"",
},
{
"https://dl.dropboxusercontent.com/u/47675/jar_thing/hi_linux_amd64",
"",
},
}
for i, p := range passing {
destDir, err := ioutil.TempDir("", fmt.Sprintf("nomad-test-%d", i))
if err != nil {
t.Fatalf("Error in TestGetArtifact_basic makeing TempDir: %s", err)
}
path, err := GetArtifact(destDir, p.Source, p.Checksum, logger)
if err != nil {
t.Fatalf("TestGetArtifact_basic unexpected failure here: %s", err)
}
if p.Checksum != "" {
if ok := strings.Contains(path, p.Checksum); ok {
t.Fatalf("path result should not contain the checksum, got: %s", path)
}
}
// verify artifact exists
if _, err := os.Stat(path); err != nil {
t.Fatalf("source path error: %s", err)
}
}
}
func TestGetArtifact_fails(t *testing.T) {
logger := log.New(os.Stderr, "", log.LstdFlags)
failing := []struct {
Source, Checksum string
}{
{
"",
"sha256:66aa0f05fc0cfcf1e5ed8cc5307b5d",
},
{
"/u/47675/jar_thing/hi_darwin_amd64",
"sha256:66aa0f05fc0cfcf1e5ed8cc5307b5d",
},
{
"https://dl.dropboxusercontent.com/u/47675/jar_thing/hi_darwin_amd64",
"sha256:66aa0f05fc0cfcf1e5ed8cc5307b5d",
},
{
"https://dl.dropboxusercontent.com/u/47675/jar_thing/hi_linux_amd64",
"sha257:6f99b4c5184726e601ecb062500aeb9537862434dfe1898dbe5c68d9f50c179c",
},
// malformed checksum
{
"https://dl.dropboxusercontent.com/u/47675/jar_thing/hi_linux_amd64",
"6f99b4c5184726e601ecb062500aeb9537862434dfe1898dbe5c68d9f50c179c",
},
// 404
{
"https://dl.dropboxusercontent.com/u/47675/jar_thing/hi_linux_amd86",
"",
},
}
for i, p := range failing {
destDir, err := ioutil.TempDir("", fmt.Sprintf("nomad-test-%d", i))
if err != nil {
t.Fatalf("Error in TestGetArtifact_basic makeing TempDir: %s", err)
}
_, err = GetArtifact(destDir, p.Source, p.Checksum, logger)
if err == nil {
t.Fatalf("TestGetArtifact_basic expected failure, but got none")
}
}
}

View file

@ -24,6 +24,10 @@ The `exec` driver supports the following configuration in the job spec:
* `artifact_source` (Optional) Source location of an executable artifact. Must be accessible
from the Nomad client. If you specify an `artifact_source` to be executed, you
must reference it in the `command` as show in the examples below
* `checksum` - **(Optional)** The checksum type and value for the `artifact_source` image.
The format is `type:value`, where type is any of `md5`, `sha1`, `sha256`, or `sha512`,
and the value is the computed checksum. If a checksum is supplied and does not
match the downloaded artifact, the driver will fail to start
* `args` - The argument list to the command, space seperated. Optional.
## Client Requirements
@ -53,6 +57,7 @@ To execute a binary specified by `artifact_source`:
```
config {
artifact_source = "https://dl.dropboxusercontent.com/u/1234/binary.bin"
checksum = "sha256:abd123445ds4555555555"
command = "$NOMAD_TASK_DIR/binary.bin"
}
```

View file

@ -18,8 +18,12 @@ HTTP from the Nomad client.
The `java` driver supports the following configuration in the job spec:
* `jar_source` - **(Required)** The hosted location of the source Jar file. Must be accessible
* `artifact_source` - **(Required)** The hosted location of the source Jar file. Must be accessible
from the Nomad client
* `checksum` - **(Optional)** The checksum type and value for the `artifact_source` image.
The format is `type:value`, where type is any of `md5`, `sha1`, `sha256`, or `sha512`,
and the value is the computed checksum. If a checksum is supplied and does not
match the downloaded artifact, the driver will fail to start
* `args` - **(Optional)** The argument list for the `java` command, space separated.
@ -29,10 +33,27 @@ from the Nomad client
## Client Requirements
The `java` driver requires Java to be installed and in your systems `$PATH`.
The `jar_source` must be accessible by the node running Nomad. This can be an
The `artifact_source` must be accessible by the node running Nomad. This can be an
internal source, private to your cluster, but it must be reachable by the client
over HTTP.
## Examples
A simple config block to run a Java Jar:
```json
# Define a task to run
task "web" {
# Run a Java Jar
driver = "java"
config {
artifact_source = "https://dl.dropboxusercontent.com/u/1234/hello.jar"
checksum = "md5:123445555555555"
jvm_options = "-Xmx2048m -Xms256m"
}
```
## Client Attributes
The `java` driver will set the following client attributes:

View file

@ -23,10 +23,12 @@ The `Qemu` driver can execute any regular `qemu` image (e.g. `qcow`, `img`,
The `Qemu` driver supports the following configuration in the job spec:
* `image_source` - **(Required)** The hosted location of the source Qemu image. Must be accessible
* `artifact_source` - **(Required)** The hosted location of the source Qemu image. Must be accessible
from the Nomad client, via HTTP.
* `checksum` - **(Required)** The SHA256 checksum of the `qemu` image. If the
checksums do not match, the `Qemu` driver will fail to start the image
* `checksum` - **(Optional)** The checksum type and value for the `artifact_source` image.
The format is `type:value`, where type is any of `md5`, `sha1`, `sha256`, or `sha512`,
and the value is the computed checksum. If a checksum is supplied and does not
match the downloaded artifact, the driver will fail to start
* `accelerator` - (Optional) The type of accelerator to use in the invocation.
If the host machine has `Qemu` installed with KVM support, users can specify `kvm` for the `accelerator`. Default is `tcg`
* `host_port` - **(Required)** Port on the host machine to forward to the guest
@ -38,7 +40,7 @@ in the `Task` specification
## Client Requirements
The `Qemu` driver requires Qemu to be installed and in your system's `$PATH`.
The `image_source` must be accessible by the node running Nomad. This can be an
The `artifact_source` must be accessible by the node running Nomad. This can be an
internal source, private to your cluster, but it must be reachable by the client
over HTTP.

View file

@ -22,6 +22,10 @@ The `raw_exec` driver supports the following configuration in the job spec:
* `artifact_source` (Optional) Source location of an executable artifact. Must be accessible
from the Nomad client. If you specify an `artifact_source` to be executed, you
must reference it in the `command` as show in the examples below
* `checksum` - **(Optional)** The checksum type and value for the `artifact_source` image.
The format is `type:value`, where type is any of `md5`, `sha1`, `sha256`, or `sha512`,
and the value is the computed checksum. If a checksum is supplied and does not
match the downloaded artifact, the driver will fail to start
* `args` - The argument list to the command, space seperated. Optional.
## Client Requirements
@ -57,6 +61,7 @@ To execute a binary specified by `artifact_source`:
```
config {
artifact_source = "https://dl.dropboxusercontent.com/u/1234/binary.bin"
checksum = "sha256:133jifjiofu9090fsadjofsdjlk"
command = "$NOMAD_TASK_DIR/binary.bin"
}
```