From 6a2cfcc3ac655c2f02a8b5ba47bb732db4ae6ff1 Mon Sep 17 00:00:00 2001 From: samerbahri98 Date: Sat, 20 Jan 2024 23:46:43 +0100 Subject: [PATCH 01/11] feat(api): backup --- api/projects/backupRestore.go | 39 +++++++ api/router.go | 4 +- db/BackupEntity.go | 54 +++++++++ services/project/backup.go | 214 ++++++++++++++++++++++++++++++++++ services/project/types.go | 115 ++++++++++++++++++ 5 files changed, 425 insertions(+), 1 deletion(-) create mode 100644 api/projects/backupRestore.go create mode 100644 db/BackupEntity.go create mode 100644 services/project/backup.go create mode 100644 services/project/types.go diff --git a/api/projects/backupRestore.go b/api/projects/backupRestore.go new file mode 100644 index 000000000..4ece3fe64 --- /dev/null +++ b/api/projects/backupRestore.go @@ -0,0 +1,39 @@ +package projects + +import ( + "net/http" + + log "github.com/Sirupsen/logrus" + "github.com/ansible-semaphore/semaphore/api/helpers" + "github.com/ansible-semaphore/semaphore/db" + projectService "github.com/ansible-semaphore/semaphore/services/project" + "github.com/gorilla/context" +) + +func GetBackup(w http.ResponseWriter, r *http.Request) { + project := context.Get(r, "project").(db.Project) + + store := helpers.Store(r) + + backup, err := projectService.GetBackup(project.ID, store) + + if err != nil { + helpers.WriteError(w, err) + return + } + helpers.WriteJSON(w, http.StatusOK, backup) +} + +func Restore(w http.ResponseWriter, r *http.Request) { + var backup projectService.BackupFormat + if !helpers.Bind(w, r, &backup) { + helpers.WriteJSON(w, http.StatusBadRequest, backup) + return + } + if err := projectService.Restore(backup); err != nil { + log.Error(*err) + helpers.WriteError(w, (*err)) + return + } + helpers.WriteJSON(w, http.StatusOK, nil) +} diff --git a/api/router.go b/api/router.go index 5822074d4..e5bd06de1 100644 --- a/api/router.go +++ b/api/router.go @@ -2,13 +2,13 @@ package api import ( "fmt" - "github.com/ansible-semaphore/semaphore/api/runners" "net/http" "os" "strings" "github.com/ansible-semaphore/semaphore/api/helpers" "github.com/ansible-semaphore/semaphore/api/projects" + "github.com/ansible-semaphore/semaphore/api/runners" "github.com/ansible-semaphore/semaphore/api/sockets" "github.com/ansible-semaphore/semaphore/db" "github.com/ansible-semaphore/semaphore/util" @@ -180,6 +180,8 @@ func Route() *mux.Router { projectUserAPI.Path("/views").HandlerFunc(projects.AddView).Methods("POST") projectUserAPI.Path("/views/positions").HandlerFunc(projects.SetViewPositions).Methods("POST") + projectUserAPI.Path("/backup").HandlerFunc(projects.GetBackup).Methods("GET", "HEAD") + // // Updating and deleting project projectAdminAPI := authenticatedAPI.Path("/project/{project_id}").Subrouter() diff --git a/db/BackupEntity.go b/db/BackupEntity.go new file mode 100644 index 000000000..24999c4a8 --- /dev/null +++ b/db/BackupEntity.go @@ -0,0 +1,54 @@ +package db + +type BackupEntity interface { + GetID() int + GetName() string +} + +func (e View) GetID() int { + return e.ID +} + +func (e View) GetName() string { + return e.Title +} + +func (e Template) GetID() int { + return e.ID +} + +func (e Template) GetName() string { + return e.Name +} + +func (e Inventory) GetID() int { + return e.ID +} + +func (e Inventory) GetName() string { + return e.Name +} + +func (e AccessKey) GetID() int { + return e.ID +} + +func (e AccessKey) GetName() string { + return e.Name +} + +func (e Repository) GetID() int { + return e.ID +} + +func (e Repository) GetName() string { + return e.Name +} + +func (e Environment) GetID() int { + return e.ID +} + +func (e Environment) GetName() string { + return e.Name +} diff --git a/services/project/backup.go b/services/project/backup.go new file mode 100644 index 000000000..f0fa1f9d1 --- /dev/null +++ b/services/project/backup.go @@ -0,0 +1,214 @@ +package project + +import ( + "fmt" + + "github.com/ansible-semaphore/semaphore/db" +) + +func findNameByID[T db.BackupEntity](ID int, items []T) (*string, error) { + for _, o := range items { + if o.GetID() == ID { + name := o.GetName() + return &name, nil + } + } + return nil, fmt.Errorf("item %d does not exist", ID) +} +func findEntityByName[T db.BackupEntity](name *string, items []T) *T { + if name == nil { + return nil + } + for _, o := range items { + if o.GetName() == *name { + return &o + } + } + return nil +} + +func getSchedulesByProject(projectID int, schedules []db.Schedule) []db.Schedule { + result := make([]db.Schedule, 0) + for _, o := range schedules { + if o.ProjectID == projectID { + result = append(result, o) + } + } + return result +} + +func getScheduleByTemplate(templateID int, schedules []db.Schedule) *string { + for _, o := range schedules { + if o.TemplateID == templateID { + return &o.CronFormat + } + } + return nil +} + +func (b *BackupDB) new(projectID int, store db.Store) (*BackupDB, error) { + var err error + + b.templates, err = store.GetTemplates(projectID, db.TemplateFilter{}, db.RetrieveQueryParams{}) + if err != nil { + return nil, err + } + + b.repositories, err = store.GetRepositories(projectID, db.RetrieveQueryParams{}) + if err != nil { + return nil, err + } + + b.keys, err = store.GetAccessKeys(projectID, db.RetrieveQueryParams{}) + if err != nil { + return nil, err + } + + b.views, err = store.GetViews(projectID) + if err != nil { + return nil, err + } + + b.inventories, err = store.GetInventories(projectID, db.RetrieveQueryParams{}) + if err != nil { + return nil, err + } + + b.environments, err = store.GetEnvironments(projectID, db.RetrieveQueryParams{}) + if err != nil { + return nil, err + } + schedules, err := store.GetSchedules() + if err != nil { + return nil, err + } + b.schedules = getSchedulesByProject(projectID, schedules) + b.meta, err = store.GetProject(projectID) + if err != nil { + return nil, err + } + return b, nil +} + +func (b *BackupDB) format() (*BackupFormat, error) { + keys := make([]BackupKey, len(b.keys)) + for i, o := range b.keys { + keys[i] = BackupKey{ + Name: o.Name, + Type: o.Type, + } + } + + environments := make([]BackupEnvironment, len(b.environments)) + for i, o := range b.environments { + environments[i] = BackupEnvironment{ + Name: o.Name, + ENV: o.ENV, + JSON: o.JSON, + Password: o.Password, + } + } + + inventories := make([]BackupInventory, len(b.inventories)) + for i, o := range b.inventories { + var SSHKey *string = nil + if o.SSHKeyID != nil { + SSHKey, _ = findNameByID[db.AccessKey](*o.SSHKeyID, b.keys) + } + var BecomeKey *string = nil + if o.BecomeKeyID != nil { + BecomeKey, _ = findNameByID[db.AccessKey](*o.BecomeKeyID, b.keys) + } + inventories[i] = BackupInventory{ + Name: o.Name, + Inventory: o.Inventory, + Type: o.Type, + SSHKey: SSHKey, + BecomeKey: BecomeKey, + } + } + + views := make([]BackupView, len(b.views)) + for i, o := range b.views { + views[i] = BackupView{ + Name: o.Title, + Position: o.Position, + } + } + + repositories := make([]BackupRepository, len(b.repositories)) + for i, o := range b.repositories { + SSHKey, _ := findNameByID[db.AccessKey](o.SSHKeyID, b.keys) + repositories[i] = BackupRepository{ + Name: o.Name, + SSHKey: SSHKey, + GitURL: o.GitURL, + GitBranch: o.GitBranch, + } + } + + templates := make([]BackupTemplate, len(b.templates)) + for i, o := range b.templates { + var View *string = nil + if o.ViewID != nil { + View, _ = findNameByID[db.View](*o.ViewID, b.views) + } + var VaultKey *string = nil + if o.VaultKeyID != nil { + VaultKey, _ = findNameByID[db.AccessKey](*o.VaultKeyID, b.keys) + } + var Environment *string = nil + if o.EnvironmentID != nil { + Environment, _ = findNameByID[db.Environment](*o.EnvironmentID, b.environments) + } + var BuildTemplate *string = nil + if o.BuildTemplateID != nil { + BuildTemplate, _ = findNameByID[db.Template](*o.BuildTemplateID, b.templates) + } + Repository, _ := findNameByID[db.Repository](o.RepositoryID, b.repositories) + Inventory, _ := findNameByID[db.Inventory](o.InventoryID, b.inventories) + + templates[i] = BackupTemplate{ + Name: o.Name, + AllowOverrideArgsInTask: o.AllowOverrideArgsInTask, + Arguments: o.Arguments, + Autorun: o.Autorun, + Description: o.Description, + Playbook: o.Playbook, + StartVersion: o.StartVersion, + SuppressSuccessAlerts: o.SuppressSuccessAlerts, + SurveyVars: o.SurveyVarsJSON, + Type: o.Type, + View: View, + VaultKey: VaultKey, + Repository: *Repository, + Inventory: *Inventory, + Environment: Environment, + BuildTemplate: BuildTemplate, + Cron: getScheduleByTemplate(o.ID, b.schedules), + } + } + return &BackupFormat{ + Meta: BackupMeta{ + Name: b.meta.Name, + MaxParallelTasks: b.meta.MaxParallelTasks, + Alert: b.meta.Alert, + AlertChat: b.meta.AlertChat, + }, + Inventories: inventories, + Environments: environments, + Views: views, + Repositories: repositories, + Keys: keys, + Templates: templates, + }, nil +} + +func GetBackup(projectID int, store db.Store) (*BackupFormat, error) { + backup := BackupDB{} + if _, err := backup.new(projectID, store); err != nil { + return nil, err + } + + return backup.format() +} diff --git a/services/project/types.go b/services/project/types.go new file mode 100644 index 000000000..b287df03f --- /dev/null +++ b/services/project/types.go @@ -0,0 +1,115 @@ +package project + +import ( + "github.com/ansible-semaphore/semaphore/db" +) + +type BackupDB struct { + meta db.Project + templates []db.Template + repositories []db.Repository + keys []db.AccessKey + views []db.View + inventories []db.Inventory + environments []db.Environment + schedules []db.Schedule +} + +type BackupFormat struct { + Meta BackupMeta `json:"meta"` + Templates []BackupTemplate `json:"templates"` + Repositories []BackupRepository `json:"repositories"` + Keys []BackupKey `json:"keys"` + Views []BackupView `json:"views"` + Inventories []BackupInventory `json:"inventories"` + Environments []BackupEnvironment `json:"environments"` +} + +type BackupMeta struct { + Name string `json:"name"` + Alert bool `json:"alert"` + AlertChat *string `json:"alert_chat"` + MaxParallelTasks int `json:"max_parallel_tasks"` +} + +type BackupEnvironment struct { + Name string `json:"name"` + Password *string `json:"password"` + JSON string `json:"json"` + ENV *string `json:"env"` +} + +type BackupKey struct { + Name string `json:"name"` + Type db.AccessKeyType `json:"type"` +} + +type BackupView struct { + Name string `json:"name"` + Position int `json:"position"` +} + +type BackupInventory struct { + Name string `json:"name"` + Inventory string `json:"inventory"` + SSHKey *string `json:"ssh_key"` + BecomeKey *string `json:"become_key"` + Type string `json:"type"` +} + +type BackupRepository struct { + Name string `json:"name"` + GitURL string `json:"git_url"` + GitBranch string `json:"git_branch"` + SSHKey *string `json:"ssh_key"` +} + +type BackupTemplate struct { + Inventory string `json:"inventory"` + Repository string `json:"repository"` + Environment *string `json:"environment"` + Name string `json:"name"` + Playbook string `json:"playbook"` + Arguments *string `json:"arguments"` + AllowOverrideArgsInTask bool `json:"allow_override_args_in_task"` + Description *string `json:"description"` + VaultKey *string `json:"vault_key"` + Type db.TemplateType `json:"type"` + StartVersion *string `json:"start_version"` + BuildTemplate *string `json:"build_template"` + View *string `json:"view"` + Autorun bool `json:"autorun"` + SurveyVars *string `json:"survey_vars"` + SuppressSuccessAlerts bool `json:"suppress_success_alerts"` + Cron *string `json:"cron"` +} + +type BackupEntry interface { + GetName() string + Verify(backup *BackupFormat) error + Restore(store db.Store, b *BackupDB) error +} + +func (e BackupEnvironment) GetName() string { + return e.Name +} + +func (e BackupInventory) GetName() string { + return e.Name +} + +func (e BackupKey) GetName() string { + return e.Name +} + +func (e BackupRepository) GetName() string { + return e.Name +} + +func (e BackupView) GetName() string { + return e.Name +} + +func (e BackupTemplate) GetName() string { + return e.Name +} From 84a3841c80d49fba2ee0478acdaf79255a978ac1 Mon Sep 17 00:00:00 2001 From: samerbahri98 Date: Sun, 4 Feb 2024 13:46:27 +0100 Subject: [PATCH 02/11] feat(api): restore --- api/projects/backupRestore.go | 17 +- api/router.go | 1 + db/sql/template.go | 4 + services/project/restore.go | 347 ++++++++++++++++++++++++++++++++++ 4 files changed, 365 insertions(+), 4 deletions(-) create mode 100644 services/project/restore.go diff --git a/api/projects/backupRestore.go b/api/projects/backupRestore.go index 4ece3fe64..e13059501 100644 --- a/api/projects/backupRestore.go +++ b/api/projects/backupRestore.go @@ -26,14 +26,23 @@ func GetBackup(w http.ResponseWriter, r *http.Request) { func Restore(w http.ResponseWriter, r *http.Request) { var backup projectService.BackupFormat + var p *db.Project + var err error + if !helpers.Bind(w, r, &backup) { helpers.WriteJSON(w, http.StatusBadRequest, backup) return } - if err := projectService.Restore(backup); err != nil { - log.Error(*err) - helpers.WriteError(w, (*err)) + store := helpers.Store(r) + if err = backup.Verify(); err != nil { + log.Error(err) + helpers.WriteError(w, (err)) + return + } + if p, err = backup.Restore(store); err != nil { + log.Error(err) + helpers.WriteError(w, (err)) return } - helpers.WriteJSON(w, http.StatusOK, nil) + helpers.WriteJSON(w, http.StatusOK, p) } diff --git a/api/router.go b/api/router.go index e5bd06de1..63c8cc396 100644 --- a/api/router.go +++ b/api/router.go @@ -106,6 +106,7 @@ func Route() *mux.Router { authenticatedAPI.Path("/projects").HandlerFunc(projects.GetProjects).Methods("GET", "HEAD") authenticatedAPI.Path("/projects").HandlerFunc(projects.AddProject).Methods("POST") + authenticatedAPI.Path("/projects/restore").HandlerFunc(projects.Restore).Methods("POST") authenticatedAPI.Path("/events").HandlerFunc(getAllEvents).Methods("GET", "HEAD") authenticatedAPI.HandleFunc("/events/last", getLastEvents).Methods("GET", "HEAD") diff --git a/db/sql/template.go b/db/sql/template.go index d34435877..b63568ee6 100644 --- a/db/sql/template.go +++ b/db/sql/template.go @@ -2,6 +2,7 @@ package sql import ( "database/sql" + "github.com/ansible-semaphore/semaphore/db" "github.com/masterminds/squirrel" ) @@ -114,8 +115,11 @@ func (d *SqlDb) GetTemplates(projectID int, filter db.TemplateFilter, params db. "pt.arguments", "pt.allow_override_args_in_task", "pt.vault_key_id", + "pt.build_template_id", + "pt.start_version", "pt.view_id", "pt.`app`", + "pt.survey_vars", "pt.start_version", "pt.`type`"). From("project__template pt") diff --git a/services/project/restore.go b/services/project/restore.go new file mode 100644 index 000000000..3297afca0 --- /dev/null +++ b/services/project/restore.go @@ -0,0 +1,347 @@ +package project + +import ( + "fmt" + + "github.com/ansible-semaphore/semaphore/db" +) + +func getEntryByName[T BackupEntry](name *string, items []T) *T { + if name == nil { + return nil + } + for _, o := range items { + if o.GetName() == *name { + return &o + } + } + return nil +} + +func verifyDuplicate[T BackupEntry](name string, items []T) error { + n := 0 + for _, o := range items { + if o.GetName() == name { + n++ + } + if n > 2 { + return fmt.Errorf("%s is duplicate", name) + } + } + return nil +} + +func (e BackupEnvironment) Verify(backup *BackupFormat) error { + return verifyDuplicate[BackupEnvironment](e.Name, backup.Environments) +} + +func (e BackupEnvironment) Restore(store db.Store, b *BackupDB) error { + environment, err := store.CreateEnvironment( + db.Environment{ + Name: e.Name, + Password: e.Password, + ProjectID: b.meta.ID, + JSON: e.JSON, + ENV: e.ENV, + }, + ) + if err != nil { + return err + } + b.environments = append(b.environments, environment) + return nil +} + +func (e BackupView) Verify(backup *BackupFormat) error { + return verifyDuplicate[BackupView](e.Name, backup.Views) +} + +func (e BackupView) Restore(store db.Store, b *BackupDB) error { + view, err := store.CreateView( + db.View{ + Title: e.Name, + ProjectID: b.meta.ID, + Position: e.Position, + }, + ) + if err != nil { + return err + } + b.views = append(b.views, view) + return nil +} + +func (e BackupKey) Verify(backup *BackupFormat) error { + return verifyDuplicate[BackupKey](e.Name, backup.Keys) +} + +func (e BackupKey) Restore(store db.Store, b *BackupDB) error { + key, err := store.CreateAccessKey( + db.AccessKey{ + Name: e.Name, + ProjectID: &b.meta.ID, + Type: e.Type, + }, + ) + if err != nil { + return err + } + b.keys = append(b.keys, key) + return nil +} + +func (e BackupInventory) Verify(backup *BackupFormat) error { + if err := verifyDuplicate[BackupInventory](e.Name, backup.Inventories); err != nil { + return err + } + if e.SSHKey != nil && getEntryByName[BackupKey](e.SSHKey, backup.Keys) == nil { + return fmt.Errorf("SSHKey does not exist in keys[].Name") + } + if e.BecomeKey != nil && getEntryByName[BackupKey](e.BecomeKey, backup.Keys) == nil { + return fmt.Errorf("BecomeKey does not exist in keys[].Name") + } + return nil +} + +func (e BackupInventory) Restore(store db.Store, b *BackupDB) error { + var SSHKeyID *int + if e.SSHKey == nil { + SSHKeyID = nil + } else if k := findEntityByName[db.AccessKey](e.SSHKey, b.keys); k == nil { + SSHKeyID = nil + } else { + SSHKeyID = &((*k).ID) + } + var BecomeKeyID *int + if e.BecomeKey == nil { + BecomeKeyID = nil + } else if k := findEntityByName[db.AccessKey](e.BecomeKey, b.keys); k == nil { + BecomeKeyID = nil + } else { + BecomeKeyID = &((*k).ID) + } + inventory, err := store.CreateInventory( + db.Inventory{ + ProjectID: b.meta.ID, + Name: e.Name, + Type: e.Type, + SSHKeyID: SSHKeyID, + BecomeKeyID: BecomeKeyID, + }, + ) + if err != nil { + return err + } + b.inventories = append(b.inventories, inventory) + return nil +} + +func (e BackupRepository) Verify(backup *BackupFormat) error { + if err := verifyDuplicate[BackupRepository](e.Name, backup.Repositories); err != nil { + return err + } + if e.SSHKey != nil && getEntryByName[BackupKey](e.SSHKey, backup.Keys) == nil { + return fmt.Errorf("SSHKey does not exist in keys[].Name") + } + return nil +} + +func (e BackupRepository) Restore(store db.Store, b *BackupDB) error { + var SSHKeyID int + if k := findEntityByName[db.AccessKey](e.SSHKey, b.keys); k == nil { + return fmt.Errorf("SSHKey does not exist in keys[].Name") + } else { + SSHKeyID = (*k).ID + } + repository, err := store.CreateRepository( + db.Repository{ + ProjectID: b.meta.ID, + Name: e.Name, + GitBranch: e.GitBranch, + GitURL: e.GitURL, + SSHKeyID: SSHKeyID, + }, + ) + if err != nil { + return err + } + b.repositories = append(b.repositories, repository) + return nil +} + +func (e BackupTemplate) Verify(backup *BackupFormat) error { + if err := verifyDuplicate[BackupTemplate](e.Name, backup.Templates); err != nil { + return err + } + if getEntryByName[BackupRepository](&e.Repository, backup.Repositories) == nil { + return fmt.Errorf("repository does not exist in repositories[].name") + } + if getEntryByName[BackupInventory](&e.Inventory, backup.Inventories) == nil { + return fmt.Errorf("inventory does not exist in inventories[].name") + } + if e.VaultKey != nil && getEntryByName[BackupKey](e.VaultKey, backup.Keys) == nil { + return fmt.Errorf("vault_key does not exist in keys[].name") + } + if e.View != nil && getEntryByName[BackupView](e.View, backup.Views) == nil { + return fmt.Errorf("view does not exist in views[].name") + } + if string(e.Type) == "deploy" && e.BuildTemplate == nil { + return fmt.Errorf("type is deploy but build_template is null") + } + if string(e.Type) != "deploy" && e.BuildTemplate != nil { + return fmt.Errorf("type is not deploy but build_template is not null") + } + if buildTemplate := getEntryByName[BackupTemplate](e.BuildTemplate, backup.Templates); string(e.Type) == "deploy" && buildTemplate == nil { + return fmt.Errorf("deploy is build but build_template does not exist in templates[].name") + } + return nil +} + +func (e BackupTemplate) Restore(store db.Store, b *BackupDB) error { + var InventoryID int + if k := findEntityByName[db.Inventory](&e.Inventory, b.inventories); k == nil { + return fmt.Errorf("inventory does not exist in inventories[].name") + } else { + InventoryID = k.GetID() + } + var EnvironmentID int + if k := findEntityByName[db.Environment](e.Environment, b.environments); k == nil { + return fmt.Errorf("environment does not exist in environments[].name") + } else { + EnvironmentID = k.GetID() + } + var RepositoryID int + if k := findEntityByName[db.Repository](&e.Repository, b.repositories); k == nil { + return fmt.Errorf("repository does not exist in repositories[].name") + } else { + RepositoryID = k.GetID() + } + var BuildTemplateID *int + if string(e.Type) != "deploy" { + BuildTemplateID = nil + } else if k := findEntityByName[db.Template](e.BuildTemplate, b.templates); k == nil { + BuildTemplateID = nil + } else { + BuildTemplateID = &(k.ID) + } + var ViewID *int + if k := findEntityByName[db.View](e.View, b.views); k == nil { + ViewID = nil + } else { + ViewID = &k.ID + } + template, err := store.CreateTemplate( + db.Template{ + ProjectID: b.meta.ID, + InventoryID: InventoryID, + EnvironmentID: &EnvironmentID, + RepositoryID: RepositoryID, + ViewID: ViewID, + Autorun: e.Autorun, + AllowOverrideArgsInTask: e.AllowOverrideArgsInTask, + SuppressSuccessAlerts: e.SuppressSuccessAlerts, + Name: e.Name, + Playbook: e.Playbook, + Arguments: e.Arguments, + Type: e.Type, + BuildTemplateID: BuildTemplateID, + }, + ) + if err != nil { + return err + } + b.templates = append(b.templates, template) + return nil +} + +func (backup *BackupFormat) Verify() error { + for i, o := range backup.Environments { + if err := o.Verify(backup); err != nil { + return fmt.Errorf("error at environments[%d]: %s", i, err.Error()) + } + } + for i, o := range backup.Views { + if err := o.Verify(backup); err != nil { + return fmt.Errorf("error at views[%d]: %s", i, err.Error()) + } + } + for i, o := range backup.Keys { + if err := o.Verify(backup); err != nil { + return fmt.Errorf("error at keys[%d]: %s", i, err.Error()) + } + } + for i, o := range backup.Repositories { + if err := o.Verify(backup); err != nil { + return fmt.Errorf("error at repositories[%d]: %s", i, err.Error()) + } + } + for i, o := range backup.Inventories { + if err := o.Verify(backup); err != nil { + return fmt.Errorf("error at inventories[%d]: %s", i, err.Error()) + } + } + for i, o := range backup.Templates { + if err := o.Verify(backup); err != nil { + return fmt.Errorf("error at templates[%d]: %s", i, err.Error()) + } + } + return nil +} + +func (backup *BackupFormat) Restore(store db.Store) (*db.Project, error) { + var b = BackupDB{} + project, err := store.CreateProject( + db.Project{ + Name: backup.Meta.Name, + Alert: backup.Meta.Alert, + MaxParallelTasks: backup.Meta.MaxParallelTasks, + AlertChat: b.meta.AlertChat, + }, + ) + if err != nil { + return nil, err + } + b.meta = project + for i, o := range backup.Environments { + if err := o.Restore(store, &b); err != nil { + return nil, fmt.Errorf("error at environments[%d]: %s", i, err.Error()) + } + } + for i, o := range backup.Views { + if err := o.Restore(store, &b); err != nil { + return nil, fmt.Errorf("error at views[%d]: %s", i, err.Error()) + } + } + for i, o := range backup.Keys { + if err := o.Restore(store, &b); err != nil { + return nil, fmt.Errorf("error at keys[%d]: %s", i, err.Error()) + } + } + for i, o := range backup.Repositories { + if err := o.Restore(store, &b); err != nil { + return nil, fmt.Errorf("error at repositories[%d]: %s", i, err.Error()) + } + } + for i, o := range backup.Inventories { + if err := o.Restore(store, &b); err != nil { + return nil, fmt.Errorf("error at inventories[%d]: %s", i, err.Error()) + } + } + deployTemplates := make([]int, 0) + for i, o := range backup.Templates { + if string(o.Type) == "deploy" { + deployTemplates = append(deployTemplates, i) + continue + } + if err := o.Restore(store, &b); err != nil { + return nil, fmt.Errorf("error at templates[%d]: %s", i, err.Error()) + } + } + for _, i := range deployTemplates { + o := backup.Templates[i] + if err := o.Restore(store, &b); err != nil { + return nil, fmt.Errorf("error at templates[%d]: %s", i, err.Error()) + } + } + return &project, nil +} From 1d38fd2d09e21fd6ebec8eb1a9ec42b75fe0bd2c Mon Sep 17 00:00:00 2001 From: samerbahri98 Date: Sun, 4 Feb 2024 14:50:38 +0100 Subject: [PATCH 03/11] docs(api) backup and restore --- api-docs.yml | 152 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 152 insertions(+) diff --git a/api-docs.yml b/api-docs.yml index acc75bdfe..1be890171 100644 --- a/api-docs.yml +++ b/api-docs.yml @@ -131,6 +131,128 @@ definitions: username: type: string + ProjectBackup: + type: object + properties: + meta: + type: object + properties: + name: + type: string + alert: + type: boolean + alert_chat: + type: string + max_parallel_tasks: + type: integer + templates: + type: array + items: + type: object + properties: + inventory: + type: string + repository: + type: string + environment: + type: string + view: + type: string + name: + type: string + example: Test + playbook: + type: string + example: test.yml + arguments: + type: string + example: '[]' + description: + type: string + example: Hello, World! + allow_override_args_in_task: + type: boolean + example: false + suppress_success_alerts: + type: boolean + cron: + type: string + build_template: + type: string + autorun: + type: boolean + survey_vars: + type: string + start_version: + type: string + type: + type: string + vault_key: + type: string + repositories: + type: array + items: + type: object + properties: + name: + type: string + git_url: + type: string + git_branch: + type: string + ssh_key: + type: string + keys: + type: array + items: + type: object + properties: + name: + type: string + type: + type: string + views: + type: array + items: + type: object + properties: + name: + type: string + position: + type: integer + inventories: + type: array + items: + type: object + properties: + name: + type: string + example: Test + inventory: + type: string + ssh_key: + type: string + become_key: + type: string + type: + type: string + enum: [static, static-yaml, file] + environments: + type: array + items: + type: object + properties: + name: + type: string + password: + type: string + json: + type: string + example: '{}' + env: + type: string + example: '{}' + APIToken: type: object properties: @@ -953,6 +1075,24 @@ paths: responses: 201: description: Created project + /projects/restore: + post: + tags: + - projects + summary: Restore Project + consumes: + - application/json + parameters: + - name: Backup + in: body + required: true + schema: + $ref: '#/definitions/ProjectBackup' + responses: + 200: + description: Created project + schema: + $ref: "#/definitions/Project" /events: get: @@ -1011,6 +1151,18 @@ paths: 204: description: Project deleted + /project/{project_id}/backup: + parameters: + - $ref: "#/parameters/project_id" + get: + tags: + - project + summary: Backup A Project + responses: + 200: + description: Backup + schema: + $ref: '#/definitions/ProjectBackup' /project/{project_id}/role: parameters: From 8cfdb2533787ef70ed9cd733f9ce817ab5413d0e Mon Sep 17 00:00:00 2001 From: samerbahri98 Date: Mon, 5 Feb 2024 21:18:24 +0100 Subject: [PATCH 04/11] feat(ui) backup --- web/src/lang/de.js | 2 ++ web/src/lang/en.js | 2 ++ web/src/lang/es.js | 2 ++ web/src/lang/fr.js | 2 ++ web/src/lang/it.js | 2 ++ web/src/lang/pl.js | 2 ++ web/src/lang/pt.js | 2 ++ web/src/lang/pt_br.js | 2 ++ web/src/lang/ru.js | 2 ++ web/src/lang/zh_hans.js | 2 ++ web/src/lang/zh_hant.js | 2 ++ web/src/views/project/Settings.vue | 51 ++++++++++++++++++++++++------ 12 files changed, 64 insertions(+), 9 deletions(-) diff --git a/web/src/lang/de.js b/web/src/lang/de.js index 0e576e527..1dcbcb4eb 100644 --- a/web/src/lang/de.js +++ b/web/src/lang/de.js @@ -1,4 +1,6 @@ export default { + backup: 'Sicherung', + downloadTheProjectBackupFile: 'Laden Sie die Projektsicherungsdatei (in JSON) herunter.', incorrectUsrPwd: 'Falscher Benutzername oder falsches Passwort', askDeleteUser: 'Soll dieser Benutzer gelöscht werden?', askDeleteTemp: 'Soll diese Vorlage gelöscht werden?', diff --git a/web/src/lang/en.js b/web/src/lang/en.js index 16588d94c..4423267eb 100644 --- a/web/src/lang/en.js +++ b/web/src/lang/en.js @@ -1,4 +1,6 @@ export default { + backup: 'Backup', + downloadTheProjectBackupFile: 'Download the project backup file (in json)', incorrectUsrPwd: 'Incorrect login or password', askDeleteUser: 'Do you really want to delete this user?', askDeleteTemp: 'Do you really want to delete this template?', diff --git a/web/src/lang/es.js b/web/src/lang/es.js index d17e26356..11dfbb3cf 100644 --- a/web/src/lang/es.js +++ b/web/src/lang/es.js @@ -1,4 +1,6 @@ export default { + backup: 'Respaldo', + downloadTheProjectBackupFile: 'Descargue el archivo de copia de seguridad del proyecto (en json)', incorrectUsrPwd: 'Usuario o contraseña incorrecta', askDeleteUser: '¿Realmente desea eliminar este usuario?', askDeleteTemp: '¿Realmente desea eliminar esta plantilla?', diff --git a/web/src/lang/fr.js b/web/src/lang/fr.js index 197aef9fe..4cd6e299d 100644 --- a/web/src/lang/fr.js +++ b/web/src/lang/fr.js @@ -1,4 +1,6 @@ export default { + backup: 'Sauvegarde', + downloadTheProjectBackupFile: 'Téléchargez le fichier de sauvegarde du projet (en json)', incorrectUsrPwd: 'Identifiant ou mot de passe incorrect', askDeleteUser: 'Voulez-vous vraiment supprimer cet utilisateur ?', askDeleteTemp: 'Voulez-vous vraiment supprimer ce modèle ?', diff --git a/web/src/lang/it.js b/web/src/lang/it.js index 2d520920c..50acbbe97 100644 --- a/web/src/lang/it.js +++ b/web/src/lang/it.js @@ -1,4 +1,6 @@ export default { + backup: 'backup', + downloadTheProjectBackupFile: 'Scarica il file di backup del progetto (in json)', incorrectUsrPwd: 'Nome utente o password errati', askDeleteUser: 'Vuoi davvero eliminare questo utente?', askDeleteTemp: 'Vuoi davvero eliminare questo modello?', diff --git a/web/src/lang/pl.js b/web/src/lang/pl.js index 7e56bc636..d1a9d95f2 100644 --- a/web/src/lang/pl.js +++ b/web/src/lang/pl.js @@ -1,4 +1,6 @@ export default { + backup: 'Kopia zapasowa', + downloadTheProjectBackupFile: 'Pobierz plik kopii zapasowej projektu (w formacie json)', incorrectUsrPwd: 'Nieprawidłowa nazwa użytkownika lub hasło.', askDeleteUser: 'Czy na pewno chcesz usunąć tego użytkownika?', askDeleteTemp: 'Czy na pewno chcesz usunąć ten szablon?', diff --git a/web/src/lang/pt.js b/web/src/lang/pt.js index 7769cb0f5..c8909dee3 100644 --- a/web/src/lang/pt.js +++ b/web/src/lang/pt.js @@ -1,4 +1,6 @@ export default { + backup: 'Cópia de segurança', + downloadTheProjectBackupFile: 'Baixe o arquivo de backup do projeto (em json)', incorrectUsrPwd: 'Nome de utilizador ou palavra-passe incorretos', askDeleteUser: 'Tem a certeza de que deseja eliminar este utilizador?', askDeleteTemp: 'Tem a certeza de que deseja eliminar este modelo?', diff --git a/web/src/lang/pt_br.js b/web/src/lang/pt_br.js index 6bdd6d5d9..193578f5f 100644 --- a/web/src/lang/pt_br.js +++ b/web/src/lang/pt_br.js @@ -1,4 +1,6 @@ export default { + backup: 'Cópia de segurança', + downloadTheProjectBackupFile: 'Baixe o arquivo de backup do projeto (em json)', incorrectUsrPwd: 'Usuário ou senha incorretos', askDeleteUser: 'Você realmente deseja excluir este usuário?', askDeleteTemp: 'Você realmente deseja excluir este modelo?', diff --git a/web/src/lang/ru.js b/web/src/lang/ru.js index 32b67eb2e..571284e55 100644 --- a/web/src/lang/ru.js +++ b/web/src/lang/ru.js @@ -1,4 +1,6 @@ export default { + backup: 'Бэкап', + downloadTheProjectBackupFile: 'Загрузите файл резервной копии проекта (в формате JSON)', incorrectUsrPwd: 'Некорректный логин или пароль', askDeleteUser: 'Вы действительно хотите удалить этого пользователя?', askDeleteTemp: 'Вы действительно хотите удалить этот шаблон?', diff --git a/web/src/lang/zh_hans.js b/web/src/lang/zh_hans.js index 4bfe9d2c8..389dd47c6 100644 --- a/web/src/lang/zh_hans.js +++ b/web/src/lang/zh_hans.js @@ -1,4 +1,6 @@ export default { + backup: '备份', + downloadTheProjectBackupFile: '下载项目备份文件(json格式)', incorrectUsrPwd: '用户名或密码错误', askDeleteUser: '您确定要删除此用户吗?', askDeleteTemp: '您确实要删除此模板吗?', diff --git a/web/src/lang/zh_hant.js b/web/src/lang/zh_hant.js index 89f388fc1..f633fc3b4 100644 --- a/web/src/lang/zh_hant.js +++ b/web/src/lang/zh_hant.js @@ -1,4 +1,6 @@ export default { + backup: '備份', + downloadTheProjectBackupFile: '下載專案備份檔(json格式)', incorrectUsrPwd: '使用者名稱或密碼錯誤', askDeleteUser: '您確定要刪除此使用者嗎? ', askDeleteTemp: '您確實要刪除此範本嗎? ', diff --git a/web/src/views/project/Settings.vue b/web/src/views/project/Settings.vue index c45570ab7..5a4d72582 100644 --- a/web/src/views/project/Settings.vue +++ b/web/src/views/project/Settings.vue @@ -27,8 +27,20 @@ {{ $t('save') }} - -
+
+ + + {{ $t('backup') }} + + + +
+ {{ $t('downloadTheProjectBackupFile') }} +
+
+
+
+
{{ $t('deleteProject2') }} @@ -44,14 +56,13 @@