backend/remote: extend mocks and add sentinel tests

This commit is contained in:
Sander van Harmelen 2018-09-26 21:35:26 +02:00
parent 2bd1040bbd
commit b28f47055d
18 changed files with 430 additions and 31 deletions

View File

@ -461,6 +461,9 @@ func (b *Remote) cancel(cancelCtx context.Context, r *tfe.Run) error {
if err != nil {
return generalError("error cancelling run", err)
}
if b.CLI != nil {
b.CLI.Output(b.Colorize().Color(strings.TrimSpace(cancelPendingOperation)))
}
}
return nil
@ -511,6 +514,10 @@ connection problem, in which case you could retry the command. If the issue
persists please open a support ticket to get help resolving the problem.
`
const cancelPendingOperation = `[reset][red]
Pending remote operation cancelled.[reset]
`
var schemaDescriptions = map[string]string{
"hostname": "The remote backend hostname to connect to (defaults to app.terraform.io).",
"organization": "The name of the organization containing the targeted workspace(s).",

View File

@ -91,7 +91,7 @@ func TestRemote_applyWithVCS(t *testing.T) {
<-run.Done()
if run.Err == nil {
t.Fatalf("expected a apply error, got: %v", run.Err)
t.Fatalf("expected an apply error, got: %v", run.Err)
}
if !strings.Contains(run.Err.Error(), "not allowed for workspaces with a VCS") {
t.Fatalf("expected a VCS error, got: %v", run.Err)
@ -116,7 +116,7 @@ func TestRemote_applyWithPlan(t *testing.T) {
<-run.Done()
if run.Err == nil {
t.Fatalf("expected a apply error, got: %v", run.Err)
t.Fatalf("expected an apply error, got: %v", run.Err)
}
if !strings.Contains(run.Err.Error(), "saved plan is currently not supported") {
t.Fatalf("expected a saved plan error, got: %v", run.Err)
@ -141,7 +141,7 @@ func TestRemote_applyWithTarget(t *testing.T) {
<-run.Done()
if run.Err == nil {
t.Fatalf("expected a apply error, got: %v", run.Err)
t.Fatalf("expected an apply error, got: %v", run.Err)
}
if !strings.Contains(run.Err.Error(), "targeting is currently not supported") {
t.Fatalf("expected a targeting error, got: %v", run.Err)
@ -162,7 +162,7 @@ func TestRemote_applyNoConfig(t *testing.T) {
<-run.Done()
if run.Err == nil {
t.Fatalf("expected a apply error, got: %v", run.Err)
t.Fatalf("expected an apply error, got: %v", run.Err)
}
if !strings.Contains(run.Err.Error(), "configuration files found") {
t.Fatalf("expected configuration files error, got: %v", run.Err)
@ -218,10 +218,10 @@ func TestRemote_applyNoApprove(t *testing.T) {
<-run.Done()
if run.Err == nil {
t.Fatalf("expected a apply error, got: %v", run.Err)
t.Fatalf("expected an apply error, got: %v", run.Err)
}
if !strings.Contains(run.Err.Error(), "Apply discarded") {
t.Fatalf("expected a apply discarded error, got: %v", run.Err)
t.Fatalf("expected an apply discarded error, got: %v", run.Err)
}
if len(input.answers) > 0 {
t.Fatalf("expected no unused answers, got: %v", input.answers)
@ -406,3 +406,175 @@ func TestRemote_applyDestroyNoConfig(t *testing.T) {
t.Fatalf("expected no unused answers, got: %v", input.answers)
}
}
func TestRemote_applyPolicyPass(t *testing.T) {
b := testBackendDefault(t)
mod, modCleanup := module.TestTree(t, "./test-fixtures/apply-policy-passed")
defer modCleanup()
input := testInput(t, map[string]string{
"approve": "yes",
})
op := testOperationApply()
op.Module = mod
op.UIIn = input
op.UIOut = b.CLI
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)
}
if len(input.answers) > 0 {
t.Fatalf("expected no unused answers, got: %v", input.answers)
}
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)
}
if !strings.Contains(output, "Sentinel Result: true") {
t.Fatalf("missing Sentinel result in output: %s", output)
}
if !strings.Contains(output, "1 added, 0 changed, 0 destroyed") {
t.Fatalf("missing apply summery in output: %s", output)
}
}
func TestRemote_applyPolicyHardFail(t *testing.T) {
b := testBackendDefault(t)
mod, modCleanup := module.TestTree(t, "./test-fixtures/apply-policy-hard-failed")
defer modCleanup()
input := testInput(t, map[string]string{
"approve": "yes",
})
op := testOperationApply()
op.Module = mod
op.UIIn = input
op.UIOut = b.CLI
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("expected an apply error, got: %v", run.Err)
}
if !strings.Contains(run.Err.Error(), "hard failed") {
t.Fatalf("expected a policy check error, got: %v", run.Err)
}
if len(input.answers) != 1 {
t.Fatalf("expected an unused answers, got: %v", input.answers)
}
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)
}
if !strings.Contains(output, "Sentinel Result: false") {
t.Fatalf("missing Sentinel result in output: %s", output)
}
if strings.Contains(output, "1 added, 0 changed, 0 destroyed") {
t.Fatalf("unexpected apply summery in output: %s", output)
}
}
func TestRemote_applyPolicySoftFail(t *testing.T) {
b := testBackendDefault(t)
mod, modCleanup := module.TestTree(t, "./test-fixtures/apply-policy-soft-failed")
defer modCleanup()
input := testInput(t, map[string]string{
"override": "override",
"approve": "yes",
})
op := testOperationApply()
op.Module = mod
op.UIIn = input
op.UIOut = b.CLI
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)
}
if len(input.answers) > 0 {
t.Fatalf("expected no unused answers, got: %v", input.answers)
}
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)
}
if !strings.Contains(output, "Sentinel Result: false") {
t.Fatalf("missing Sentinel result in output: %s", output)
}
if !strings.Contains(output, "1 added, 0 changed, 0 destroyed") {
t.Fatalf("missing apply summery in output: %s", output)
}
}
func TestRemote_applyPolicySoftFailAutoApprove(t *testing.T) {
b := testBackendDefault(t)
mod, modCleanup := module.TestTree(t, "./test-fixtures/apply-policy-soft-failed")
defer modCleanup()
input := testInput(t, map[string]string{
"override": "override",
})
op := testOperationApply()
op.AutoApprove = true
op.Module = mod
op.UIIn = input
op.UIOut = b.CLI
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)
}
if len(input.answers) > 0 {
t.Fatalf("expected no unused answers, got: %v", input.answers)
}
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)
}
if !strings.Contains(output, "Sentinel Result: false") {
t.Fatalf("missing Sentinel result in output: %s", output)
}
if !strings.Contains(output, "1 added, 0 changed, 0 destroyed") {
t.Fatalf("missing apply summery in output: %s", output)
}
}

View File

@ -23,6 +23,7 @@ type mockClient struct {
ConfigurationVersions *mockConfigurationVersions
Organizations *mockOrganizations
Plans *mockPlans
PolicyChecks *mockPolicyChecks
Runs *mockRuns
StateVersions *mockStateVersions
Workspaces *mockWorkspaces
@ -34,6 +35,7 @@ func newMockClient() *mockClient {
c.ConfigurationVersions = newMockConfigurationVersions(c)
c.Organizations = newMockOrganizations(c)
c.Plans = newMockPlans(c)
c.PolicyChecks = newMockPolicyChecks(c)
c.Runs = newMockRuns(c)
c.StateVersions = newMockStateVersions(c)
c.Workspaces = newMockWorkspaces(c)
@ -55,8 +57,7 @@ func newMockApplies(client *mockClient) *mockApplies {
}
// create is a helper function to create a mock apply that uses the configured
// working directory to find the logfile. This enables us to test if we are
// using the
// working directory to find the logfile.
func (m *mockApplies) create(cvID, workspaceID string) (*tfe.Apply, error) {
c, ok := m.client.ConfigurationVersions.configVersions[cvID]
if !ok {
@ -320,8 +321,7 @@ func newMockPlans(client *mockClient) *mockPlans {
}
// 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
// working directory to find the logfile.
func (m *mockPlans) create(cvID, workspaceID string) (*tfe.Plan, error) {
id := generateID("plan-")
url := fmt.Sprintf("https://app.terraform.io/_archivist/%s", id)
@ -396,6 +396,133 @@ func (m *mockPlans) Logs(ctx context.Context, planID string) (io.Reader, error)
}, nil
}
type mockPolicyChecks struct {
client *mockClient
checks map[string]*tfe.PolicyCheck
logs map[string]string
}
func newMockPolicyChecks(client *mockClient) *mockPolicyChecks {
return &mockPolicyChecks{
client: client,
checks: make(map[string]*tfe.PolicyCheck),
logs: make(map[string]string),
}
}
// create is a helper function to create a mock policy check that uses the
// configured working directory to find the logfile.
func (m *mockPolicyChecks) create(cvID, workspaceID string) (*tfe.PolicyCheck, error) {
id := generateID("pc-")
pc := &tfe.PolicyCheck{
ID: id,
Actions: &tfe.PolicyActions{},
Permissions: &tfe.PolicyPermissions{},
Scope: tfe.PolicyScopeOrganization,
Status: tfe.PolicyPending,
}
w, ok := m.client.Workspaces.workspaceIDs[workspaceID]
if !ok {
return nil, tfe.ErrResourceNotFound
}
logfile := filepath.Join(
m.client.ConfigurationVersions.uploadPaths[cvID],
w.WorkingDirectory,
"policy.log",
)
if _, err := os.Stat(logfile); os.IsNotExist(err) {
return nil, nil
}
m.logs[pc.ID] = logfile
m.checks[pc.ID] = pc
return pc, nil
}
func (m *mockPolicyChecks) List(ctx context.Context, runID string, options tfe.PolicyCheckListOptions) (*tfe.PolicyCheckList, error) {
_, ok := m.client.Runs.runs[runID]
if !ok {
return nil, tfe.ErrResourceNotFound
}
pcl := &tfe.PolicyCheckList{}
for _, pc := range m.checks {
pcl.Items = append(pcl.Items, pc)
}
pcl.Pagination = &tfe.Pagination{
CurrentPage: 1,
NextPage: 1,
PreviousPage: 1,
TotalPages: 1,
TotalCount: len(pcl.Items),
}
return pcl, nil
}
func (m *mockPolicyChecks) Read(ctx context.Context, policyCheckID string) (*tfe.PolicyCheck, error) {
pc, ok := m.checks[policyCheckID]
if !ok {
return nil, tfe.ErrResourceNotFound
}
return pc, nil
}
func (m *mockPolicyChecks) Override(ctx context.Context, policyCheckID string) (*tfe.PolicyCheck, error) {
pc, ok := m.checks[policyCheckID]
if !ok {
return nil, tfe.ErrResourceNotFound
}
pc.Status = tfe.PolicyOverridden
return pc, nil
}
func (m *mockPolicyChecks) Logs(ctx context.Context, policyCheckID string) (io.Reader, error) {
pc, ok := m.checks[policyCheckID]
if !ok {
return nil, tfe.ErrResourceNotFound
}
logfile, ok := m.logs[pc.ID]
if !ok {
return nil, tfe.ErrResourceNotFound
}
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
}
switch {
case bytes.Contains(logs, []byte("Sentinel Result: true")):
pc.Status = tfe.PolicyPasses
case bytes.Contains(logs, []byte("Sentinel Result: false")):
switch {
case bytes.Contains(logs, []byte("hard-mandatory")):
pc.Status = tfe.PolicyHardFailed
case bytes.Contains(logs, []byte("soft-mandatory")):
pc.Actions.IsOverridable = true
pc.Permissions.CanOverride = true
pc.Status = tfe.PolicySoftFailed
}
default:
// As this is an unexpected state, we say the policy errored.
pc.Status = tfe.PolicyErrored
}
return bytes.NewBuffer(logs), nil
}
type mockRuns struct {
client *mockClient
runs map[string]*tfe.Run
@ -443,6 +570,11 @@ func (m *mockRuns) Create(ctx context.Context, options tfe.RunCreateOptions) (*t
return nil, err
}
pc, err := m.client.PolicyChecks.create(options.ConfigurationVersion.ID, options.Workspace.ID)
if err != nil {
return nil, err
}
r := &tfe.Run{
ID: generateID("run-"),
Actions: &tfe.RunActions{},
@ -453,6 +585,10 @@ func (m *mockRuns) Create(ctx context.Context, options tfe.RunCreateOptions) (*t
Status: tfe.RunPending,
}
if pc != nil {
r.PolicyChecks = []*tfe.PolicyCheck{pc}
}
if options.IsDestroy != nil {
r.IsDestroy = *options.IsDestroy
}

View File

@ -144,7 +144,12 @@ func (b *Remote) plan(stopCtx, cancelCtx context.Context, op *backend.Operation,
if b.CLI != nil {
b.CLI.Output(b.Colorize().Color(strings.TrimSpace(lockTimeoutErr)))
}
syscall.Kill(syscall.Getpid(), syscall.SIGINT)
p, err := os.FindProcess(os.Getpid())
if err != nil {
log.Printf("[ERROR] error searching process ID: %v", err)
return
}
p.Signal(syscall.SIGINT)
}
}
}()

View File

@ -1,11 +1,3 @@
------------------------------------------------------------------------
Do you really want to destroy all resources in workspace "my-app-dev"?
Terraform will destroy all your managed infrastructure, as shown above.
There is no undo. Only 'yes' will be accepted to confirm.
Enter a value: yes
null_resource.hello: Destroying... (ID: 8657651096157629581)
null_resource.hello: Destruction complete after 0s

View File

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

View File

@ -0,0 +1,21 @@
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

@ -0,0 +1,12 @@
Sentinel Result: false
Sentinel evaluated to false because one or more Sentinel policies evaluated
to false. This false was not due to an undefined value or runtime error.
1 policies evaluated.
## Policy 1: Passthrough.sentinel (hard-mandatory)
Result: false
FALSE - Passthrough.sentinel:1:1 - Rule "main"

View File

@ -0,0 +1,4 @@
null_resource.hello: Creating...
null_resource.hello: Creation complete after 0s (ID: 8657651096157629581)
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

View File

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

View File

@ -0,0 +1,21 @@
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

@ -1,7 +1,3 @@
------------------------------------------------------------------------
Organization policy check:
Sentinel Result: true
This result means that Sentinel policies returned true and the protected

View File

@ -0,0 +1,4 @@
null_resource.hello: Creating...
null_resource.hello: Creation complete after 0s (ID: 8657651096157629581)
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

View File

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

View File

@ -0,0 +1,21 @@
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

@ -0,0 +1,12 @@
Sentinel Result: false
Sentinel evaluated to false because one or more Sentinel policies evaluated
to false. This false was not due to an undefined value or runtime error.
1 policies evaluated.
## Policy 1: Passthrough.sentinel (soft-mandatory)
Result: false
FALSE - Passthrough.sentinel:1:1 - Rule "main"

View File

@ -1,11 +1,3 @@
------------------------------------------------------------------------
Do you want to perform these actions in workspace "my-workspace-name"?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value: yes
null_resource.hello: Creating...
null_resource.hello: Creation complete after 0s (ID: 8657651096157629581)

View File

@ -82,6 +82,7 @@ func testBackend(t *testing.T, c map[string]interface{}) *Remote {
b.client.ConfigurationVersions = mc.ConfigurationVersions
b.client.Organizations = mc.Organizations
b.client.Plans = mc.Plans
b.client.PolicyChecks = mc.PolicyChecks
b.client.Runs = mc.Runs
b.client.StateVersions = mc.StateVersions
b.client.Workspaces = mc.Workspaces