Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion internal/kind/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package kind

import (
"context"
"encoding/base64"
"errors"
"fmt"
Expand Down Expand Up @@ -235,7 +236,7 @@ func (c *localClient) LoadImage(req LoadImageRequest) (LoadedImageResult, error)

// saved the docker image to a tar archive
commandArgs := append([]string{"save", "-o", imageTar}, image)
if err := exec.Command("docker", commandArgs...).Run(); err != nil {
if err := exec.CommandContext(context.Background(), "docker", commandArgs...).Run(); err != nil {
return LoadedImageResult{}, fmt.Errorf("failed to export image: [%s] to archive: [%s], due to: %w", imageName, imageTar, err)
}

Expand Down
35 changes: 29 additions & 6 deletions internal/plugin/resource_file.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,21 +152,35 @@ func (f *file) PlanResourceChange(ctx context.Context, req resource.PlanResource

// If we have unknown attributes we can't generate a valid Sum
if !proposedState.hasUnknownAttributes() {
// Load the file source
// Load the file source, but do not hard error if it does not exist
src, srcType, err := proposedState.openSourceOrContent()
if err != nil {
res.Diagnostics = append(res.Diagnostics, diags.ErrToDiagnostic("Invalid Configuration", err))
// If the user provided "source", but it does not exist, warn but don't fail plan
// Only warn if the source is missing, not if content is missing (which is a config error)
srcPath, okSrc := proposedState.Src.Get()
if okSrc {
res.Diagnostics = append(res.Diagnostics, diags.ErrToDiagnosticWarn(
"Source file not found at plan time",
fmt.Errorf("the source file %q does not exist or is unreadable at plan time; it will be required and validated at apply time", srcPath),
))
// Mark sum as unknown because we can't compute it
proposedState.Sum.Unknown = true
} else {
res.Diagnostics = append(res.Diagnostics, diags.ErrToDiagnostic("Invalid Configuration", err))
}

return
}
defer src.Close()

// Get the file's SHA256 sum, which we'll use to determine if the resource needs to be updated.
sum, err := tfile.SHA256(src)
if err != nil {
res.Diagnostics = append(res.Diagnostics, diags.ErrToDiagnostic(
"Invalid Configuration",
fmt.Errorf("unable to obtain file SHA256 sum for %s, due to: %w", srcType, err),
res.Diagnostics = append(res.Diagnostics, diags.ErrToDiagnosticWarn(
"Unable to hash file at plan time",
fmt.Errorf("unable to obtain file SHA256 sum for %s at plan time; file will be required and validated at apply time: %w", srcType, err),
))
proposedState.Sum.Unknown = true

return
}
Expand Down Expand Up @@ -200,7 +214,16 @@ func (f *file) ApplyResourceChange(ctx context.Context, req resource.ApplyResour

src, _, err := plannedState.openSourceOrContent()
if err != nil {
res.Diagnostics = append(res.Diagnostics, diags.ErrToDiagnostic("Invalid Configuration", err))
srcPath, okSrc := plannedState.Src.Get()
if okSrc {
res.Diagnostics = append(res.Diagnostics, diags.ErrToDiagnostic(
"Source file required at apply time",
fmt.Errorf("the source file %q does not exist or is unreadable; it must exist at apply time for provisioning", srcPath),
))
} else {
res.Diagnostics = append(res.Diagnostics, diags.ErrToDiagnostic("Invalid Configuration", err))
}

return
}
defer src.Close()
Expand Down
213 changes: 213 additions & 0 deletions internal/plugin/resource_file_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,219 @@ EOF
}
}

// TestAccResourceFileLazyCopyMissingSourceFile tests the lazy copy feature
// of the enos_file basic resource interface but also the embedded transport interface.
// As the embedded transport isn't an actual resource we're doing it here.
//
//nolint:paralleltest// because we modify the environment
func TestAccResourceFileLazyCopyMissingSourceFile(t *testing.T) {
defer resetEnv(t)

const missingFile = "/tmp/enos_file_missing_for_lazy_copy_test"

providerTransport := template.Must(template.New("enos_file").Parse(`resource "enos_file" "{{.ID.Value}}" {
{{if .Src.Value}}
source = "{{.Src.Value}}"
{{end}}

{{if .Content.Value}}
content = <<EOF
{{.Content.Value}}
EOF
{{end}}

destination = "{{.Dst.Value}}"
}`))

resourceTransport := template.Must(template.New("enos_file").
Funcs(transportRenderFunc).
Parse(`resource "enos_file" "{{.ID.Value}}" {
{{if .Src.Value}}
source = "{{.Src.Value}}"
{{end}}

{{if .Content.Value}}
content = <<EOF
{{.Content.Value}}
EOF
{{end}}

destination = "{{.Dst.Value}}"

{{ renderTransport .Transport }}
}`))

cases := []testAccResourceTransportTemplate{}

// SSH
sshState := newFileState()
sshState.ID.Set("lazycopy_ssh")
sshState.Src.Set(missingFile)
sshState.Dst.Set("/tmp/dst")
ssh := newEmbeddedTransportSSH()
ssh.User.Set("ubuntu")
ssh.Host.Set("localhost")
ssh.PrivateKeyPath.Set("../fixtures/ssh.pem")
require.NoError(t, sshState.Transport.SetTransportState(ssh))
cases = append(cases, testAccResourceTransportTemplate{
name: "[ssh] lazy copy missing source",
state: sshState,
check: resource.ComposeTestCheckFunc(),
transport: sshState.Transport,
resourceTemplate: resourceTransport,
transportUsed: SSH,
})

// K8S
k8sState := newFileState()
k8sState.ID.Set("lazycopy_k8s")
k8sState.Src.Set(missingFile)
k8sState.Dst.Set("/tmp/dst")
k8s := newEmbeddedTransportK8Sv1()
k8s.KubeConfigBase64.Set("../fixtures/kubeconfig")
k8s.ContextName.Set("kind-kind")
k8s.Pod.Set("some-pod")
require.NoError(t, k8sState.Transport.SetTransportState(k8s))
cases = append(cases, testAccResourceTransportTemplate{
name: "[k8s] lazy copy missing source",
state: k8sState,
check: resource.ComposeTestCheckFunc(),
transport: k8sState.Transport,
resourceTemplate: resourceTransport,
transportUsed: K8S,
})

// Nomad
nomadState := newFileState()
nomadState.ID.Set("lazycopy_nomad")
nomadState.Src.Set(missingFile)
nomadState.Dst.Set("/tmp/dst")
nomad := newEmbeddedTransportNomadv1()
nomad.Host.Set("http://127.0.0.1:4646")
nomad.SecretID.Set("secret")
nomad.AllocationID.Set("d76bc89d")
nomad.TaskName.Set("task")
require.NoError(t, nomadState.Transport.SetTransportState(nomad))
cases = append(cases, testAccResourceTransportTemplate{
name: "[nomad] lazy copy missing source",
state: nomadState,
check: resource.ComposeTestCheckFunc(),
transport: nomadState.Transport,
resourceTemplate: resourceTransport,
transportUsed: NOMAD,
})

for _, test := range cases {
// Resource-defined transport: PLAN
t.Run("resource transport plan "+test.name, func(t *testing.T) {
unsetAllEnosEnv(t)
defer resetEnv(t)

buf := bytes.Buffer{}
err := test.resourceTemplate.Execute(&buf, test.state)
require.NoError(t, err)
step := resource.TestStep{
Config: buf.String(),
Check: test.check,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll want to ensure that we check that a diagnostic is created during plan for the missing file.

We'll also want to test the inverse case where the file exists and doesn't render a diag.

PlanOnly: true,
ExpectNonEmptyPlan: true,
}

resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: testProviders(t),
Steps: []resource.TestStep{step},
})
})

// Resource-defined transport: APPLY
t.Run("resource transport apply "+test.name, func(t *testing.T) {
unsetAllEnosEnv(t)
defer resetEnv(t)

buf := bytes.Buffer{}
err := test.resourceTemplate.Execute(&buf, test.state)
require.NoError(t, err)
step := resource.TestStep{
Config: buf.String(),
PlanOnly: false,
ExpectError: regexp.MustCompile(`does not exist|unable to open|required at apply`),
}

resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: testProviders(t),
Steps: []resource.TestStep{step},
})
})

// Provider/env transport: PLAN
t.Run("provider transport plan "+test.name, func(t *testing.T) {
unsetAllEnosEnv(t)
defer resetEnv(t)

switch test.transportUsed {
case SSH:
setEnosSSHEnv(t, test.transport)
case K8S:
setEnosK8SEnv(t, test.transport)
case NOMAD:
setENosNomadEnv(t, test.transport)
default:
t.Errorf("undefined transport type: %s", test.transportUsed)
}
defer resetEnv(t)

buf := bytes.Buffer{}
err := providerTransport.Execute(&buf, test.state)
require.NoError(t, err)
step := resource.TestStep{
Config: buf.String(),
Check: test.check,
PlanOnly: true,
ExpectNonEmptyPlan: true,
}

resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: testProviders(t),
Steps: []resource.TestStep{step},
})
})

// Provider/env transport: APPLY
t.Run("provider transport apply "+test.name, func(t *testing.T) {
unsetAllEnosEnv(t)
switch test.transportUsed {
case SSH:
setEnosSSHEnv(t, test.transport)
case K8S:
setEnosK8SEnv(t, test.transport)
case NOMAD:
host, ok := os.LookupEnv("ENOS_TRANSPORT_HOST")
if !ok || host == "" {
t.Skip("Skipping Nomad provider transport apply: ENOS_TRANSPORT_HOST env var not set. Please set it to test this scenario.")
}
setENosNomadEnv(t, test.transport)
default:
t.Errorf("undefined transport type: %s", test.transportUsed)
}
defer resetEnv(t)

buf := bytes.Buffer{}
err := providerTransport.Execute(&buf, test.state)
require.NoError(t, err)
step := resource.TestStep{
Config: buf.String(),
PlanOnly: false,
ExpectError: regexp.MustCompile(`does not exist|unable to open|required at apply`),
}

resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: testProviders(t),
Steps: []resource.TestStep{step},
})
})
}
}

// TestResourceFileTransportInvalidAttributes ensures that we can gracefully
// handle invalid attributes in the transport configuration. Since it's a dynamic
// pseudo type we cannot rely on Terraform's built-in validation.
Expand Down
3 changes: 2 additions & 1 deletion internal/plugin/resource_local_kind_cluster_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package plugin

import (
"bytes"
"context"
"crypto/rand"
"math/big"
"os"
Expand Down Expand Up @@ -118,7 +119,7 @@ func TestClusterBuild(t *testing.T) {
t.Skip("Skipping test 'TestClusterBuild', because 'TF_ACC' not set")
}

checkDocker := exec.Command("docker", "ps")
checkDocker := exec.CommandContext(context.Background(), "docker", "ps")
err := checkDocker.Run()
if err != nil {
t.Skip("Skipping test 'TestClusterBuild' since docker daemon not available")
Expand Down
7 changes: 4 additions & 3 deletions internal/transport/ssh/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ func (c *client) Connect(ctx context.Context) error {

var err error

sshAgentConn, sshAgent, ok := c.connectSSHAgent()
sshAgentConn, sshAgent, ok := c.connectSSHAgent(ctx)
if ok {
c.clientConfig.Auth = append(c.clientConfig.Auth, sshAgent)
c.agentConn = sshAgentConn
Expand Down Expand Up @@ -418,10 +418,11 @@ func (c *client) newSession(ctx context.Context) (*xssh.Session, func() error, e
}
}

func (c *client) connectSSHAgent() (net.Conn, xssh.AuthMethod, bool) {
func (c *client) connectSSHAgent(ctx context.Context) (net.Conn, xssh.AuthMethod, bool) {
var auth xssh.AuthMethod

sshAgent, err := net.Dial("unix", os.Getenv("SSH_AUTH_SOCK"))
dialer := &net.Dialer{}
sshAgent, err := dialer.DialContext(ctx, "unix", os.Getenv("SSH_AUTH_SOCK"))
if err != nil {
return sshAgent, auth, false
}
Expand Down
Loading