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

feat: support terraform runtime #112

Merged
merged 1 commit into from
Aug 16, 2022
Merged
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
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
markliby marked this conversation as resolved.
Show resolved Hide resolved
howieyuen marked this conversation as resolved.
Show resolved Hide resolved
}
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