-
Notifications
You must be signed in to change notification settings - Fork 78
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add state storage implementation: local, oss, s3, mysql
- Loading branch information
Showing
11 changed files
with
708 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
}) | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Oops, something went wrong.