Skip to content

Commit

Permalink
feat: support terraform runtime (#112)
Browse files Browse the repository at this point in the history
  • Loading branch information
markliby committed Aug 16, 2022
1 parent fb656aa commit 62dc842
Show file tree
Hide file tree
Showing 23 changed files with 1,437 additions and 158 deletions.
29 changes: 3 additions & 26 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,12 @@ require (
bou.ke/monkey v1.0.2
github.com/AlecAivazis/survey/v2 v2.3.4
github.com/Azure/go-autorest/autorest/mocks v0.4.1
github.com/MakeNowJust/heredoc v1.0.0 // indirect
github.com/agext/levenshtein v1.2.3 // indirect
github.com/aliyun/aliyun-oss-go-sdk v2.1.8+incompatible
github.com/aws/aws-sdk-go v1.42.35
github.com/chai2010/gettext-go v0.0.0-20170215093142-bf70f2a70fb1
github.com/davecgh/go-spew v1.1.1
github.com/didi/gendry v1.7.0
github.com/elazarl/goproxy v0.0.0-20191011121108-aa519ddbe484 // indirect
github.com/evanphx/json-patch v4.11.0+incompatible
github.com/fatih/color v1.13.0 // indirect
github.com/go-errors/errors v1.4.0 // indirect
github.com/go-openapi/jsonreference v0.19.6 // indirect
github.com/go-openapi/swag v0.19.15 // indirect
github.com/go-sql-driver/mysql v1.6.0
github.com/goccy/go-yaml v1.8.9
github.com/gonvenience/bunt v1.1.1
Expand All @@ -27,45 +20,29 @@ require (
github.com/gonvenience/text v1.0.5
github.com/gonvenience/wrap v1.1.0
github.com/gonvenience/ytbx v1.3.0
github.com/google/go-cmp v0.5.8
github.com/google/uuid v1.2.0
github.com/gookit/goutil v0.5.1
github.com/hashicorp/go-version v1.4.0
github.com/hashicorp/hcl/v2 v2.11.1 // indirect
github.com/hashicorp/hcl/v2 v2.11.1
github.com/hashicorp/terraform v0.15.3
github.com/imdario/mergo v0.3.12 // indirect
github.com/imdario/mergo v0.3.13
github.com/jinzhu/copier v0.3.2
github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 // indirect
github.com/lucasb-eyer/go-colorful v1.0.3
github.com/mattn/go-colorable v0.1.11 // indirect
github.com/mitchellh/go-ps v1.0.0 // indirect
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
github.com/mitchellh/hashstructure v1.0.0
github.com/nxadm/tail v1.4.8 // indirect
github.com/pkg/errors v0.9.1
github.com/pterm/pterm v0.12.42-0.20220427210824-6bb8c6e6cc77
github.com/sergi/go-diff v1.2.0
github.com/spf13/afero v1.2.2
github.com/spf13/cobra v1.1.1
github.com/stretchr/objx v0.3.0 // indirect
github.com/stretchr/testify v1.7.1
github.com/texttheater/golang-levenshtein v1.0.1
github.com/xanzy/ssh-agent v0.3.1 // indirect
github.com/zclconf/go-cty v1.10.0
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/zap v1.16.0
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa // indirect
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c // indirect
golang.org/x/sys v0.0.0-20220429121018-84afa8d3f7b3 // indirect
golang.org/x/term v0.0.0-20220411215600-e5f449aeb171 // indirect
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20210420162539-3c870d7478d2 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/natefinch/lumberjack.v2 v2.0.0
gopkg.in/src-d/go-git.v4 v4.13.1
gopkg.in/yaml.v2 v2.4.0
gopkg.in/yaml.v3 v3.0.0
honnef.co/go/tools v0.3.0 // indirect
k8s.io/api v0.21.2
k8s.io/apimachinery v0.21.2
k8s.io/client-go v10.0.0+incompatible
Expand Down
125 changes: 36 additions & 89 deletions go.sum

Large diffs are not rendered by default.

13 changes: 13 additions & 0 deletions pkg/engine/models/resource.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package models

import "encoding/json"

type Type string

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

// DeepCopy return a copy of resource
func (r *Resource) DeepCopy() *Resource {
var out Resource
data, err := json.Marshal(r)
if err != nil {
panic(err)
}
_ = json.Unmarshal(data, &out)
return &out
}

func (rs Resources) Index() map[string]*Resource {
m := make(map[string]*Resource)
for i := range rs {
Expand Down
8 changes: 3 additions & 5 deletions pkg/engine/operation/graph/resource_node.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,8 @@ func (rn *ResourceNode) Execute(operation *opsmodels.Operation) status.Status {
priorState := operation.PriorStateResourceIndex[key]

// 3. get the latest resource from runtime
readRequest := &runtime.ReadRequest{Resource: planedState}
if readRequest.Resource == nil {
readRequest.Resource = priorState
}
readRequest := &runtime.ReadRequest{PlanResource: planedState, PriorResource: priorState}

response := operation.Runtime.Read(context.Background(), readRequest)
liveState := response.Resource
s := response.Status
Expand Down Expand Up @@ -138,7 +136,7 @@ func (rn *ResourceNode) applyResource(operation *opsmodels.Operation, priorState
}
case types.UnChange:
log.Infof("planed resource not update live state")
res = planedState
res = priorState
}
if status.IsErr(s) {
return s
Expand Down
2 changes: 1 addition & 1 deletion pkg/engine/operation/graph/resource_node_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ func TestResourceNode_Execute(t *testing.T) {
})
monkey.PatchInstanceMethod(reflect.TypeOf(tt.args.operation.Runtime), "Read",
func(k *runtime.KubernetesRuntime, ctx context.Context, request *runtime.ReadRequest) *runtime.ReadResponse {
return &runtime.ReadResponse{Resource: request.Resource}
return &runtime.ReadResponse{Resource: request.PriorResource}
})
monkey.PatchInstanceMethod(reflect.TypeOf(tt.args.operation.StateStorage), "Apply",
func(f *local.FileSystemState, state *states.State) error {
Expand Down
8 changes: 6 additions & 2 deletions pkg/engine/operation/preview_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,18 @@ func (f *fakePreviewRuntime) Apply(ctx context.Context, request *runtime.ApplyRe
}

func (f *fakePreviewRuntime) Read(ctx context.Context, request *runtime.ReadRequest) *runtime.ReadResponse {
if request.Resource.ResourceKey() == "fake-id" {
requestResource := request.PlanResource
if requestResource == nil {
requestResource = request.PriorResource
}
if requestResource.ResourceKey() == "fake-id" {
return &runtime.ReadResponse{
Resource: nil,
Status: nil,
}
}
return &runtime.ReadResponse{
Resource: request.Resource,
Resource: requestResource,
Status: nil,
}
}
Expand Down
18 changes: 18 additions & 0 deletions pkg/engine/runtime/init/init.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package init

import (
"kusionstack.io/kusion/pkg/engine/models"
"kusionstack.io/kusion/pkg/engine/runtime"
"kusionstack.io/kusion/pkg/engine/runtime/terraform"
)

func InitRuntime() map[models.Type]InitFn {
runtimes := map[models.Type]InitFn{
runtime.Kubernetes: runtime.NewKubernetesRuntime,
runtime.Terraform: terraform.NewTerraformRuntime,
}
return runtimes
}

// InitFn init Runtime
type InitFn func() (runtime.Runtime, error)
7 changes: 5 additions & 2 deletions pkg/engine/runtime/kubernetes_runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ func (k *KubernetesRuntime) Apply(ctx context.Context, request *ApplyRequest) *A
}

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

// Read kubernetes Resource by client-go
func (k *KubernetesRuntime) Read(ctx context.Context, request *ReadRequest) *ReadResponse {
requestResource := request.Resource
requestResource := request.PlanResource
if requestResource == nil {
requestResource = request.PriorResource
}
// Validate
if requestResource == nil {
return &ReadResponse{nil, status.NewErrorStatus(errors.New("requestResource is nil"))}
Expand Down
12 changes: 10 additions & 2 deletions pkg/engine/runtime/runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ import (
"kusionstack.io/kusion/pkg/status"
)

const (
Kubernetes models.Type = "Kubernetes"
Terraform models.Type = "Terraform"
)

// Runtime represents an actual infrastructure runtime managed by Kusion and every runtime implements this interface can be orchestrated
// by Kusion like normal K8s resources. All methods in this interface are designed for manipulating one Resource at a time and will be
// invoked in operations like Apply, Preview, Destroy, etc.
Expand Down Expand Up @@ -52,8 +57,11 @@ type ApplyResponse struct {
}

type ReadRequest struct {
// Resource represents the resource we want to read from the actual infra
Resource *models.Resource
// PriorResource is the last applied resource saved in state storage
PriorResource *models.Resource

// PlanResource is the resource we want to apply in this request
PlanResource *models.Resource
}

type ReadResponse struct {
Expand Down
156 changes: 156 additions & 0 deletions pkg/engine/runtime/terraform/terraform_runtime.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
package terraform

import (
"context"
"fmt"

"github.com/imdario/mergo"
"github.com/spf13/afero"
"kusionstack.io/kusion/pkg/engine/models"
"kusionstack.io/kusion/pkg/engine/runtime"
"kusionstack.io/kusion/pkg/engine/runtime/terraform/tfops"
"kusionstack.io/kusion/pkg/status"
)

var _ runtime.Runtime = &TerraformRuntime{}

type TerraformRuntime struct {
tfops.WorkspaceStore
}

func NewTerraformRuntime() (runtime.Runtime, error) {
fs := afero.Afero{Fs: afero.NewOsFs()}
ws, err := tfops.GetWorkspaceStore(fs)
if err != nil {
return nil, err
}
TFRuntime := &TerraformRuntime{ws}
return TFRuntime, nil
}

// Apply terraform apply resource
func (t *TerraformRuntime) Apply(ctx context.Context, request *runtime.ApplyRequest) *runtime.ApplyResponse {
planState := request.PlanResource
w, ok := t.Store[planState.ResourceKey()]
if !ok {
err := t.Create(ctx, planState)
if err != nil {
return &runtime.ApplyResponse{Resource: nil, Status: status.NewErrorStatus(err)}
}
w = t.Store[planState.ResourceKey()]
}

// get terraform provider version
providerAddr, err := w.GetProvider()
if err != nil {
return &runtime.ApplyResponse{Resource: nil, Status: status.NewErrorStatus(err)}
}

// terraform dry run merge state
// TODO: terraform dry run apply,not only merge state
if request.DryRun {
prior := request.PriorResource.DeepCopy()
if err := mergo.Merge(prior, planState, mergo.WithSliceDeepCopy, mergo.WithOverride); err != nil {
return &runtime.ApplyResponse{Resource: nil, Status: status.NewErrorStatus(err)}
}

return &runtime.ApplyResponse{Resource: &models.Resource{
ID: planState.ID,
Type: planState.Type,
Attributes: prior.Attributes,
DependsOn: planState.DependsOn,
Extensions: planState.Extensions,
}, Status: nil}
}
w.SetResource(planState)

if err := w.WriteHCL(); err != nil {
return &runtime.ApplyResponse{Resource: nil, Status: status.NewErrorStatus(err)}
}

tfstate, err := w.Apply(ctx)
if err != nil {
return &runtime.ApplyResponse{Resource: nil, Status: status.NewErrorStatus(err)}
}

r := tfops.ConvertTFState(tfstate, providerAddr)

return &runtime.ApplyResponse{
Resource: &models.Resource{
ID: r.ID,
Type: r.Type,
Attributes: r.Attributes,
DependsOn: planState.DependsOn,
Extensions: planState.Extensions,
},
Status: nil,
}
}

// Read terraform show state
func (t *TerraformRuntime) Read(ctx context.Context, request *runtime.ReadRequest) *runtime.ReadResponse {
priorState := request.PriorResource
planState := request.PlanResource
if priorState == nil {
return &runtime.ReadResponse{Resource: nil, Status: nil}
}
var tfstate *tfops.TFState
w, ok := t.Store[planState.ResourceKey()]
if !ok {
err := t.Create(ctx, planState)
if err != nil {
return &runtime.ReadResponse{Resource: nil, Status: status.NewErrorStatus(err)}
}
w = t.Store[priorState.ResourceKey()]
if err := w.WriteTFState(priorState); err != nil {
return &runtime.ReadResponse{Resource: nil, Status: status.NewErrorStatus(err)}
}
}

tfstate, err := w.RefreshOnly(ctx)
if err != nil {
return &runtime.ReadResponse{Resource: nil, Status: status.NewErrorStatus(err)}
}
if tfstate == nil || tfstate.Values == nil {
return &runtime.ReadResponse{Resource: nil, Status: nil}
}

// get terraform provider addr
providerAddr, err := w.GetProvider()
if err != nil {
return &runtime.ReadResponse{Resource: nil, Status: status.NewErrorStatus(err)}
}

r := tfops.ConvertTFState(tfstate, providerAddr)
return &runtime.ReadResponse{
Resource: &models.Resource{
ID: r.ID,
Type: r.Type,
Attributes: r.Attributes,
DependsOn: planState.DependsOn,
Extensions: planState.Extensions,
},
Status: nil,
}
}

// Delete terraform resource and remove workspace
func (t *TerraformRuntime) Delete(ctx context.Context, request *runtime.DeleteRequest) *runtime.DeleteResponse {
w, ok := t.Store[request.Resource.ResourceKey()]
if !ok {
return &runtime.DeleteResponse{Status: status.NewErrorStatus(fmt.Errorf("%s terraform workspace not exist, cannot delete", request.Resource.ResourceKey()))}
}
if err := w.Destroy(ctx); err != nil {
return &runtime.DeleteResponse{Status: status.NewErrorStatus(err)}
}

if err := t.Remove(ctx, request.Resource); err != nil {
return &runtime.DeleteResponse{Status: status.NewErrorStatus(err)}
}
return &runtime.DeleteResponse{Status: nil}
}

// Watch terraform resource
func (t *TerraformRuntime) Watch(ctx context.Context, request *runtime.WatchRequest) *runtime.WatchResponse {
return nil
}
Loading

0 comments on commit 62dc842

Please sign in to comment.