Repository: crossplane-contrib/provider-terraform (go)

Issue:
Workspaces with no resources run terraform apply on every reconcile
### What happened?
If a Workspace has no resources, only outputs for example, the current Observe logic returns ResourceExists: false every reconcile because there are 0 resources returned from terraform state list.

If the Workspace has outputs and terraform plan has returned no delta then the ResourceExists attribute can be set to true


### How can we reproduce it?
Create a Workspace that has no resources, only outputs, and observe in the debug logs that the Workspace is constantly calling the Update/Create method on every loop

### What environment did it happen in?
Crossplane version: 1.8.1



Pull request description:
Include outputs in test for ResourceExists  (#71)
Signed-off-by: Bob Haddleton <bob.haddleton@nokia.com>

### Description of your changes
Include outputs in the condition that determines whether the resource exists.

This will handle "calculation only" workspaces that do not create anything but still have outputs.

Fixes #71 

I have:

- [X] Read and followed Crossplane's [contribution process].
- [X] Run `make reviewable test` to ensure this PR is ready for review.

### How has this code been tested
Verified in our testing environment with Workspaces that were previously being "recreated" on every reconciliation

[contribution process]: https://git.io/fj2m9


Patch:
diff --git a/internal/controller/workspace/workspace.go b/internal/controller/workspace/workspace.go
index b86bdff..469041c 100644
--- a/internal/controller/workspace/workspace.go
+++ b/internal/controller/workspace/workspace.go
@@ -286,7 +286,7 @@ func (c *external) Observe(ctx context.Context, mg resource.Managed) (managed.Ex
 	cr.Status.AtProvider = generateWorkspaceObservation(op)
 
 	return managed.ExternalObservation{
-		ResourceExists:          len(r) > 0,
+		ResourceExists:          len(r)+len(op) > 0,
 		ResourceUpToDate:        !differs,
 		ResourceLateInitialized: false,
 		ConnectionDetails:       op2cd(op),


Test patch:
diff --git a/internal/controller/workspace/workspace_test.go b/internal/controller/workspace/workspace_test.go
index 9788046..f321dff 100644
--- a/internal/controller/workspace/workspace_test.go
+++ b/internal/controller/workspace/workspace_test.go
@@ -759,6 +759,47 @@ func TestObserve(t *testing.T) {
 				},
 			},
 		},
+		"WorkspaceExistsOnlyOutputs": {
+			reason: "A workspace with only outputs and no resources should set ResourceExists to true",
+			fields: fields{
+				tf: &MockTf{
+					MockDiff: func(ctx context.Context, o ...terraform.Option) (bool, error) { return false, nil },
+					MockResources: func(ctx context.Context) ([]string, error) {
+						return nil, nil
+					},
+					MockOutputs: func(ctx context.Context) ([]terraform.Output, error) {
+						return []terraform.Output{
+							{Name: "string", Type: terraform.OutputTypeString, Sensitive: false},
+							{Name: "object", Type: terraform.OutputTypeObject, Sensitive: true},
+						}, nil
+					},
+				},
+			},
+			args: args{
+				mg: &v1alpha1.Workspace{
+					Spec: v1alpha1.WorkspaceSpec{
+						ForProvider: v1alpha1.WorkspaceParameters{
+							PlanArgs: []string{"-refresh=false"},
+						},
+					},
+				},
+			},
+			want: want{
+				o: managed.ExternalObservation{
+					ResourceExists:   true,
+					ResourceUpToDate: true,
+					ConnectionDetails: managed.ConnectionDetails{
+						"string": {},
+						"object": []byte("null"), // Because we JSON decode the the value, which is interface{}{}
+					},
+				},
+				wo: v1alpha1.WorkspaceObservation{
+					Outputs: map[string]string{
+						"string": "",
+					},
+				},
+			},
+		},
 	}
 
 	for name, tc := range cases {