mirror of
https://github.com/semaphoreui/semaphore.git
synced 2024-11-23 12:30:41 +01:00
feat(backup): marshal/unmarshal
This commit is contained in:
parent
0d75e2e28d
commit
9aa492b53f
@ -1,7 +1,9 @@
|
||||
package project
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/ansible-semaphore/semaphore/db"
|
||||
"github.com/ansible-semaphore/semaphore/pkg/random"
|
||||
@ -266,3 +268,311 @@ func GetBackup(projectID int, store db.Store) (*BackupFormat, error) {
|
||||
|
||||
return backup.format()
|
||||
}
|
||||
|
||||
func marshalValue(v reflect.Value) (interface{}, error) {
|
||||
// Handle pointers
|
||||
if v.Kind() == reflect.Ptr {
|
||||
if v.IsNil() {
|
||||
return nil, nil
|
||||
}
|
||||
return marshalValue(v.Elem())
|
||||
}
|
||||
|
||||
// Handle structs
|
||||
if v.Kind() == reflect.Struct {
|
||||
typeOfV := v.Type()
|
||||
result := make(map[string]interface{})
|
||||
|
||||
for i := 0; i < v.NumField(); i++ {
|
||||
fieldValue := v.Field(i)
|
||||
fieldType := typeOfV.Field(i)
|
||||
|
||||
// Handle anonymous fields (embedded structs)
|
||||
if fieldType.Anonymous {
|
||||
embeddedValue, err := marshalValue(fieldValue)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if embeddedMap, ok := embeddedValue.(map[string]interface{}); ok {
|
||||
// Merge embedded struct fields into parent result map
|
||||
for k, v := range embeddedMap {
|
||||
result[k] = v
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
tag := fieldType.Tag.Get("backup")
|
||||
|
||||
// Check if the field should be backed up
|
||||
if tag == "-" {
|
||||
continue // Skip fields with backup:"-"
|
||||
} else if tag == "" {
|
||||
// Get the field name from the "db" tag
|
||||
tag = fieldType.Tag.Get("db")
|
||||
if tag == "" || tag == "-" {
|
||||
continue // Skip if "db" tag is empty or "-"
|
||||
}
|
||||
}
|
||||
|
||||
// Recursively process the field value
|
||||
value, err := marshalValue(fieldValue)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result[tag] = value
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Handle slices and arrays
|
||||
if v.Kind() == reflect.Slice || v.Kind() == reflect.Array {
|
||||
if v.IsNil() {
|
||||
return nil, nil
|
||||
}
|
||||
var result []interface{}
|
||||
for i := 0; i < v.Len(); i++ {
|
||||
elemValue, err := marshalValue(v.Index(i))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = append(result, elemValue)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Handle maps
|
||||
if v.Kind() == reflect.Map {
|
||||
if v.IsNil() {
|
||||
return nil, nil
|
||||
}
|
||||
result := make(map[string]interface{})
|
||||
for _, key := range v.MapKeys() {
|
||||
// Assuming the key is a string
|
||||
mapKey := fmt.Sprintf("%v", key.Interface())
|
||||
mapValue, err := marshalValue(v.MapIndex(key))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result[mapKey] = mapValue
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Handle other types (int, string, etc.)
|
||||
return v.Interface(), nil
|
||||
}
|
||||
|
||||
// UnmarshalStruct deserializes JSON data into a struct,
|
||||
// using the "db" tag for field names and excluding fields with backup:"-".
|
||||
func UnmarshalStruct(data []byte, v interface{}) error {
|
||||
// Parse the JSON data into an interface{}
|
||||
var jsonData interface{}
|
||||
if err := json.Unmarshal(data, &jsonData); err != nil {
|
||||
return err
|
||||
}
|
||||
// Start the recursive unmarshaling process
|
||||
return unmarshalValue(jsonData, reflect.ValueOf(v))
|
||||
}
|
||||
|
||||
func unmarshalValue(data interface{}, v reflect.Value) error {
|
||||
// Handle pointers
|
||||
if v.Kind() == reflect.Ptr {
|
||||
// Initialize pointer if it's nil
|
||||
if v.IsNil() {
|
||||
v.Set(reflect.New(v.Type().Elem()))
|
||||
}
|
||||
return unmarshalValue(data, v.Elem())
|
||||
}
|
||||
|
||||
// Handle structs
|
||||
if v.Kind() == reflect.Struct {
|
||||
// Data should be a map
|
||||
m, ok := data.(map[string]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("expected object for struct, got %T", data)
|
||||
}
|
||||
return unmarshalStruct(m, v)
|
||||
}
|
||||
|
||||
// Handle slices and arrays
|
||||
if v.Kind() == reflect.Slice {
|
||||
// Data should be an array
|
||||
dataSlice, ok := data.([]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("expected array for slice, got %T", data)
|
||||
}
|
||||
// Create a new slice
|
||||
slice := reflect.MakeSlice(v.Type(), len(dataSlice), len(dataSlice))
|
||||
for i := 0; i < len(dataSlice); i++ {
|
||||
elem := slice.Index(i)
|
||||
if err := unmarshalValue(dataSlice[i], elem); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
v.Set(slice)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Handle maps
|
||||
if v.Kind() == reflect.Map {
|
||||
// Data should be a map
|
||||
dataMap, ok := data.(map[string]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("expected object for map, got %T", data)
|
||||
}
|
||||
mapType := v.Type()
|
||||
mapValue := reflect.MakeMap(mapType)
|
||||
for key, value := range dataMap {
|
||||
keyVal := reflect.ValueOf(key).Convert(mapType.Key())
|
||||
valVal := reflect.New(mapType.Elem()).Elem()
|
||||
if err := unmarshalValue(value, valVal); err != nil {
|
||||
return err
|
||||
}
|
||||
mapValue.SetMapIndex(keyVal, valVal)
|
||||
}
|
||||
v.Set(mapValue)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Handle basic types
|
||||
if err := setBasicType(data, v); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func unmarshalStruct(data map[string]interface{}, v reflect.Value) error {
|
||||
t := v.Type()
|
||||
|
||||
// Build a map of db tags to field indices
|
||||
fieldMap := make(map[string]int)
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
fieldType := t.Field(i)
|
||||
|
||||
// Skip fields with backup:"-"
|
||||
if backupTag := fieldType.Tag.Get("backup"); backupTag == "-" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Get the field name from the "db" tag
|
||||
dbTag := fieldType.Tag.Get("db")
|
||||
if dbTag == "" || dbTag == "-" {
|
||||
continue
|
||||
}
|
||||
|
||||
fieldMap[dbTag] = i
|
||||
}
|
||||
|
||||
// Iterate over the JSON data and set struct fields
|
||||
for key, value := range data {
|
||||
if index, ok := fieldMap[key]; ok {
|
||||
field := v.Field(index)
|
||||
if !field.CanSet() {
|
||||
continue // Skip unexportable fields
|
||||
}
|
||||
if err := unmarshalValue(value, field); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func setBasicType(data interface{}, v reflect.Value) error {
|
||||
if !v.CanSet() {
|
||||
return fmt.Errorf("cannot set value of type %v", v.Type())
|
||||
}
|
||||
|
||||
switch v.Kind() {
|
||||
case reflect.Bool:
|
||||
b, ok := data.(bool)
|
||||
if !ok {
|
||||
return fmt.Errorf("expected bool for field, got %T", data)
|
||||
}
|
||||
v.SetBool(b)
|
||||
case reflect.String:
|
||||
s, ok := data.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("expected string for field, got %T", data)
|
||||
}
|
||||
v.SetString(s)
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
n, ok := toFloat64(data)
|
||||
if !ok {
|
||||
return fmt.Errorf("expected number for field, got %T", data)
|
||||
}
|
||||
v.SetInt(int64(n))
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
n, ok := toFloat64(data)
|
||||
if !ok {
|
||||
return fmt.Errorf("expected number for field, got %T", data)
|
||||
}
|
||||
v.SetUint(uint64(n))
|
||||
case reflect.Float32, reflect.Float64:
|
||||
n, ok := toFloat64(data)
|
||||
if !ok {
|
||||
return fmt.Errorf("expected number for field, got %T", data)
|
||||
}
|
||||
v.SetFloat(n)
|
||||
default:
|
||||
return fmt.Errorf("unsupported kind %v", v.Kind())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func toFloat64(data interface{}) (float64, bool) {
|
||||
switch n := data.(type) {
|
||||
case float64:
|
||||
return n, true
|
||||
case float32:
|
||||
return float64(n), true
|
||||
case int:
|
||||
return float64(n), true
|
||||
case int64:
|
||||
return float64(n), true
|
||||
case int32:
|
||||
return float64(n), true
|
||||
case int16:
|
||||
return float64(n), true
|
||||
case int8:
|
||||
return float64(n), true
|
||||
case uint:
|
||||
return float64(n), true
|
||||
case uint64:
|
||||
return float64(n), true
|
||||
case uint32:
|
||||
return float64(n), true
|
||||
case uint16:
|
||||
return float64(n), true
|
||||
case uint8:
|
||||
return float64(n), true
|
||||
default:
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
|
||||
func (b *BackupFormat) Marshal() (res string, err error) {
|
||||
data, err := marshalValue(reflect.ValueOf(b))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
bytes, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
res = string(bytes)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (b *BackupFormat) Unmarshal(res string) (err error) {
|
||||
err = UnmarshalStruct([]byte(res), reflect.ValueOf(b))
|
||||
|
||||
return
|
||||
}
|
||||
|
@ -1,6 +1,9 @@
|
||||
package project
|
||||
|
||||
import (
|
||||
"github.com/ansible-semaphore/semaphore/db"
|
||||
"github.com/ansible-semaphore/semaphore/db/bolt"
|
||||
"github.com/ansible-semaphore/semaphore/util"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@ -8,6 +11,98 @@ type testItem struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
func TestBackupProject(t *testing.T) {
|
||||
util.Config = &util.ConfigType{
|
||||
TmpPath: "/tmp",
|
||||
}
|
||||
|
||||
store := bolt.CreateTestStore()
|
||||
|
||||
proj, err := store.CreateProject(db.Project{
|
||||
Name: "Test 123",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
key, err := store.CreateAccessKey(db.AccessKey{
|
||||
ProjectID: &proj.ID,
|
||||
Type: db.AccessKeyNone,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
repo, err := store.CreateRepository(db.Repository{
|
||||
ProjectID: proj.ID,
|
||||
SSHKeyID: key.ID,
|
||||
Name: "Test",
|
||||
GitURL: "git@example.com:test/test",
|
||||
GitBranch: "master",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
inv, err := store.CreateInventory(db.Inventory{
|
||||
ProjectID: proj.ID,
|
||||
ID: 1,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
env, err := store.CreateEnvironment(db.Environment{
|
||||
ProjectID: proj.ID,
|
||||
Name: "test",
|
||||
JSON: `{"author": "Denis", "comment": "Hello, World!"}`,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = store.CreateTemplate(db.Template{
|
||||
Name: "Test",
|
||||
Playbook: "test.yml",
|
||||
ProjectID: proj.ID,
|
||||
RepositoryID: repo.ID,
|
||||
InventoryID: &inv.ID,
|
||||
EnvironmentID: &env.ID,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
backup, err := GetBackup(proj.ID, store)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if backup.Meta.ID != proj.ID {
|
||||
t.Fatal("backup meta ID wrong")
|
||||
}
|
||||
|
||||
str, err := backup.Marshal()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if str != `{"environments":[{"env":null,"json":"{\"author\": \"Denis\", \"comment\": \"Hello, World!\"}","name":"test","password":null}],"inventories":[{"become_key":null,"inventory":"","name":"","ssh_key":null,"type":""}],"keys":[{"name":"","type":"none"}],"meta":{"alert":false,"alert_chat":null,"max_parallel_tasks":0,"name":"Test 123","type":""},"repositories":[{"git_branch":"master","git_url":"git@example.com:test/test","name":"Test","ssh_key":""}],"templates":[{"allow_override_args_in_task":false,"app":"","arguments":null,"autorun":false,"build_template":null,"cron":null,"description":null,"environment":"test","inventory":"","name":"Test","playbook":"test.yml","repository":"Test","start_version":null,"suppress_success_alerts":false,"survey_vars":"null","type":"","vaults":null,"view":null}],"views":null}` {
|
||||
t.Fatal("Invalid backup content")
|
||||
}
|
||||
|
||||
restoredBackup := &BackupFormat{}
|
||||
err = restoredBackup.Unmarshal(str)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if restoredBackup.Meta.ID != proj.ID {
|
||||
t.Fatal("backup meta ID wrong")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func isUnique(items []testItem) bool {
|
||||
for i, item := range items {
|
||||
for k, other := range items {
|
||||
|
Loading…
Reference in New Issue
Block a user