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: add state storage implementation: local, oss, s3, mysql #902

Merged
merged 1 commit into from
Mar 12, 2024
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
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
Loading