github.com/crossplane/upjet@v1.3.0/pkg/terraform/workspace_test.go (about)

     1  // SPDX-FileCopyrightText: 2023 The Crossplane Authors <https://crossplane.io>
     2  //
     3  // SPDX-License-Identifier: Apache-2.0
     4  
     5  package terraform
     6  
     7  import (
     8  	"context"
     9  	"testing"
    10  	"time"
    11  
    12  	"github.com/crossplane/crossplane-runtime/pkg/test"
    13  	"github.com/google/go-cmp/cmp"
    14  	"github.com/pkg/errors"
    15  	"github.com/spf13/afero"
    16  	k8sExec "k8s.io/utils/exec"
    17  	testingexec "k8s.io/utils/exec/testing"
    18  
    19  	"github.com/crossplane/upjet/pkg/resource/json"
    20  	tferrors "github.com/crossplane/upjet/pkg/terraform/errors"
    21  )
    22  
    23  var (
    24  	testType              = "very-cool-type"
    25  	applyType             = "apply"
    26  	lineage               = "very-cool-lineage"
    27  	terraformVersion      = "1.0.10"
    28  	version               = 1
    29  	serial                = 3
    30  	directory             = "random-dir/"
    31  	changeSummaryAdd      = `{"@level":"info","@message":"Plan: 1 to add, 0 to change, 0 to destroy.","@module":"terraform.ui","@timestamp":"0000-00-00T00:00:00.000000+03:00","changes":{"add":1,"change":0,"remove":0,"operation":"plan"},"type":"change_summary"}`
    32  	changeSummaryUpdate   = `{"@level":"info","@message":"Plan: 0 to add, 1 to change, 0 to destroy.","@module":"terraform.ui","@timestamp":"0000-00-00T00:00:00.000000+03:00","changes":{"add":0,"change":1,"remove":0,"operation":"plan"},"type":"change_summary"}`
    33  	changeSummaryNoAction = `{"@level":"info","@message":"Plan: 0 to add, 0 to change, 0 to destroy.","@module":"terraform.ui","@timestamp":"0000-00-00T00:00:00.000000+03:00","changes":{"add":0,"change":0,"remove":0,"operation":"plan"},"type":"change_summary"}`
    34  	filter                = `{"@level":"info","@message":"Terraform 1.2.1","@module":"terraform.ui","@timestamp":"2022-08-08T14:42:59.377073+03:00","terraform":"1.2.1","type":"version","ui":"1.0"}
    35  {"@level":"error","@message":"Error: error configuring Terraform AWS Provider: error validating provider credentials: error calling sts:GetCallerIdentity: operation error STS: GetCallerIdentity, https response error StatusCode: 403, RequestID: *****, api error InvalidClientTokenId: The security token included in the request is invalid.","@module":"terraform.ui","@timestamp":"2022-08-08T14:43:00.808602+03:00","diagnostic":{"severity":"error","summary":"error configuring Terraform AWS Provider: error validating provider credentials: error calling sts:GetCallerIdentity: operation error STS: GetCallerIdentity, https response error StatusCode: 403, RequestID: *****, api error InvalidClientTokenId: The security token included in the request is invalid.","detail":"","address":"provider[\"registry.terraform.io/hashicorp/aws\"]","range":{"filename":"main.tf.json","start":{"line":1,"column":173,"byte":172},"end":{"line":1,"column":174,"byte":173}},"snippet":{"context":"provider.aws","code":"{\"provider\":{\"aws\":{\"access_key\":\"*****\",\"region\":\"us-east-1\",\"secret_key\":\"/*****\",\"skip_region_validation\":true,\"token\":\"\"}},\"resource\":{\"aws_iam_user\":{\"sample-user\":{\"lifecycle\":{\"prevent_destroy\":true},\"name\":\"sample-user\",\"tags\":{\"crossplane-kind\":\"user.iam.aws.upbound.io\",\"crossplane-name\":\"sample-user\",\"crossplane-providerconfig\":\"default\"}}}},\"terraform\":{\"required_providers\":{\"aws\":{\"source\":\"hashicorp/aws\",\"version\":\"4.15.1\"}}}}","start_line":1,"highlight_start_offset":172,"highlight_end_offset":173,"values":[]}},"type":"diagnostic"}`
    36  
    37  	state = &json.StateV4{
    38  		Version:          uint64(version),
    39  		TerraformVersion: terraformVersion,
    40  		Serial:           uint64(serial),
    41  		Lineage:          lineage,
    42  		RootOutputs:      map[string]json.OutputStateV4{},
    43  		Resources:        []json.ResourceStateV4{},
    44  	}
    45  
    46  	now = time.Now()
    47  
    48  	fs = afero.Afero{
    49  		Fs: afero.NewMemMapFs(),
    50  	}
    51  
    52  	tfstate = `{"version": 1,"terraform_version": "1.0.10","serial": 3,"lineage": "very-cool-lineage","outputs": {},"resources": []}`
    53  
    54  	filterFn = func(s string) string {
    55  		return ""
    56  	}
    57  )
    58  
    59  func newFakeExec(stdOut string, err error) *testingexec.FakeExec {
    60  	return &testingexec.FakeExec{
    61  		CommandScript: []testingexec.FakeCommandAction{
    62  			func(_ string, _ ...string) k8sExec.Cmd {
    63  				return &testingexec.FakeCmd{
    64  					CombinedOutputScript: []testingexec.FakeAction{
    65  						func() ([]byte, []byte, error) {
    66  							return []byte(stdOut), nil, err
    67  						},
    68  					},
    69  				}
    70  			},
    71  		},
    72  	}
    73  }
    74  
    75  func TestWorkspaceApply(t *testing.T) {
    76  	type args struct {
    77  		w *Workspace
    78  	}
    79  	type want struct {
    80  		r   ApplyResult
    81  		err error
    82  	}
    83  
    84  	cases := map[string]struct {
    85  		args
    86  		want
    87  	}{
    88  		"Running": {
    89  			args: args{
    90  				w: NewWorkspace(directory, WithLastOperation(&Operation{Type: testType, startTime: &now, endTime: nil}),
    91  					WithAferoFs(fs), WithFilterFn(filterFn)),
    92  			},
    93  			want: want{
    94  				err: errors.Errorf("%s operation that started at %s is still running", testType, now.String()),
    95  			},
    96  		},
    97  		"Success": {
    98  			args: args{
    99  				w: NewWorkspace(directory, WithExecutor(&testingexec.FakeExec{DisableScripts: true}), WithAferoFs(fs),
   100  					WithFilterFn(filterFn), WithProviderInUse(noopInUse{})),
   101  			},
   102  			want: want{
   103  				r: ApplyResult{
   104  					State: state,
   105  				},
   106  			},
   107  		},
   108  		"Failure": {
   109  			args: args{
   110  				w: NewWorkspace(directory, WithExecutor(newFakeExec(errBoom.Error(), errBoom)), WithAferoFs(fs),
   111  					WithFilterFn(filterFn), WithProviderInUse(noopInUse{})),
   112  			},
   113  			want: want{
   114  				err: tferrors.NewApplyFailed([]byte(errBoom.Error())),
   115  			},
   116  		},
   117  		"Filter": {
   118  			args: args{
   119  				w: NewWorkspace(directory, WithExecutor(newFakeExec(filter, errors.New(filter))), WithAferoFs(fs),
   120  					WithFilterFn(filterFn)),
   121  			},
   122  			want: want{
   123  				err: tferrors.NewApplyFailed([]byte(filter)),
   124  			},
   125  		},
   126  	}
   127  
   128  	for name, tc := range cases {
   129  		t.Run(name, func(t *testing.T) {
   130  			if err := tc.w.fs.WriteFile(directory+"terraform.tfstate", []byte(tfstate), 0777); err != nil {
   131  				panic(err)
   132  			}
   133  			r, err := tc.w.Apply(context.TODO())
   134  			if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
   135  				t.Errorf("\n%s\nApply(...): -want error, +got error:\n%s", name, diff)
   136  			}
   137  			if diff := cmp.Diff(tc.want.r, r, test.EquateErrors()); diff != "" {
   138  				t.Errorf("\n%s\nApply(...): -want error, +got error:\n%s", name, diff)
   139  			}
   140  		})
   141  	}
   142  }
   143  
   144  func TestWorkspaceDestroy(t *testing.T) {
   145  	type args struct {
   146  		w *Workspace
   147  	}
   148  	type want struct {
   149  		err error
   150  	}
   151  
   152  	cases := map[string]struct {
   153  		args
   154  		want
   155  	}{
   156  		"Running": {
   157  			args: args{
   158  				w: NewWorkspace(directory, WithLastOperation(&Operation{Type: testType, startTime: &now, endTime: nil}),
   159  					WithFilterFn(filterFn)),
   160  			},
   161  			want: want{
   162  				err: errors.Errorf("%s operation that started at %s is still running", testType, now.String()),
   163  			},
   164  		},
   165  		"Success": {
   166  			args: args{
   167  				w: NewWorkspace(
   168  					directory, WithExecutor(&testingexec.FakeExec{DisableScripts: true}), WithFilterFn(filterFn)),
   169  			},
   170  			want: want{},
   171  		},
   172  		"Failure": {
   173  			args: args{
   174  				w: NewWorkspace(directory, WithExecutor(newFakeExec(errBoom.Error(), errBoom)), WithFilterFn(filterFn)),
   175  			},
   176  			want: want{
   177  				err: tferrors.NewDestroyFailed([]byte(errBoom.Error())),
   178  			},
   179  		},
   180  		"Filter": {
   181  			args: args{
   182  				w: NewWorkspace(directory, WithExecutor(newFakeExec(filter, errors.New(filter))), WithAferoFs(fs),
   183  					WithFilterFn(filterFn)),
   184  			},
   185  			want: want{
   186  				err: tferrors.NewDestroyFailed([]byte(filter)),
   187  			},
   188  		},
   189  	}
   190  
   191  	for name, tc := range cases {
   192  		t.Run(name, func(t *testing.T) {
   193  			err := tc.w.Destroy(context.TODO())
   194  			if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
   195  				t.Errorf("\n%s\nDestroy(...): -want error, +got error:\n%s", name, diff)
   196  			}
   197  		})
   198  	}
   199  }
   200  
   201  func TestWorkspaceRefresh(t *testing.T) {
   202  	type args struct {
   203  		w *Workspace
   204  	}
   205  	type want struct {
   206  		r   RefreshResult
   207  		err error
   208  	}
   209  
   210  	cases := map[string]struct {
   211  		args
   212  		want
   213  	}{
   214  		"Running": {
   215  			args: args{
   216  				w: NewWorkspace(directory, WithLastOperation(&Operation{Type: applyType, startTime: &now, endTime: nil}),
   217  					WithAferoFs(fs), WithFilterFn(filterFn)),
   218  			},
   219  			want: want{
   220  				r: RefreshResult{
   221  					ASyncInProgress: true,
   222  				},
   223  			},
   224  		},
   225  		"Success": {
   226  			args: args{
   227  				w: NewWorkspace(
   228  					directory, WithExecutor(&testingexec.FakeExec{DisableScripts: true}), WithAferoFs(fs),
   229  					WithFilterFn(filterFn)),
   230  			},
   231  			want: want{
   232  				r: RefreshResult{
   233  					State: state,
   234  				},
   235  			},
   236  		},
   237  		"Failure": {
   238  			args: args{
   239  				w: NewWorkspace(directory, WithExecutor(newFakeExec(errBoom.Error(), errBoom)), WithAferoFs(fs),
   240  					WithFilterFn(filterFn)),
   241  			},
   242  			want: want{
   243  				err: tferrors.NewRefreshFailed([]byte(errBoom.Error())),
   244  			},
   245  		},
   246  		"Filter": {
   247  			args: args{
   248  				w: NewWorkspace(directory, WithExecutor(newFakeExec(filter, errors.New(filter))), WithAferoFs(fs),
   249  					WithFilterFn(filterFn)),
   250  			},
   251  			want: want{
   252  				err: tferrors.NewRefreshFailed([]byte(filter)),
   253  			},
   254  		},
   255  	}
   256  
   257  	for name, tc := range cases {
   258  		t.Run(name, func(t *testing.T) {
   259  			if err := tc.w.fs.WriteFile(directory+"terraform.tfstate", []byte(tfstate), 0777); err != nil {
   260  				panic(err)
   261  			}
   262  			r, err := tc.w.Refresh(context.TODO())
   263  			if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
   264  				t.Errorf("\n%s\nRefresh(...): -want error, +got error:\n%s", name, diff)
   265  			}
   266  			if diff := cmp.Diff(tc.want.r, r, test.EquateErrors()); diff != "" {
   267  				t.Errorf("\n%s\nRefresh(...): -want error, +got error:\n%s", name, diff)
   268  			}
   269  		})
   270  	}
   271  }
   272  
   273  func TestWorkspacePlan(t *testing.T) {
   274  	type args struct {
   275  		w *Workspace
   276  	}
   277  	type want struct {
   278  		r   PlanResult
   279  		err error
   280  	}
   281  
   282  	cases := map[string]struct {
   283  		args
   284  		want
   285  	}{
   286  		"Running": {
   287  			args: args{
   288  				w: NewWorkspace(directory, WithLastOperation(&Operation{Type: testType, startTime: &now, endTime: nil})),
   289  			},
   290  			want: want{
   291  				err: errors.Errorf("%s operation that started at %s is still running", testType, now.String()),
   292  			},
   293  		},
   294  		"NoChangeSummary": {
   295  			args: args{
   296  				w: NewWorkspace(directory, WithExecutor(&testingexec.FakeExec{DisableScripts: true}), WithFilterFn(filterFn)),
   297  			},
   298  			want: want{
   299  				err: errors.Errorf("cannot find the change summary line in plan log: "),
   300  			},
   301  		},
   302  		"ChangeSummaryAdd": {
   303  			args: args{
   304  				w: NewWorkspace(directory, WithExecutor(newFakeExec(changeSummaryAdd, nil)), WithFilterFn(filterFn)),
   305  			},
   306  			want: want{
   307  				r: PlanResult{
   308  					Exists:   false,
   309  					UpToDate: true,
   310  				},
   311  			},
   312  		},
   313  		"ChangeSummaryUpdate": {
   314  			args: args{
   315  				w: NewWorkspace(directory, WithExecutor(newFakeExec(changeSummaryUpdate, nil)), WithFilterFn(filterFn)),
   316  			},
   317  			want: want{
   318  				r: PlanResult{
   319  					Exists:   true,
   320  					UpToDate: false,
   321  				},
   322  			},
   323  		},
   324  		"ChangeSummaryNoAction": {
   325  			args: args{
   326  				w: NewWorkspace(directory, WithExecutor(newFakeExec(changeSummaryNoAction, nil)), WithFilterFn(filterFn)),
   327  			},
   328  			want: want{
   329  				r: PlanResult{
   330  					Exists:   true,
   331  					UpToDate: true,
   332  				},
   333  			},
   334  		},
   335  		"Failure": {
   336  			args: args{
   337  				w: NewWorkspace(directory, WithExecutor(newFakeExec(errBoom.Error(), errBoom)), WithFilterFn(filterFn)),
   338  			},
   339  			want: want{
   340  				err: tferrors.NewPlanFailed([]byte(errBoom.Error())),
   341  			},
   342  		},
   343  		"Filter": {
   344  			args: args{
   345  				w: NewWorkspace(directory, WithExecutor(newFakeExec(filter, errors.New(filter))), WithAferoFs(fs), WithFilterFn(filterFn)),
   346  			},
   347  			want: want{
   348  				err: tferrors.NewPlanFailed([]byte(filter)),
   349  			},
   350  		},
   351  	}
   352  
   353  	for name, tc := range cases {
   354  		t.Run(name, func(t *testing.T) {
   355  			r, err := tc.w.Plan(context.TODO())
   356  			if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
   357  				t.Errorf("\n%s\nPlan(...): -want error, +got error:\n%s", name, diff)
   358  			}
   359  			if diff := cmp.Diff(tc.want.r, r, test.EquateErrors()); diff != "" {
   360  				t.Errorf("\n%s\nPlan(...): -want error, +got error:\n%s", name, diff)
   361  			}
   362  		})
   363  	}
   364  }
   365  
   366  func TestWorkspaceApplyAsync(t *testing.T) {
   367  	calls := make(chan bool)
   368  
   369  	type args struct {
   370  		w *Workspace
   371  		c CallbackFn
   372  	}
   373  	type want struct {
   374  		called bool
   375  		err    error
   376  	}
   377  
   378  	cases := map[string]struct {
   379  		args
   380  		want
   381  	}{
   382  		"Running": {
   383  			args: args{
   384  				w: NewWorkspace(directory, WithLastOperation(&Operation{Type: testType, startTime: &now, endTime: nil}),
   385  					WithFilterFn(filterFn)),
   386  			},
   387  			want: want{
   388  				err: errors.Errorf("%s operation that started at %s is still running", testType, now.String()),
   389  			},
   390  		},
   391  		"Callback": {
   392  			args: args{
   393  				w: NewWorkspace(directory, WithExecutor(&testingexec.FakeExec{DisableScripts: true}), WithFilterFn(filterFn)),
   394  				c: func(err error, ctx context.Context) error {
   395  					calls <- true
   396  					return nil
   397  				},
   398  			},
   399  			want: want{
   400  				called: true,
   401  			},
   402  		},
   403  	}
   404  
   405  	for name, tc := range cases {
   406  		t.Run(name, func(t *testing.T) {
   407  			err := tc.w.ApplyAsync(tc.c)
   408  			if t.Name() == "TestWorkspaceApplyAsync/Callback" {
   409  				called := <-calls
   410  
   411  				if diff := cmp.Diff(tc.want.called, called, test.EquateErrors()); diff != "" {
   412  					t.Errorf("\n%s\nApplyAsync(...): -want error, +got error:\n%s", name, diff)
   413  				}
   414  			}
   415  			if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
   416  				t.Errorf("\n%s\nApplyAsync(...): -want error, +got error:\n%s", name, diff)
   417  			}
   418  		})
   419  	}
   420  }
   421  
   422  func TestWorkspaceDestroyAsync(t *testing.T) {
   423  	calls := make(chan bool)
   424  
   425  	type args struct {
   426  		w *Workspace
   427  		c CallbackFn
   428  	}
   429  	type want struct {
   430  		called bool
   431  		err    error
   432  	}
   433  
   434  	cases := map[string]struct {
   435  		args
   436  		want
   437  	}{
   438  		"Running": {
   439  			args: args{
   440  				w: NewWorkspace(directory, WithLastOperation(&Operation{Type: testType, startTime: &now, endTime: nil}),
   441  					WithFilterFn(filterFn)),
   442  			},
   443  			want: want{
   444  				err: errors.Errorf("%s operation that started at %s is still running", testType, now.String()),
   445  			},
   446  		},
   447  		"Callback": {
   448  			args: args{
   449  				w: NewWorkspace(directory, WithExecutor(&testingexec.FakeExec{DisableScripts: true}), WithFilterFn(filterFn)),
   450  				c: func(err error, ctx context.Context) error {
   451  					calls <- true
   452  					return nil
   453  				},
   454  			},
   455  			want: want{
   456  				called: true,
   457  			},
   458  		},
   459  	}
   460  
   461  	for name, tc := range cases {
   462  		t.Run(name, func(t *testing.T) {
   463  			err := tc.w.DestroyAsync(tc.c)
   464  			if t.Name() == "TestWorkspaceDestroyAsync/Callback" {
   465  				called := <-calls
   466  
   467  				if diff := cmp.Diff(tc.want.called, called, test.EquateErrors()); diff != "" {
   468  					t.Errorf("\n%s\nDestroyAsync(...): -want error, +got error:\n%s", name, diff)
   469  				}
   470  			}
   471  			if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
   472  				t.Errorf("\n%s\nDestroyAsync(...): -want error, +got error:\n%s", name, diff)
   473  			}
   474  		})
   475  	}
   476  }