Merge pull request #18773 from hashicorp/b-working-directories

backend/remote: take working directories into account
This commit is contained in:
Sander van Harmelen 2018-09-06 19:00:01 +02:00 committed by GitHub
commit 7cfeffe36b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 210 additions and 48 deletions

View File

@ -9,21 +9,45 @@ import (
"io"
"io/ioutil"
"math/rand"
"os"
"path/filepath"
tfe "github.com/hashicorp/go-tfe"
)
type mockConfigurationVersions struct {
configVersions map[string]*tfe.ConfigurationVersion
uploadURLs map[string]*tfe.ConfigurationVersion
workspaces map[string]*tfe.ConfigurationVersion
type mockClient struct {
ConfigurationVersions *mockConfigurationVersions
Organizations *mockOrganizations
Plans *mockPlans
Runs *mockRuns
StateVersions *mockStateVersions
Workspaces *mockWorkspaces
}
func newMockConfigurationVersions() *mockConfigurationVersions {
func newMockClient() *mockClient {
c := &mockClient{}
c.ConfigurationVersions = newMockConfigurationVersions(c)
c.Organizations = newMockOrganizations(c)
c.Plans = newMockPlans(c)
c.Runs = newMockRuns(c)
c.StateVersions = newMockStateVersions(c)
c.Workspaces = newMockWorkspaces(c)
return c
}
type mockConfigurationVersions struct {
client *mockClient
configVersions map[string]*tfe.ConfigurationVersion
uploadPaths map[string]string
uploadURLs map[string]*tfe.ConfigurationVersion
}
func newMockConfigurationVersions(client *mockClient) *mockConfigurationVersions {
return &mockConfigurationVersions{
client: client,
configVersions: make(map[string]*tfe.ConfigurationVersion),
uploadPaths: make(map[string]string),
uploadURLs: make(map[string]*tfe.ConfigurationVersion),
workspaces: make(map[string]*tfe.ConfigurationVersion),
}
}
@ -47,7 +71,6 @@ func (m *mockConfigurationVersions) Create(ctx context.Context, workspaceID stri
m.configVersions[cv.ID] = cv
m.uploadURLs[url] = cv
m.workspaces[workspaceID] = cv
return cv, nil
}
@ -65,16 +88,19 @@ func (m *mockConfigurationVersions) Upload(ctx context.Context, url, path string
if !ok {
return errors.New("404 not found")
}
m.uploadPaths[cv.ID] = path
cv.Status = tfe.ConfigurationUploaded
return nil
}
type mockOrganizations struct {
client *mockClient
organizations map[string]*tfe.Organization
}
func newMockOrganizations() *mockOrganizations {
func newMockOrganizations(client *mockClient) *mockOrganizations {
return &mockOrganizations{
client: client,
organizations: make(map[string]*tfe.Organization),
}
}
@ -117,32 +143,53 @@ func (m *mockOrganizations) Delete(ctx context.Context, name string) error {
}
type mockPlans struct {
logs map[string]string
plans map[string]*tfe.Plan
client *mockClient
logs map[string]string
plans map[string]*tfe.Plan
}
func newMockPlans() *mockPlans {
func newMockPlans(client *mockClient) *mockPlans {
return &mockPlans{
logs: make(map[string]string),
plans: make(map[string]*tfe.Plan),
client: client,
logs: make(map[string]string),
plans: make(map[string]*tfe.Plan),
}
}
// create is a helper function to create a mock plan that uses the configured
// working directory to find the logfile. This enables us to test if we are
// using the
func (m *mockPlans) create(cvID, workspaceID string) (*tfe.Plan, error) {
id := generateID("plan-")
url := fmt.Sprintf("https://app.terraform.io/_archivist/%s", id)
p := &tfe.Plan{
ID: id,
LogReadURL: url,
Status: tfe.PlanPending,
}
w, ok := m.client.Workspaces.workspaceIDs[workspaceID]
if !ok {
return nil, tfe.ErrResourceNotFound
}
m.logs[url] = filepath.Join(
m.client.ConfigurationVersions.uploadPaths[cvID],
w.WorkingDirectory,
"output.log",
)
m.plans[p.ID] = p
return p, nil
}
func (m *mockPlans) Read(ctx context.Context, planID string) (*tfe.Plan, error) {
p, ok := m.plans[planID]
if !ok {
url := fmt.Sprintf("https://app.terraform.io/_archivist/%s", planID)
p = &tfe.Plan{
ID: planID,
LogReadURL: url,
Status: tfe.PlanFinished,
}
m.logs[url] = "plan/output.log"
m.plans[p.ID] = p
return nil, tfe.ErrResourceNotFound
}
p.Status = tfe.PlanFinished
return p, nil
}
@ -157,7 +204,11 @@ func (m *mockPlans) Logs(ctx context.Context, planID string) (io.Reader, error)
return nil, tfe.ErrResourceNotFound
}
logs, err := ioutil.ReadFile("./test-fixtures/" + logfile)
if _, err := os.Stat(logfile); os.IsNotExist(err) {
return bytes.NewBufferString("logfile does not exist"), nil
}
logs, err := ioutil.ReadFile(logfile)
if err != nil {
return nil, err
}
@ -166,34 +217,39 @@ func (m *mockPlans) Logs(ctx context.Context, planID string) (io.Reader, error)
}
type mockRuns struct {
client *mockClient
runs map[string]*tfe.Run
workspaces map[string][]*tfe.Run
}
func newMockRuns() *mockRuns {
func newMockRuns(client *mockClient) *mockRuns {
return &mockRuns{
client: client,
runs: make(map[string]*tfe.Run),
workspaces: make(map[string][]*tfe.Run),
}
}
func (m *mockRuns) List(ctx context.Context, workspaceID string, options tfe.RunListOptions) ([]*tfe.Run, error) {
w, ok := m.client.Workspaces.workspaceIDs[workspaceID]
if !ok {
return nil, tfe.ErrResourceNotFound
}
var rs []*tfe.Run
for _, r := range m.workspaces[workspaceID] {
for _, r := range m.workspaces[w.ID] {
rs = append(rs, r)
}
return rs, nil
}
func (m *mockRuns) Create(ctx context.Context, options tfe.RunCreateOptions) (*tfe.Run, error) {
id := generateID("run-")
p := &tfe.Plan{
ID: generateID("plan-"),
Status: tfe.PlanPending,
p, err := m.client.Plans.create(options.ConfigurationVersion.ID, options.Workspace.ID)
if err != nil {
return nil, err
}
r := &tfe.Run{
ID: id,
ID: generateID("run-"),
Plan: p,
Status: tfe.RunPending,
}
@ -225,13 +281,15 @@ func (m *mockRuns) Discard(ctx context.Context, runID string, options tfe.RunDis
}
type mockStateVersions struct {
client *mockClient
states map[string][]byte
stateVersions map[string]*tfe.StateVersion
workspaces map[string][]string
}
func newMockStateVersions() *mockStateVersions {
func newMockStateVersions(client *mockClient) *mockStateVersions {
return &mockStateVersions{
client: client,
states: make(map[string][]byte),
stateVersions: make(map[string]*tfe.StateVersion),
workspaces: make(map[string][]string),
@ -277,14 +335,21 @@ func (m *mockStateVersions) Read(ctx context.Context, svID string) (*tfe.StateVe
}
func (m *mockStateVersions) Current(ctx context.Context, workspaceID string) (*tfe.StateVersion, error) {
svs, ok := m.workspaces[workspaceID]
w, ok := m.client.Workspaces.workspaceIDs[workspaceID]
if !ok {
return nil, tfe.ErrResourceNotFound
}
svs, ok := m.workspaces[w.ID]
if !ok || len(svs) == 0 {
return nil, tfe.ErrResourceNotFound
}
sv, ok := m.stateVersions[svs[len(svs)-1]]
if !ok {
return nil, tfe.ErrResourceNotFound
}
return sv, nil
}
@ -297,12 +362,14 @@ func (m *mockStateVersions) Download(ctx context.Context, url string) ([]byte, e
}
type mockWorkspaces struct {
client *mockClient
workspaceIDs map[string]*tfe.Workspace
workspaceNames map[string]*tfe.Workspace
}
func newMockWorkspaces() *mockWorkspaces {
func newMockWorkspaces(client *mockClient) *mockWorkspaces {
return &mockWorkspaces{
client: client,
workspaceIDs: make(map[string]*tfe.Workspace),
workspaceNames: make(map[string]*tfe.Workspace),
}
@ -317,9 +384,8 @@ func (m *mockWorkspaces) List(ctx context.Context, organization string, options
}
func (m *mockWorkspaces) Create(ctx context.Context, organization string, options tfe.WorkspaceCreateOptions) (*tfe.Workspace, error) {
id := generateID("ws-")
w := &tfe.Workspace{
ID: id,
ID: generateID("ws-"),
Name: *options.Name,
}
m.workspaceIDs[w.ID] = w
@ -340,8 +406,16 @@ func (m *mockWorkspaces) Update(ctx context.Context, organization, workspace str
if !ok {
return nil, tfe.ErrResourceNotFound
}
w.Name = *options.Name
w.TerraformVersion = *options.TerraformVersion
if options.Name != nil {
w.Name = *options.Name
}
if options.TerraformVersion != nil {
w.TerraformVersion = *options.TerraformVersion
}
if options.WorkingDirectory != nil {
w.WorkingDirectory = *options.WorkingDirectory
}
delete(m.workspaceNames, workspace)
m.workspaceNames[w.Name] = w

View File

@ -8,6 +8,7 @@ import (
"io/ioutil"
"log"
"os"
"path/filepath"
"strings"
"time"
@ -64,15 +65,32 @@ func (b *Remote) opPlan(stopCtx, cancelCtx context.Context, op *backend.Operatio
var configDir string
if op.Module != nil && op.Module.Config().Dir != "" {
configDir = op.Module.Config().Dir
// Make sure to take the working directory into account by removing
// the working directory from the current path. This will result in
// a path that points to the expected root of the workspace.
configDir = filepath.Clean(strings.TrimSuffix(
filepath.Clean(op.Module.Config().Dir),
filepath.Clean(w.WorkingDirectory),
))
} else {
// We did a check earlier to make sure we either have a config dir,
// or the plan is run with -destroy. So this else clause will only
// be executed when we are destroying and doesn't need the config.
configDir, err = ioutil.TempDir("", "tf")
if err != nil {
runningOp.Err = fmt.Errorf(strings.TrimSpace(fmt.Sprintf(
generalErr, "error creating temp directory", err)))
generalErr, "error creating temporary directory", err)))
return
}
defer os.RemoveAll(configDir)
// Make sure the configured working directory exists.
err = os.MkdirAll(filepath.Join(configDir, w.WorkingDirectory), 0700)
if err != nil {
runningOp.Err = fmt.Errorf(strings.TrimSpace(fmt.Sprintf(
generalErr, "error creating temporary working directory", err)))
return
}
}
err = b.client.ConfigurationVersions.Upload(stopCtx, cv.UploadURL, configDir)

View File

@ -5,6 +5,7 @@ import (
"strings"
"testing"
tfe "github.com/hashicorp/go-tfe"
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/config/module"
"github.com/hashicorp/terraform/terraform"
@ -179,3 +180,39 @@ func TestRemote_planDestroyNoConfig(t *testing.T) {
t.Fatalf("unexpected plan error: %v", run.Err)
}
}
func TestRemote_planWithWorkingDirectory(t *testing.T) {
b := testBackendDefault(t)
options := tfe.WorkspaceUpdateOptions{
WorkingDirectory: tfe.String("terraform"),
}
// Configure the workspace to use a custom working direcrtory.
_, err := b.client.Workspaces.Update(context.Background(), b.organization, b.workspace, options)
if err != nil {
t.Fatalf("error configuring working directory: %v", err)
}
mod, modCleanup := module.TestTree(t, "./test-fixtures/plan-with-working-directory/terraform")
defer modCleanup()
op := testOperationPlan()
op.Module = mod
op.Workspace = backend.DefaultStateName
run, err := b.Operation(context.Background(), op)
if err != nil {
t.Fatalf("error starting operation: %v", err)
}
<-run.Done()
if run.Err != nil {
t.Fatalf("error running operation: %v", run.Err)
}
output := b.CLI.(*cli.MockUi).OutputWriter.String()
if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
t.Fatalf("missing plan summery in output: %s", output)
}
}

View File

@ -0,0 +1 @@
resource "null_resource" "foo" {}

View File

@ -0,0 +1,29 @@
Running plan in the remote backend. Output will stream here. Pressing Ctrl-C
will stop streaming the logs, but will not stop the plan running remotely.
To view this plan in a browser, visit:
https://atlas.local/app/demo1/my-app-web/runs/run-cPK6EnfTpqwy6ucU
Waiting for the plan to start...
Terraform v0.11.7
Configuring remote state backend...
Initializing Terraform configuration...
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.
------------------------------------------------------------------------
An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
+ null_resource.foo
id: <computed>
Plan: 1 to add, 0 to change, 0 to destroy.

View File

@ -69,14 +69,17 @@ func testBackend(t *testing.T, c map[string]interface{}) *Remote {
// Configure the backend so the client is created.
backend.TestBackendConfig(t, b, c)
// Once the client exists, mock the services we use..
// Get a new mock client.
mc := newMockClient()
// Replace the services we use with our mock services.
b.CLI = cli.NewMockUi()
b.client.ConfigurationVersions = newMockConfigurationVersions()
b.client.Organizations = newMockOrganizations()
b.client.Plans = newMockPlans()
b.client.Runs = newMockRuns()
b.client.StateVersions = newMockStateVersions()
b.client.Workspaces = newMockWorkspaces()
b.client.ConfigurationVersions = mc.ConfigurationVersions
b.client.Organizations = mc.Organizations
b.client.Plans = mc.Plans
b.client.Runs = mc.Runs
b.client.StateVersions = mc.StateVersions
b.client.Workspaces = mc.Workspaces
ctx := context.Background()