Skip to content

Commit

Permalink
Support source files and improve error handling
Browse files Browse the repository at this point in the history
Signed-off-by: Andy Lo-A-Foe <[email protected]>
  • Loading branch information
loafoe committed Jan 25, 2021
1 parent acc481f commit 8ce553a
Show file tree
Hide file tree
Showing 4 changed files with 135 additions and 53 deletions.
70 changes: 36 additions & 34 deletions docs/resources/container_host.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,42 @@
# hsdp_container_host
Provides HSDP Container Host instances
Manage HSDP Container Host instances

> This resource is only available when the `cartel_*` keys are set in the provider config
## Example Usage

The following example provisions three (3) new container host instances
The following example provisions and bootstraps a container host instance:

```hcl
resource "hsdp_container_host" "zahadoom" {
name = "mybox.dev"
instance_type = "t2.medium"
user_groups = var.user_groups
security_groups = ["analytics", var.user]
bastion_host = var.bastion_host
user = var.user
private_key = var.private_key
tags = {
created_by = "terraform"
}
file {
content = "This string will be stored remotely"
destination = "/tmp/stored.txt"
}
commands = [
"cat /tmp/stored.txt",
"docker volume create fluent-bit",
"docker run -d -p 24224:24224 -v fluent-bit:/fluent-bit/etc philipssoftware/fluent-bit-out-hsdp:1.4.4"
]
}
```

The following example provisions three (3) new container host instances and using Terraform's traditional provisioners

```hcl
resource "hsdp_container_host" "zahadoom" {
Expand Down Expand Up @@ -41,36 +72,6 @@ resource "hsdp_container_host" "zahadoom" {
}
```

The following example uses the internal provisioning support for bootstrapping an instance

```hcl
resource "hsdp_container_host" "zahadoom" {
name = "mybox.dev"
instance_type = "t2.medium"
user_groups = var.user_groups
security_groups = ["analytics", var.user]
bastion_host = var.bastion_host
user = var.user
private_key = var.private_key
tags = {
created_by = "terraform"
}
file {
content = "This string will be stored remotely"
destination = "/tmp/stored.txt"
}
commands = [
"cat /tmp/stored.txt",
"docker volume create fluent-bit",
"docker run -d -p 24224:24224 -v fluent-bit:/fluent-bit/etc philipssoftware/fluent-bit-out-hsdp:1.4.4"
]
}
```

## Argument Reference

Expand All @@ -96,9 +97,10 @@ The following arguments are supported:
* `commands` - (Optional, list(string)) List of commands to execute after creation of container host
* `bastion_host` - (Optional) The bastion host to use. When not set, this will be deduced from the container host location

Each `file` block should contain the following fields:
Each `file` block can contain the following fields. Use either `content` or `source`:

* `content` - (Required, string) Content of the file
* `source` - (Optional, file path) Content of the file. Conflicts with `content`
* `content` - (Optional, string) Content of the file. Conflicts with `source`
* `destination` - (Required, string) Remote filename to store the content in

## Attributes Reference
Expand Down
7 changes: 4 additions & 3 deletions docs/resources/container_host_exec.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# hsdp_container_host_exec
Copies content and executes command on Container Host instances
Copies content and executes commands on Container Host instances

> This resource is only available when the `cartel_*` keys are set in the provider config
Expand Down Expand Up @@ -37,9 +37,10 @@ The following arguments are supported:
* `triggers` - (Optiona, list(string)) An list of strings which when changes will trigger recreation of the resource triggering
all create files and commands executions.

Each `file` block should contain the following fields:
Each `file` block can contain the following fields. Use either `content` or `source`:

* `content` - (Required, string) Content of the file
* `source` - (Optional, file path) Content of the file. Conflicts with `content`
* `content` - (Optional, string) Content of the file. Conflicts with `source`
* `destination` - (Required, string) Remote filename to store the content in

## Attributes Reference
Expand Down
91 changes: 83 additions & 8 deletions hsdp/resource_container_host.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
"github.com/loafoe/easyssh-proxy/v2"
"github.com/philips-software/go-hsdp-api/cartel"
"os"

"log"
"net/http"
Expand Down Expand Up @@ -175,13 +176,17 @@ func resourceContainerHost() *schema.Resource {
Optional: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"source": {
Type: schema.TypeString,
Optional: true,
},
"content": {
Type: schema.TypeString,
Optional: true,
},
"destination": {
Type: schema.TypeString,
Optional: true,
Required: true,
},
},
},
Expand Down Expand Up @@ -370,22 +375,52 @@ func resourceContainerHostCreate(ctx context.Context, d *schema.ResourceData, m
}

// Create files
for _, f := range createFiles {
buffer := bytes.NewBufferString(f.Content)
// Should we fail the complete provision on errors here?
_ = ssh.WriteFile(buffer, int64(buffer.Len()), f.Destination)
_, _ = config.Debug("Created remote file %s:%s: %d bytes\n", privateIP, f.Destination, buffer.Len())
if err := copyFiles(ssh, config, createFiles); err != nil {
diags = append(diags, diag.Diagnostic{
Severity: diag.Warning,
Summary: "failed to copy all files",
Detail: fmt.Sprintf("One or more files failed to copy: %v", err),
})
}

// Run commands
for i := 0; i < len(commands); i++ {
stdout, stderr, done, err := ssh.Run(commands[i], 5*time.Minute)
if err != nil {
return diag.FromErr(err)
return append(diags, diag.FromErr(fmt.Errorf("command [%s]: %w", commands[i], err))...)
} else {
_, _ = config.Debug("command: %s\ndone: %t\nstdout:\n%s\nstderr:\n%s\n", commands[i], done, stdout, stderr)
}
}
return resourceContainerHostRead(ctx, d, m)
readDiags := resourceContainerHostRead(ctx, d, m)
return append(diags, readDiags...)
}

func copyFiles(ssh *easyssh.MakeConfig, config *Config, createFiles []provisionFile) error {
for _, f := range createFiles {
if f.Source != "" {
src, srcErr := os.Open(f.Source)
if srcErr != nil {
_, _ = config.Debug("Failed to open source file %s: %v\n", f.Source, srcErr)
return srcErr
}
srcStat, statErr := src.Stat()
if statErr != nil {
_, _ = config.Debug("Failed to stat source file %s: %v\n", f.Source, statErr)
_ = src.Close()
return statErr
}
_ = ssh.WriteFile(src, srcStat.Size(), f.Destination)
_, _ = config.Debug("Copied %s to remote file %s:%s: %d bytes\n", f.Source, ssh.Server, f.Destination, srcStat.Size())
_ = src.Close()
} else {
buffer := bytes.NewBufferString(f.Content)
// Should we fail the complete provision on errors here?
_ = ssh.WriteFile(buffer, int64(buffer.Len()), f.Destination)
_, _ = config.Debug("Created remote file %s:%s: %d bytes\n", ssh.Server, f.Destination, buffer.Len())
}
}
return nil
}

func collectCommands(d *schema.ResourceData) ([]string, diag.Diagnostics) {
Expand All @@ -400,6 +435,7 @@ func collectCommands(d *schema.ResourceData) ([]string, diag.Diagnostics) {
}

type provisionFile struct {
Source string
Content string
Destination string
}
Expand All @@ -412,9 +448,48 @@ func collectFilesToCreate(d *schema.ResourceData) ([]provisionFile, diag.Diagnos
for _, vi := range vL {
mVi := vi.(map[string]interface{})
file := provisionFile{
Source: mVi["source"].(string),
Content: mVi["content"].(string),
Destination: mVi["destination"].(string),
}
if file.Source == "" && file.Content == "" {
diags = append(diags, diag.Diagnostic{
Severity: diag.Error,
Summary: "conflict in file block",
Detail: fmt.Sprintf("file %s has neither 'source' or 'content', set one", file.Destination),
})
continue
}
if file.Source != "" && file.Content != "" {
diags = append(diags, diag.Diagnostic{
Severity: diag.Error,
Summary: "conflict in file block",
Detail: fmt.Sprintf("file %s has conflicting 'source' and 'content', choose only one", file.Destination),
})
continue
}
if file.Source != "" {
src, srcErr := os.Open(file.Source)
if srcErr != nil {
diags = append(diags, diag.Diagnostic{
Severity: diag.Error,
Summary: "issue with source",
Detail: fmt.Sprintf("file %s: %v", file.Source, srcErr),
})
continue
}
_, statErr := src.Stat()
if statErr != nil {
diags = append(diags, diag.Diagnostic{
Severity: diag.Error,
Summary: "issue with source stat",
Detail: fmt.Sprintf("file %s: %v", file.Source, statErr),
})
_ = src.Close()
continue
}
_ = src.Close()
}
files = append(files, file)
}
}
Expand Down
20 changes: 12 additions & 8 deletions hsdp/resource_container_host_exec.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package hsdp

import (
"bytes"
"context"
"fmt"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
Expand Down Expand Up @@ -62,13 +61,20 @@ The ` + "`triggers`" + ` argument allows specifying an arbitrary set of values t
ForceNew: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"source": {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
"content": {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
"destination": {
Type: schema.TypeString,
Optional: true,
Required: true,
ForceNew: true,
},
},
},
Expand Down Expand Up @@ -129,17 +135,15 @@ func resourceContainerHostExecCreate(_ context.Context, d *schema.ResourceData,
}

// Provision files
for _, f := range createFiles {
buffer := bytes.NewBufferString(f.Content)
// Should we fail the complete provision on error here?
_ = ssh.WriteFile(buffer, int64(buffer.Len()), f.Destination)
_, _ = config.Debug("Wrote remote %s:%s: %d bytes\n", privateIP, f.Destination, len(f.Content))
if err := copyFiles(ssh, config, createFiles); err != nil {
return diag.FromErr(fmt.Errorf("copying files to remote: %w", err))
}

// Run commands
for i := 0; i < len(commands); i++ {
stdout, stderr, done, err := ssh.Run(commands[i], 5*time.Minute)
if err != nil {
return diag.FromErr(err)
return append(diags, diag.FromErr(fmt.Errorf("command [%s]: %w", commands[i], err))...)
} else {
_, _ = config.Debug("command: %s\ndone: %t\nstdout:\n%s\nstderr:\n%s\n", commands[i], done, stdout, stderr)
}
Expand Down

0 comments on commit 8ce553a

Please sign in to comment.