Skip to content

Commit 62dc842

Browse files
authored
feat: support terraform runtime (#112)
1 parent fb656aa commit 62dc842

23 files changed

+1437
-158
lines changed

go.mod

Lines changed: 3 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,12 @@ require (
66
bou.ke/monkey v1.0.2
77
github.com/AlecAivazis/survey/v2 v2.3.4
88
github.com/Azure/go-autorest/autorest/mocks v0.4.1
9-
github.com/MakeNowJust/heredoc v1.0.0 // indirect
10-
github.com/agext/levenshtein v1.2.3 // indirect
119
github.com/aliyun/aliyun-oss-go-sdk v2.1.8+incompatible
1210
github.com/aws/aws-sdk-go v1.42.35
1311
github.com/chai2010/gettext-go v0.0.0-20170215093142-bf70f2a70fb1
1412
github.com/davecgh/go-spew v1.1.1
1513
github.com/didi/gendry v1.7.0
16-
github.com/elazarl/goproxy v0.0.0-20191011121108-aa519ddbe484 // indirect
1714
github.com/evanphx/json-patch v4.11.0+incompatible
18-
github.com/fatih/color v1.13.0 // indirect
19-
github.com/go-errors/errors v1.4.0 // indirect
20-
github.com/go-openapi/jsonreference v0.19.6 // indirect
21-
github.com/go-openapi/swag v0.19.15 // indirect
2215
github.com/go-sql-driver/mysql v1.6.0
2316
github.com/goccy/go-yaml v1.8.9
2417
github.com/gonvenience/bunt v1.1.1
@@ -27,45 +20,29 @@ require (
2720
github.com/gonvenience/text v1.0.5
2821
github.com/gonvenience/wrap v1.1.0
2922
github.com/gonvenience/ytbx v1.3.0
23+
github.com/google/go-cmp v0.5.8
3024
github.com/google/uuid v1.2.0
3125
github.com/gookit/goutil v0.5.1
3226
github.com/hashicorp/go-version v1.4.0
33-
github.com/hashicorp/hcl/v2 v2.11.1 // indirect
27+
github.com/hashicorp/hcl/v2 v2.11.1
3428
github.com/hashicorp/terraform v0.15.3
35-
github.com/imdario/mergo v0.3.12 // indirect
29+
github.com/imdario/mergo v0.3.13
3630
github.com/jinzhu/copier v0.3.2
37-
github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 // indirect
3831
github.com/lucasb-eyer/go-colorful v1.0.3
39-
github.com/mattn/go-colorable v0.1.11 // indirect
40-
github.com/mitchellh/go-ps v1.0.0 // indirect
41-
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
4232
github.com/mitchellh/hashstructure v1.0.0
43-
github.com/nxadm/tail v1.4.8 // indirect
4433
github.com/pkg/errors v0.9.1
4534
github.com/pterm/pterm v0.12.42-0.20220427210824-6bb8c6e6cc77
4635
github.com/sergi/go-diff v1.2.0
4736
github.com/spf13/afero v1.2.2
4837
github.com/spf13/cobra v1.1.1
49-
github.com/stretchr/objx v0.3.0 // indirect
5038
github.com/stretchr/testify v1.7.1
5139
github.com/texttheater/golang-levenshtein v1.0.1
52-
github.com/xanzy/ssh-agent v0.3.1 // indirect
5340
github.com/zclconf/go-cty v1.10.0
54-
go.uber.org/atomic v1.7.0 // indirect
5541
go.uber.org/zap v1.16.0
56-
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa // indirect
57-
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c // indirect
58-
golang.org/x/sys v0.0.0-20220429121018-84afa8d3f7b3 // indirect
59-
golang.org/x/term v0.0.0-20220411215600-e5f449aeb171 // indirect
60-
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 // indirect
61-
google.golang.org/appengine v1.6.7 // indirect
62-
google.golang.org/genproto v0.0.0-20210420162539-3c870d7478d2 // indirect
63-
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
6442
gopkg.in/natefinch/lumberjack.v2 v2.0.0
6543
gopkg.in/src-d/go-git.v4 v4.13.1
6644
gopkg.in/yaml.v2 v2.4.0
6745
gopkg.in/yaml.v3 v3.0.0
68-
honnef.co/go/tools v0.3.0 // indirect
6946
k8s.io/api v0.21.2
7047
k8s.io/apimachinery v0.21.2
7148
k8s.io/client-go v10.0.0+incompatible

go.sum

Lines changed: 36 additions & 89 deletions
Large diffs are not rendered by default.

pkg/engine/models/resource.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package models
22

3+
import "encoding/json"
4+
35
type Type string
46

57
type Resources []Resource
@@ -26,6 +28,17 @@ func (r *Resource) ResourceKey() string {
2628
return r.ID
2729
}
2830

31+
// DeepCopy return a copy of resource
32+
func (r *Resource) DeepCopy() *Resource {
33+
var out Resource
34+
data, err := json.Marshal(r)
35+
if err != nil {
36+
panic(err)
37+
}
38+
_ = json.Unmarshal(data, &out)
39+
return &out
40+
}
41+
2942
func (rs Resources) Index() map[string]*Resource {
3043
m := make(map[string]*Resource)
3144
for i := range rs {

pkg/engine/operation/graph/resource_node.go

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,8 @@ func (rn *ResourceNode) Execute(operation *opsmodels.Operation) status.Status {
5454
priorState := operation.PriorStateResourceIndex[key]
5555

5656
// 3. get the latest resource from runtime
57-
readRequest := &runtime.ReadRequest{Resource: planedState}
58-
if readRequest.Resource == nil {
59-
readRequest.Resource = priorState
60-
}
57+
readRequest := &runtime.ReadRequest{PlanResource: planedState, PriorResource: priorState}
58+
6159
response := operation.Runtime.Read(context.Background(), readRequest)
6260
liveState := response.Resource
6361
s := response.Status
@@ -138,7 +136,7 @@ func (rn *ResourceNode) applyResource(operation *opsmodels.Operation, priorState
138136
}
139137
case types.UnChange:
140138
log.Infof("planed resource not update live state")
141-
res = planedState
139+
res = priorState
142140
}
143141
if status.IsErr(s) {
144142
return s

pkg/engine/operation/graph/resource_node_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ func TestResourceNode_Execute(t *testing.T) {
181181
})
182182
monkey.PatchInstanceMethod(reflect.TypeOf(tt.args.operation.Runtime), "Read",
183183
func(k *runtime.KubernetesRuntime, ctx context.Context, request *runtime.ReadRequest) *runtime.ReadResponse {
184-
return &runtime.ReadResponse{Resource: request.Resource}
184+
return &runtime.ReadResponse{Resource: request.PriorResource}
185185
})
186186
monkey.PatchInstanceMethod(reflect.TypeOf(tt.args.operation.StateStorage), "Apply",
187187
func(f *local.FileSystemState, state *states.State) error {

pkg/engine/operation/preview_test.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,14 +51,18 @@ func (f *fakePreviewRuntime) Apply(ctx context.Context, request *runtime.ApplyRe
5151
}
5252

5353
func (f *fakePreviewRuntime) Read(ctx context.Context, request *runtime.ReadRequest) *runtime.ReadResponse {
54-
if request.Resource.ResourceKey() == "fake-id" {
54+
requestResource := request.PlanResource
55+
if requestResource == nil {
56+
requestResource = request.PriorResource
57+
}
58+
if requestResource.ResourceKey() == "fake-id" {
5559
return &runtime.ReadResponse{
5660
Resource: nil,
5761
Status: nil,
5862
}
5963
}
6064
return &runtime.ReadResponse{
61-
Resource: request.Resource,
65+
Resource: requestResource,
6266
Status: nil,
6367
}
6468
}

pkg/engine/runtime/init/init.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package init
2+
3+
import (
4+
"kusionstack.io/kusion/pkg/engine/models"
5+
"kusionstack.io/kusion/pkg/engine/runtime"
6+
"kusionstack.io/kusion/pkg/engine/runtime/terraform"
7+
)
8+
9+
func InitRuntime() map[models.Type]InitFn {
10+
runtimes := map[models.Type]InitFn{
11+
runtime.Kubernetes: runtime.NewKubernetesRuntime,
12+
runtime.Terraform: terraform.NewTerraformRuntime,
13+
}
14+
return runtimes
15+
}
16+
17+
// InitFn init Runtime
18+
type InitFn func() (runtime.Runtime, error)

pkg/engine/runtime/kubernetes_runtime.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ func (k *KubernetesRuntime) Apply(ctx context.Context, request *ApplyRequest) *A
6464
}
6565

6666
// Get live state
67-
response := k.Read(ctx, &ReadRequest{planState})
67+
response := k.Read(ctx, &ReadRequest{PlanResource: planState})
6868
if status.IsErr(response.Status) {
6969
return &ApplyResponse{nil, response.Status}
7070
}
@@ -127,7 +127,10 @@ func (k *KubernetesRuntime) Apply(ctx context.Context, request *ApplyRequest) *A
127127

128128
// Read kubernetes Resource by client-go
129129
func (k *KubernetesRuntime) Read(ctx context.Context, request *ReadRequest) *ReadResponse {
130-
requestResource := request.Resource
130+
requestResource := request.PlanResource
131+
if requestResource == nil {
132+
requestResource = request.PriorResource
133+
}
131134
// Validate
132135
if requestResource == nil {
133136
return &ReadResponse{nil, status.NewErrorStatus(errors.New("requestResource is nil"))}

pkg/engine/runtime/runtime.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ import (
88
"kusionstack.io/kusion/pkg/status"
99
)
1010

11+
const (
12+
Kubernetes models.Type = "Kubernetes"
13+
Terraform models.Type = "Terraform"
14+
)
15+
1116
// Runtime represents an actual infrastructure runtime managed by Kusion and every runtime implements this interface can be orchestrated
1217
// by Kusion like normal K8s resources. All methods in this interface are designed for manipulating one Resource at a time and will be
1318
// invoked in operations like Apply, Preview, Destroy, etc.
@@ -52,8 +57,11 @@ type ApplyResponse struct {
5257
}
5358

5459
type ReadRequest struct {
55-
// Resource represents the resource we want to read from the actual infra
56-
Resource *models.Resource
60+
// PriorResource is the last applied resource saved in state storage
61+
PriorResource *models.Resource
62+
63+
// PlanResource is the resource we want to apply in this request
64+
PlanResource *models.Resource
5765
}
5866

5967
type ReadResponse struct {
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
package terraform
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/imdario/mergo"
8+
"github.com/spf13/afero"
9+
"kusionstack.io/kusion/pkg/engine/models"
10+
"kusionstack.io/kusion/pkg/engine/runtime"
11+
"kusionstack.io/kusion/pkg/engine/runtime/terraform/tfops"
12+
"kusionstack.io/kusion/pkg/status"
13+
)
14+
15+
var _ runtime.Runtime = &TerraformRuntime{}
16+
17+
type TerraformRuntime struct {
18+
tfops.WorkspaceStore
19+
}
20+
21+
func NewTerraformRuntime() (runtime.Runtime, error) {
22+
fs := afero.Afero{Fs: afero.NewOsFs()}
23+
ws, err := tfops.GetWorkspaceStore(fs)
24+
if err != nil {
25+
return nil, err
26+
}
27+
TFRuntime := &TerraformRuntime{ws}
28+
return TFRuntime, nil
29+
}
30+
31+
// Apply terraform apply resource
32+
func (t *TerraformRuntime) Apply(ctx context.Context, request *runtime.ApplyRequest) *runtime.ApplyResponse {
33+
planState := request.PlanResource
34+
w, ok := t.Store[planState.ResourceKey()]
35+
if !ok {
36+
err := t.Create(ctx, planState)
37+
if err != nil {
38+
return &runtime.ApplyResponse{Resource: nil, Status: status.NewErrorStatus(err)}
39+
}
40+
w = t.Store[planState.ResourceKey()]
41+
}
42+
43+
// get terraform provider version
44+
providerAddr, err := w.GetProvider()
45+
if err != nil {
46+
return &runtime.ApplyResponse{Resource: nil, Status: status.NewErrorStatus(err)}
47+
}
48+
49+
// terraform dry run merge state
50+
// TODO: terraform dry run apply,not only merge state
51+
if request.DryRun {
52+
prior := request.PriorResource.DeepCopy()
53+
if err := mergo.Merge(prior, planState, mergo.WithSliceDeepCopy, mergo.WithOverride); err != nil {
54+
return &runtime.ApplyResponse{Resource: nil, Status: status.NewErrorStatus(err)}
55+
}
56+
57+
return &runtime.ApplyResponse{Resource: &models.Resource{
58+
ID: planState.ID,
59+
Type: planState.Type,
60+
Attributes: prior.Attributes,
61+
DependsOn: planState.DependsOn,
62+
Extensions: planState.Extensions,
63+
}, Status: nil}
64+
}
65+
w.SetResource(planState)
66+
67+
if err := w.WriteHCL(); err != nil {
68+
return &runtime.ApplyResponse{Resource: nil, Status: status.NewErrorStatus(err)}
69+
}
70+
71+
tfstate, err := w.Apply(ctx)
72+
if err != nil {
73+
return &runtime.ApplyResponse{Resource: nil, Status: status.NewErrorStatus(err)}
74+
}
75+
76+
r := tfops.ConvertTFState(tfstate, providerAddr)
77+
78+
return &runtime.ApplyResponse{
79+
Resource: &models.Resource{
80+
ID: r.ID,
81+
Type: r.Type,
82+
Attributes: r.Attributes,
83+
DependsOn: planState.DependsOn,
84+
Extensions: planState.Extensions,
85+
},
86+
Status: nil,
87+
}
88+
}
89+
90+
// Read terraform show state
91+
func (t *TerraformRuntime) Read(ctx context.Context, request *runtime.ReadRequest) *runtime.ReadResponse {
92+
priorState := request.PriorResource
93+
planState := request.PlanResource
94+
if priorState == nil {
95+
return &runtime.ReadResponse{Resource: nil, Status: nil}
96+
}
97+
var tfstate *tfops.TFState
98+
w, ok := t.Store[planState.ResourceKey()]
99+
if !ok {
100+
err := t.Create(ctx, planState)
101+
if err != nil {
102+
return &runtime.ReadResponse{Resource: nil, Status: status.NewErrorStatus(err)}
103+
}
104+
w = t.Store[priorState.ResourceKey()]
105+
if err := w.WriteTFState(priorState); err != nil {
106+
return &runtime.ReadResponse{Resource: nil, Status: status.NewErrorStatus(err)}
107+
}
108+
}
109+
110+
tfstate, err := w.RefreshOnly(ctx)
111+
if err != nil {
112+
return &runtime.ReadResponse{Resource: nil, Status: status.NewErrorStatus(err)}
113+
}
114+
if tfstate == nil || tfstate.Values == nil {
115+
return &runtime.ReadResponse{Resource: nil, Status: nil}
116+
}
117+
118+
// get terraform provider addr
119+
providerAddr, err := w.GetProvider()
120+
if err != nil {
121+
return &runtime.ReadResponse{Resource: nil, Status: status.NewErrorStatus(err)}
122+
}
123+
124+
r := tfops.ConvertTFState(tfstate, providerAddr)
125+
return &runtime.ReadResponse{
126+
Resource: &models.Resource{
127+
ID: r.ID,
128+
Type: r.Type,
129+
Attributes: r.Attributes,
130+
DependsOn: planState.DependsOn,
131+
Extensions: planState.Extensions,
132+
},
133+
Status: nil,
134+
}
135+
}
136+
137+
// Delete terraform resource and remove workspace
138+
func (t *TerraformRuntime) Delete(ctx context.Context, request *runtime.DeleteRequest) *runtime.DeleteResponse {
139+
w, ok := t.Store[request.Resource.ResourceKey()]
140+
if !ok {
141+
return &runtime.DeleteResponse{Status: status.NewErrorStatus(fmt.Errorf("%s terraform workspace not exist, cannot delete", request.Resource.ResourceKey()))}
142+
}
143+
if err := w.Destroy(ctx); err != nil {
144+
return &runtime.DeleteResponse{Status: status.NewErrorStatus(err)}
145+
}
146+
147+
if err := t.Remove(ctx, request.Resource); err != nil {
148+
return &runtime.DeleteResponse{Status: status.NewErrorStatus(err)}
149+
}
150+
return &runtime.DeleteResponse{Status: nil}
151+
}
152+
153+
// Watch terraform resource
154+
func (t *TerraformRuntime) Watch(ctx context.Context, request *runtime.WatchRequest) *runtime.WatchResponse {
155+
return nil
156+
}

0 commit comments

Comments
 (0)