From 6a2cfcc3ac655c2f02a8b5ba47bb732db4ae6ff1 Mon Sep 17 00:00:00 2001 From: samerbahri98 Date: Sat, 20 Jan 2024 23:46:43 +0100 Subject: [PATCH] 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 00000000..4ece3fe6 --- /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 5822074d..e5bd06de 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 00000000..24999c4a --- /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 00000000..f0fa1f9d --- /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 00000000..b287df03 --- /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 +}