Skip to content

Commit

Permalink
feat: add state storage implementation: local, oss, s3, mysql
Browse files Browse the repository at this point in the history
  • Loading branch information
healthjyk committed Mar 11, 2024
1 parent dfd8298 commit 27cee93
Show file tree
Hide file tree
Showing 11 changed files with 708 additions and 2 deletions.
2 changes: 1 addition & 1 deletion pkg/apis/core/v1/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ type State struct {
CreateTime time.Time `json:"createTime" yaml:"createTime"`

// ModifiedTime is the time State is modified each time
ModifiedTime time.Time `json:"modifiedTime,omitempty" yaml:"modifiedTime"`
ModifiedTime time.Time `json:"modifiedTime,omitempty" yaml:"modifiedTime,omitempty"`
}

func NewState() *State {
Expand Down
3 changes: 2 additions & 1 deletion pkg/engine/operation/models/operation_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (

"github.com/jinzhu/copier"

"kusionstack.io/kusion/pkg/apis/core/v1"
v1 "kusionstack.io/kusion/pkg/apis/core/v1"
"kusionstack.io/kusion/pkg/engine/runtime"
"kusionstack.io/kusion/pkg/engine/states"
"kusionstack.io/kusion/pkg/log"
Expand Down Expand Up @@ -139,6 +139,7 @@ func (o *Operation) UpdateState(resourceIndex map[string]*v1.Resource) error {
}

state.Resources = res
// todo: update
err := o.StateStorage.Apply(state)
if err != nil {
return fmt.Errorf("apply State failed. %w", err)
Expand Down
46 changes: 46 additions & 0 deletions pkg/engine/state/storages/local.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package storages

import (
"io/fs"
"os"

"gopkg.in/yaml.v3"

v1 "kusionstack.io/kusion/pkg/apis/core/v1"
)

// LocalStorage is an implementation of state.Storage which uses local filesystem as storage.
type LocalStorage struct {
// The path of state file.
path string
}

func NewLocalStorage(path string) *LocalStorage {
return &LocalStorage{path: path}
}

func (s *LocalStorage) Get() (*v1.State, error) {
content, err := os.ReadFile(s.path)
if err != nil && !os.IsNotExist(err) {
return nil, err
}

if len(content) != 0 {
state := &v1.State{}
err = yaml.Unmarshal(content, state)
if err != nil {
return nil, err
}
return state, nil
} else {
return nil, nil
}
}

func (s *LocalStorage) Apply(state *v1.State) error {
content, err := yaml.Marshal(state)
if err != nil {
return err
}
return os.WriteFile(s.path, content, fs.ModePerm)
}
120 changes: 120 additions & 0 deletions pkg/engine/state/storages/local_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package storages

import (
"os"
"testing"

"github.com/bytedance/mockey"
"github.com/stretchr/testify/assert"

v1 "kusionstack.io/kusion/pkg/apis/core/v1"
)

func mockState() *v1.State {
return &v1.State{
ID: 1,
Project: "wordpress",
Stack: "dev",
Workspace: "dev",
Version: 1,
KusionVersion: "0.11.0",
Serial: 1,
Operator: "kk-confused",
Resources: v1.Resources{
v1.Resource{
ID: "v1:ServiceAccount:wordpress:wordpress",
Type: "Kubernetes",
Attributes: map[string]interface{}{
"apiVersion": "v1",
"kind": "ServiceAccount",
"metadata": map[string]interface{}{
"name": "wordpress",
"namespace": "wordpress",
},
},
},
},
}
}

func mockStateContent() string {
return `
id: 1
project: wordpress
stack: dev
workspace: dev
version: 1
kusionVersion: 0.11.0
serial: 1
operator: kk-confused
resources:
- id: v1:ServiceAccount:wordpress:wordpress
type: Kubernetes
attributes:
apiVersion: v1
kind: ServiceAccount
metadata:
name: wordpress
namespace: wordpress
createTime: 0001-01-01T00:00:00Z
`
}

func mockLocalStorage() *LocalStorage {
return &LocalStorage{}
}

func TestLocalStorage_Get(t *testing.T) {
testcases := []struct {
name string
success bool
content []byte
state *v1.State
}{
{
name: "get local state successfully",
success: true,
content: []byte(mockStateContent()),
state: mockState(),
},
{
name: "get empty local state successfully",
success: true,
content: nil,
state: nil,
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
mockey.PatchConvey("mock read state file", t, func() {
mockey.Mock(os.ReadFile).Return(tc.content, nil).Build()
state, err := mockLocalStorage().Get()
assert.Equal(t, tc.success, err == nil)
assert.Equal(t, tc.state, state)
})
})
}
}

func TestLocalStorage_Apply(t *testing.T) {
testcases := []struct {
name string
success bool
state *v1.State
}{
{
name: "apply local state successfully",
success: true,
state: mockState(),
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
mockey.PatchConvey("mock write state file", t, func() {
mockey.Mock(os.WriteFile).Return(nil).Build()
err := mockLocalStorage().Apply(tc.state)
assert.Equal(t, tc.success, err == nil)
})
})
}
}
120 changes: 120 additions & 0 deletions pkg/engine/state/storages/mysql.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package storages

import (
"gopkg.in/yaml.v3"
"gorm.io/gorm"

v1 "kusionstack.io/kusion/pkg/apis/core/v1"
)

// MysqlStorage is an implementation of state.Storage which uses mysql as storage.
type MysqlStorage struct {
db *gorm.DB
project, stack, workspace string
}

func NewMysqlStorage(db *gorm.DB, project, stack, workspace string) *MysqlStorage {
return &MysqlStorage{
db: db,
project: project,
stack: stack,
workspace: workspace,
}
}

func (s *MysqlStorage) Get() (*v1.State, error) {
stateDO, err := getState(s.db, s.project, s.stack, s.workspace)
if err != nil {
return nil, err
}
if stateDO == nil {
return nil, nil
}
return convertFromDO(stateDO)
}

func (s *MysqlStorage) Apply(state *v1.State) error {
exist, err := isStateExist(s.db, s.project, s.stack, s.workspace)
if err != nil {
return err
}

stateDO, err := convertToDO(state)
if err != nil {
return err
}
if exist {
return updateState(s.db, stateDO)
} else {
return createState(s.db, stateDO)
}
}

// State is the data object stored in the mysql db.
type State struct {
Project string
Stack string
Workspace string
Content string
}

func (s State) TableName() string {
return stateTable
}

func getState(db *gorm.DB, project, stack, workspace string) (*State, error) {
q := &State{
Project: project,
Stack: stack,
Workspace: workspace,
}
s := &State{}
result := db.Where(q).First(s)
// if no record, return nil
if *s == (State{}) {
s = nil
}
return s, result.Error
}

func isStateExist(db *gorm.DB, project, stack, workspace string) (bool, error) {
s, err := getState(db, project, stack, workspace)
if err != nil {
return false, err
}
return s != nil, err
}

func createState(db *gorm.DB, s *State) error {
return db.Create(s).Error
}

func updateState(db *gorm.DB, s *State) error {
q := &State{
Project: s.Project,
Stack: s.Stack,
Workspace: s.Workspace,
}
return db.Where(q).Updates(s).Error
}

func convertToDO(state *v1.State) (*State, error) {
content, err := yaml.Marshal(state)
if err != nil {
return nil, err
}
return &State{
Project: state.Project,
Stack: state.Stack,
Workspace: state.Workspace,
Content: string(content),
}, nil
}

func convertFromDO(s *State) (*v1.State, error) {
state := &v1.State{}
if err := yaml.Unmarshal([]byte(s.Content), state); err != nil {
return nil, err
}
return state, nil
}
Loading

0 comments on commit 27cee93

Please sign in to comment.