Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Terraform Plan Output in Status Field #226

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
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
10 changes: 9 additions & 1 deletion apis/v1beta1/workspace_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,13 @@ type WorkspaceParameters struct {
// +optional
Entrypoint string `json:"entrypoint"`

// Environment variables.
// Include the output of terraform plan in the status.
// The plan will be gzipped and base64 encoded.
// +kubebuilder:default=false
// +optional
IncludePlan *bool `json:"includePlan"`

// Environment variables.
// +optional
Env []EnvVar `json:"env,omitempty"`

Expand Down Expand Up @@ -151,6 +157,8 @@ type WorkspaceParameters struct {

// WorkspaceObservation are the observable fields of a Workspace.
type WorkspaceObservation struct {
// +optional
Plan *string `json:"tfPlan,omitempty"`
Checksum string `json:"checksum,omitempty"`
Outputs map[string]extensionsV1.JSON `json:"outputs,omitempty"`
}
Expand Down
9 changes: 9 additions & 0 deletions apis/v1beta1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion examples/workspace-inline.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ spec:
# For simple cases you can use an inline source to specify the content of
# main.tf as opaque, inline HCL.
source: Inline
showPlan: true
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should be includePlan ?

module: |
// Outputs are written to the connection secret.
output "url" {
Expand All @@ -29,7 +30,7 @@ spec:
name = "crossplane-example-${terraform.workspace}-${random_id.example.hex}"
location = "US"
force_destroy = true

public_access_prevention = "enforced"
}
writeConnectionSecretToRef:
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -112,4 +112,4 @@ require (
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
sigs.k8s.io/yaml v1.4.0 // indirect
)
)
275 changes: 142 additions & 133 deletions go.sum

Large diffs are not rendered by default.

21 changes: 14 additions & 7 deletions internal/controller/workspace/workspace.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import (
corev1 "k8s.io/api/core/v1"
extensionsV1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/utils/ptr"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"

Expand Down Expand Up @@ -104,7 +105,7 @@ type tfclient interface {
Workspace(ctx context.Context, name string) error
Outputs(ctx context.Context) ([]terraform.Output, error)
Resources(ctx context.Context) ([]string, error)
Diff(ctx context.Context, o ...terraform.Option) (bool, error)
Diff(ctx context.Context, o ...terraform.Option) (bool, string, error)
Apply(ctx context.Context, o ...terraform.Option) error
Destroy(ctx context.Context, o ...terraform.Option) error
DeleteCurrentWorkspace(ctx context.Context) error
Expand Down Expand Up @@ -350,32 +351,34 @@ type external struct {
logger logging.Logger
}

func (c *external) checkDiff(ctx context.Context, cr *v1beta1.Workspace) (bool, error) {
func (c *external) checkDiff(ctx context.Context, cr *v1beta1.Workspace) (bool, string, error) {
o, err := c.options(ctx, cr.Spec.ForProvider)
if err != nil {
return false, errors.Wrap(err, errOptions)
return false, "", errors.Wrap(err, errOptions)
}

o = append(o, terraform.WithArgs(cr.Spec.ForProvider.PlanArgs))
differs, err := c.tf.Diff(ctx, o...)
differs, planOutput, err := c.tf.Diff(ctx, o...)

if err != nil {
if !meta.WasDeleted(cr) {
return false, errors.Wrap(err, errDiff)
return false, planOutput, errors.Wrap(err, errDiff)
}
// terraform plan can fail on deleted resources, so let the reconciliation loop
// call Delete() if there are still resources in the tfstate file
differs = false
}
return differs, nil
return differs, planOutput, nil
}

//nolint:gocyclo
func (c *external) Observe(ctx context.Context, mg resource.Managed) (managed.ExternalObservation, error) {
cr, ok := mg.(*v1beta1.Workspace)
if !ok {
return managed.ExternalObservation{}, errors.New(errNotWorkspace)
}

differs, err := c.checkDiff(ctx, cr)
differs, planOutput, err := c.checkDiff(ctx, cr)
if err != nil {
return managed.ExternalObservation{}, err
}
Expand All @@ -402,6 +405,10 @@ func (c *external) Observe(ctx context.Context, mg resource.Managed) (managed.Ex
}
cr.Status.AtProvider.Checksum = checksum

if ptr.Deref[bool](cr.Spec.ForProvider.IncludePlan, false) {
cr.Status.AtProvider.Plan = &planOutput
}

if !differs {
// TODO(negz): Allow Workspaces to optionally derive their readiness from an
// output - similar to the logic XRs use to derive readiness from a field of
Expand Down
54 changes: 42 additions & 12 deletions internal/controller/workspace/workspace_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,12 @@ import (
)

const (
tfChecksum = "checksum"
tfChecksum = "checksum"
noDiffInPlan = "No Change in terraform plan"
)

var (
emptyString = ""
)

type ErrFs struct {
Expand Down Expand Up @@ -72,7 +77,7 @@ type MockTf struct {
MockWorkspace func(ctx context.Context, name string) error
MockOutputs func(ctx context.Context) ([]terraform.Output, error)
MockResources func(ctx context.Context) ([]string, error)
MockDiff func(ctx context.Context, o ...terraform.Option) (bool, error)
MockDiff func(ctx context.Context, o ...terraform.Option) (bool, string, error)
MockApply func(ctx context.Context, o ...terraform.Option) error
MockDestroy func(ctx context.Context, o ...terraform.Option) error
MockDeleteCurrentWorkspace func(ctx context.Context) error
Expand All @@ -99,7 +104,7 @@ func (tf *MockTf) Resources(ctx context.Context) ([]string, error) {
return tf.MockResources(ctx)
}

func (tf *MockTf) Diff(ctx context.Context, o ...terraform.Option) (bool, error) {
func (tf *MockTf) Diff(ctx context.Context, o ...terraform.Option) (bool, string, error) {
return tf.MockDiff(ctx, o...)
}

Expand Down Expand Up @@ -599,6 +604,7 @@ func TestConnect(t *testing.T) {
},
Status: v1beta1.WorkspaceStatus{
AtProvider: v1beta1.WorkspaceObservation{
Plan: &emptyString,
Checksum: tfChecksum,
},
},
Expand Down Expand Up @@ -634,6 +640,7 @@ func TestConnect(t *testing.T) {
},
Status: v1beta1.WorkspaceStatus{
AtProvider: v1beta1.WorkspaceObservation{
Plan: &emptyString,
Checksum: tfChecksum,
},
},
Expand Down Expand Up @@ -863,7 +870,9 @@ func TestObserve(t *testing.T) {
reason: "We should return any error encountered while diffing the Terraform configuration",
fields: fields{
tf: &MockTf{
MockDiff: func(ctx context.Context, o ...terraform.Option) (bool, error) { return false, errBoom },
MockDiff: func(ctx context.Context, o ...terraform.Option) (bool, string, error) {
return false, noDiffInPlan, errBoom
},
},
},
args: args{
Expand All @@ -877,7 +886,9 @@ func TestObserve(t *testing.T) {
reason: "We should return ResourceUpToDate true when resource is deleted and there are existing resources but terraform plan fails",
fields: fields{
tf: &MockTf{
MockDiff: func(ctx context.Context, o ...terraform.Option) (bool, error) { return false, errBoom },
MockDiff: func(ctx context.Context, o ...terraform.Option) (bool, string, error) {
return false, emptyString, errBoom
},
MockGenerateChecksum: func(ctx context.Context) (string, error) { return tfChecksum, nil },
MockOutputs: func(ctx context.Context) ([]terraform.Output, error) { return nil, nil },
MockResources: func(ctx context.Context) ([]string, error) {
Expand All @@ -899,6 +910,7 @@ func TestObserve(t *testing.T) {
ConnectionDetails: managed.ConnectionDetails{},
},
wo: v1beta1.WorkspaceObservation{
Plan: nil,
Checksum: tfChecksum,
Outputs: map[string]extensionsV1.JSON{},
},
Expand All @@ -908,7 +920,9 @@ func TestObserve(t *testing.T) {
reason: "We should return ResourceUpToDate true when resource is deleted and there are no existing resources and terraform plan fails",
fields: fields{
tf: &MockTf{
MockDiff: func(ctx context.Context, o ...terraform.Option) (bool, error) { return false, errBoom },
MockDiff: func(ctx context.Context, o ...terraform.Option) (bool, string, error) {
return false, emptyString, errBoom
},
MockGenerateChecksum: func(ctx context.Context) (string, error) { return tfChecksum, nil },
MockOutputs: func(ctx context.Context) ([]terraform.Output, error) { return nil, nil },
MockResources: func(ctx context.Context) ([]string, error) { return nil, nil },
Expand All @@ -929,6 +943,7 @@ func TestObserve(t *testing.T) {
ConnectionDetails: managed.ConnectionDetails{},
},
wo: v1beta1.WorkspaceObservation{
Plan: nil,
Checksum: tfChecksum,
Outputs: map[string]extensionsV1.JSON{},
},
Expand All @@ -938,7 +953,9 @@ func TestObserve(t *testing.T) {
reason: "We should return ResourceUpToDate true when resource is deleted and there are no existing resources and terraform plan fails",
fields: fields{
tf: &MockTf{
MockDiff: func(ctx context.Context, o ...terraform.Option) (bool, error) { return false, errBoom },
MockDiff: func(ctx context.Context, o ...terraform.Option) (bool, string, error) {
return false, noDiffInPlan, errBoom
},
MockResources: func(ctx context.Context) ([]string, error) { return nil, nil },
MockDeleteCurrentWorkspace: func(ctx context.Context) error { return errBoom },
},
Expand All @@ -958,7 +975,9 @@ func TestObserve(t *testing.T) {
reason: "We should return any error encountered while listing extant Terraform resources",
fields: fields{
tf: &MockTf{
MockDiff: func(ctx context.Context, o ...terraform.Option) (bool, error) { return false, nil },
MockDiff: func(ctx context.Context, o ...terraform.Option) (bool, string, error) {
return false, noDiffInPlan, nil
},
MockResources: func(ctx context.Context) ([]string, error) { return nil, errBoom },
},
},
Expand All @@ -973,7 +992,9 @@ func TestObserve(t *testing.T) {
reason: "We should return any error encountered while listing Terraform outputs",
fields: fields{
tf: &MockTf{
MockDiff: func(ctx context.Context, o ...terraform.Option) (bool, error) { return false, nil },
MockDiff: func(ctx context.Context, o ...terraform.Option) (bool, string, error) {
return false, noDiffInPlan, nil
},
MockResources: func(ctx context.Context) ([]string, error) { return nil, nil },
MockOutputs: func(ctx context.Context) ([]terraform.Output, error) { return nil, errBoom },
},
Expand All @@ -989,7 +1010,9 @@ func TestObserve(t *testing.T) {
reason: "A workspace with zero resources should be considered to be non-existent",
fields: fields{
tf: &MockTf{
MockDiff: func(ctx context.Context, o ...terraform.Option) (bool, error) { return false, nil },
MockDiff: func(ctx context.Context, o ...terraform.Option) (bool, string, error) {
return false, emptyString, nil
},
MockGenerateChecksum: func(ctx context.Context) (string, error) { return tfChecksum, nil },
MockResources: func(ctx context.Context) ([]string, error) { return []string{}, nil },
MockOutputs: func(ctx context.Context) ([]terraform.Output, error) { return nil, nil },
Expand All @@ -1005,6 +1028,7 @@ func TestObserve(t *testing.T) {
ConnectionDetails: managed.ConnectionDetails{},
},
wo: v1beta1.WorkspaceObservation{
Plan: nil,
Checksum: tfChecksum,
Outputs: map[string]extensionsV1.JSON{},
},
Expand All @@ -1014,7 +1038,9 @@ func TestObserve(t *testing.T) {
reason: "A workspace with resources should return its outputs as connection details",
fields: fields{
tf: &MockTf{
MockDiff: func(ctx context.Context, o ...terraform.Option) (bool, error) { return false, nil },
MockDiff: func(ctx context.Context, o ...terraform.Option) (bool, string, error) {
return false, emptyString, nil
},
MockGenerateChecksum: func(ctx context.Context) (string, error) { return tfChecksum, nil },
MockResources: func(ctx context.Context) ([]string, error) {
return []string{"cool_resource.very"}, nil
Expand Down Expand Up @@ -1046,6 +1072,7 @@ func TestObserve(t *testing.T) {
},
},
wo: v1beta1.WorkspaceObservation{
Plan: nil,
Checksum: tfChecksum,
Outputs: map[string]extensionsV1.JSON{
"string": {Raw: []byte("null")},
Expand All @@ -1057,7 +1084,9 @@ func TestObserve(t *testing.T) {
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 },
MockDiff: func(ctx context.Context, o ...terraform.Option) (bool, string, error) {
return false, emptyString, nil
},
MockGenerateChecksum: func(ctx context.Context) (string, error) { return tfChecksum, nil },
MockResources: func(ctx context.Context) ([]string, error) {
return nil, nil
Expand Down Expand Up @@ -1090,6 +1119,7 @@ func TestObserve(t *testing.T) {
},
wo: v1beta1.WorkspaceObservation{
Checksum: tfChecksum,
Plan: nil,
Outputs: map[string]extensionsV1.JSON{
"string": {Raw: []byte("null")},
},
Expand Down
Loading