diff --git a/api/dataservices/interface.go b/api/dataservices/interface.go index e6e071afe..6fbfb218d 100644 --- a/api/dataservices/interface.go +++ b/api/dataservices/interface.go @@ -40,6 +40,7 @@ type ( Role() RoleService APIKeyRepository() APIKeyRepository Settings() SettingsService + Snapshot() SnapshotService SSLSettings() SSLSettingsService Stack() StackService Tag() TagService @@ -214,6 +215,15 @@ type ( BucketName() string } + SnapshotService interface { + Snapshot(endpointID portainer.EndpointID) (*portainer.Snapshot, error) + Snapshots() ([]portainer.Snapshot, error) + UpdateSnapshot(snapshot *portainer.Snapshot) error + DeleteSnapshot(endpointID portainer.EndpointID) error + Create(snapshot *portainer.Snapshot) error + BucketName() string + } + // SSLSettingsService represents a service for managing application settings SSLSettingsService interface { Settings() (*portainer.SSLSettings, error) diff --git a/api/dataservices/snapshot/snapshot.go b/api/dataservices/snapshot/snapshot.go new file mode 100644 index 000000000..ab5909d26 --- /dev/null +++ b/api/dataservices/snapshot/snapshot.go @@ -0,0 +1,76 @@ +package snapshot + +import ( + "fmt" + + portainer "github.com/portainer/portainer/api" + "github.com/sirupsen/logrus" +) + +const ( + BucketName = "snapshots" +) + +type Service struct { + connection portainer.Connection +} + +func (service *Service) BucketName() string { + return BucketName +} + +func NewService(connection portainer.Connection) (*Service, error) { + err := connection.SetServiceName(BucketName) + if err != nil { + return nil, err + } + + return &Service{ + connection: connection, + }, nil +} + +func (service *Service) Snapshot(endpointID portainer.EndpointID) (*portainer.Snapshot, error) { + var snapshot portainer.Snapshot + identifier := service.connection.ConvertToKey(int(endpointID)) + + err := service.connection.GetObject(BucketName, identifier, &snapshot) + if err != nil { + return nil, err + } + + return &snapshot, nil +} + +func (service *Service) Snapshots() ([]portainer.Snapshot, error) { + var snapshots = make([]portainer.Snapshot, 0) + + err := service.connection.GetAllWithJsoniter( + BucketName, + &portainer.Snapshot{}, + func(obj interface{}) (interface{}, error) { + snapshot, ok := obj.(*portainer.Snapshot) + if !ok { + logrus.WithField("obj", obj).Errorf("Failed to convert to Snapshot object") + return nil, fmt.Errorf("failed to convert to Snapshot object: %s", obj) + } + snapshots = append(snapshots, *snapshot) + return &portainer.Snapshot{}, nil + }) + + return snapshots, err +} + +func (service *Service) UpdateSnapshot(snapshot *portainer.Snapshot) error { + identifier := service.connection.ConvertToKey(int(snapshot.EndpointID)) + return service.connection.UpdateObject(BucketName, identifier, snapshot) +} + +func (service *Service) DeleteSnapshot(endpointID portainer.EndpointID) error { + identifier := service.connection.ConvertToKey(int(endpointID)) + return service.connection.DeleteObject(BucketName, identifier) +} + +func (service *Service) Create(snapshot *portainer.Snapshot) error { + return service.connection.CreateObjectWithId(BucketName, int(snapshot.EndpointID), snapshot) +} diff --git a/api/datastore/migrate_data.go b/api/datastore/migrate_data.go index 3292b8273..2cac19ee4 100644 --- a/api/datastore/migrate_data.go +++ b/api/datastore/migrate_data.go @@ -40,6 +40,7 @@ func (store *Store) MigrateData() error { RoleService: store.RoleService, ScheduleService: store.ScheduleService, SettingsService: store.SettingsService, + SnapshotService: store.SnapshotService, StackService: store.StackService, TagService: store.TagService, TeamMembershipService: store.TeamMembershipService, diff --git a/api/datastore/migrator/migrate_ce.go b/api/datastore/migrator/migrate_ce.go index 07e4d67f7..01514e35f 100644 --- a/api/datastore/migrator/migrate_ce.go +++ b/api/datastore/migrator/migrate_ce.go @@ -108,6 +108,9 @@ func (m *Migrator) Migrate() error { // Portainer 2.15 newMigration(60, m.migrateDBVersionToDB60), + + // Portainer 2.16 + newMigration(70, m.migrateDBVersionToDB70), } var lastDbVersion int diff --git a/api/datastore/migrator/migrate_dbversion70.go b/api/datastore/migrator/migrate_dbversion70.go new file mode 100644 index 000000000..28ceb6f5a --- /dev/null +++ b/api/datastore/migrator/migrate_dbversion70.go @@ -0,0 +1,46 @@ +package migrator + +import ( + portainer "github.com/portainer/portainer/api" +) + +func (m *Migrator) migrateDBVersionToDB70() error { + // foreach endpoint + endpoints, err := m.endpointService.Endpoints() + if err != nil { + return err + } + + for _, endpoint := range endpoints { + // copy snapshots to new object + migrateLog.Info("- moving snapshots from endpoint to new object") + snapshot := portainer.Snapshot{EndpointID: endpoint.ID} + + if len(endpoint.Snapshots) > 0 { + snapshot.Docker = &endpoint.Snapshots[len(endpoint.Snapshots)-1] + } + + if len(endpoint.Kubernetes.Snapshots) > 0 { + snapshot.Kubernetes = &endpoint.Kubernetes.Snapshots[len(endpoint.Kubernetes.Snapshots)-1] + } + + // save new object + err = m.snapshotService.Create(&snapshot) + if err != nil { + return err + } + + // set to nil old fields + migrateLog.Info("- deleting snapshot from endpoint") + endpoint.Snapshots = []portainer.DockerSnapshot{} + endpoint.Kubernetes.Snapshots = []portainer.KubernetesSnapshot{} + + // update endpoint + err = m.endpointService.UpdateEndpoint(endpoint.ID, &endpoint) + if err != nil { + return err + } + } + + return nil +} diff --git a/api/datastore/migrator/migrator.go b/api/datastore/migrator/migrator.go index 22431b612..aae3821cd 100644 --- a/api/datastore/migrator/migrator.go +++ b/api/datastore/migrator/migrator.go @@ -13,6 +13,7 @@ import ( "github.com/portainer/portainer/api/dataservices/role" "github.com/portainer/portainer/api/dataservices/schedule" "github.com/portainer/portainer/api/dataservices/settings" + "github.com/portainer/portainer/api/dataservices/snapshot" "github.com/portainer/portainer/api/dataservices/stack" "github.com/portainer/portainer/api/dataservices/tag" "github.com/portainer/portainer/api/dataservices/teammembership" @@ -35,6 +36,7 @@ type ( roleService *role.Service scheduleService *schedule.Service settingsService *settings.Service + snapshotService *snapshot.Service stackService *stack.Service tagService *tag.Service teamMembershipService *teammembership.Service @@ -58,6 +60,7 @@ type ( RoleService *role.Service ScheduleService *schedule.Service SettingsService *settings.Service + SnapshotService *snapshot.Service StackService *stack.Service TagService *tag.Service TeamMembershipService *teammembership.Service @@ -83,6 +86,7 @@ func NewMigrator(parameters *MigratorParameters) *Migrator { roleService: parameters.RoleService, scheduleService: parameters.ScheduleService, settingsService: parameters.SettingsService, + snapshotService: parameters.SnapshotService, tagService: parameters.TagService, teamMembershipService: parameters.TeamMembershipService, stackService: parameters.StackService, diff --git a/api/datastore/services.go b/api/datastore/services.go index 2350dac31..d3ed2ee0c 100644 --- a/api/datastore/services.go +++ b/api/datastore/services.go @@ -26,6 +26,7 @@ import ( "github.com/portainer/portainer/api/dataservices/role" "github.com/portainer/portainer/api/dataservices/schedule" "github.com/portainer/portainer/api/dataservices/settings" + "github.com/portainer/portainer/api/dataservices/snapshot" "github.com/portainer/portainer/api/dataservices/ssl" "github.com/portainer/portainer/api/dataservices/stack" "github.com/portainer/portainer/api/dataservices/tag" @@ -63,6 +64,7 @@ type Store struct { APIKeyRepositoryService *apikeyrepository.Service ScheduleService *schedule.Service SettingsService *settings.Service + SnapshotService *snapshot.Service SSLSettingsService *ssl.Service StackService *stack.Service TagService *tag.Service @@ -171,6 +173,12 @@ func (store *Store) initServices() error { } store.SettingsService = settingsService + snapshotService, err := snapshot.NewService(store.connection) + if err != nil { + return err + } + store.SnapshotService = snapshotService + sslSettingsService, err := ssl.NewService(store.connection) if err != nil { return err @@ -315,6 +323,10 @@ func (store *Store) Settings() dataservices.SettingsService { return store.SettingsService } +func (store *Store) Snapshot() dataservices.SnapshotService { + return store.SnapshotService +} + // SSLSettings gives access to the SSL Settings data management layer func (store *Store) SSLSettings() dataservices.SSLSettingsService { return store.SSLSettingsService @@ -375,6 +387,7 @@ type storeExport struct { Role []portainer.Role `json:"roles,omitempty"` Schedules []portainer.Schedule `json:"schedules,omitempty"` Settings portainer.Settings `json:"settings,omitempty"` + Snapshot []portainer.Snapshot `json:"snapshots,omitempty"` SSLSettings portainer.SSLSettings `json:"ssl,omitempty"` Stack []portainer.Stack `json:"stacks,omitempty"` Tag []portainer.Tag `json:"tags,omitempty"` @@ -503,6 +516,14 @@ func (store *Store) Export(filename string) (err error) { backup.Settings = *settings } + if snapshot, err := store.Snapshot().Snapshots(); err != nil { + if !store.IsErrObjectNotFound(err) { + logrus.WithError(err).Errorf("Exporting Snapshots") + } + } else { + backup.Snapshot = snapshot + } + if settings, err := store.SSLSettings().Settings(); err != nil { if !store.IsErrObjectNotFound(err) { log.Error().Err(err).Msg("exporting SSL Settings") @@ -662,6 +683,10 @@ func (store *Store) Import(filename string) (err error) { store.Settings().UpdateSettings(&backup.Settings) store.SSLSettings().UpdateSettings(&backup.SSLSettings) + for _, v := range backup.Snapshot { + store.Snapshot().UpdateSnapshot(&v) + } + for _, v := range backup.Stack { store.Stack().UpdateStack(v.ID, &v) } diff --git a/api/datastore/test_data/output_24_to_latest.json b/api/datastore/test_data/output_24_to_latest.json index f491e5d8a..fa041676d 100644 --- a/api/datastore/test_data/output_24_to_latest.json +++ b/api/datastore/test_data/output_24_to_latest.json @@ -60,7 +60,7 @@ "UseLoadBalancer": false, "UseServerMetrics": false }, - "Snapshots": null + "Snapshots": [] }, "LastCheckInDate": 0, "Name": "local", @@ -77,127 +77,7 @@ "allowVolumeBrowserForRegularUsers": false, "enableHostManagementFeatures": false }, - "Snapshots": [ - { - "DockerSnapshotRaw": { - "Containers": null, - "Images": null, - "Info": { - "Architecture": "", - "BridgeNfIp6tables": false, - "BridgeNfIptables": false, - "CPUSet": false, - "CPUShares": false, - "CgroupDriver": "", - "ContainerdCommit": { - "Expected": "", - "ID": "" - }, - "Containers": 0, - "ContainersPaused": 0, - "ContainersRunning": 0, - "ContainersStopped": 0, - "CpuCfsPeriod": false, - "CpuCfsQuota": false, - "Debug": false, - "DefaultRuntime": "", - "DockerRootDir": "", - "Driver": "", - "DriverStatus": null, - "ExperimentalBuild": false, - "GenericResources": null, - "HttpProxy": "", - "HttpsProxy": "", - "ID": "", - "IPv4Forwarding": false, - "Images": 0, - "IndexServerAddress": "", - "InitBinary": "", - "InitCommit": { - "Expected": "", - "ID": "" - }, - "Isolation": "", - "KernelMemory": false, - "KernelMemoryTCP": false, - "KernelVersion": "", - "Labels": null, - "LiveRestoreEnabled": false, - "LoggingDriver": "", - "MemTotal": 0, - "MemoryLimit": false, - "NCPU": 0, - "NEventsListener": 0, - "NFd": 0, - "NGoroutines": 0, - "Name": "", - "NoProxy": "", - "OSType": "", - "OSVersion": "", - "OomKillDisable": false, - "OperatingSystem": "", - "PidsLimit": false, - "Plugins": { - "Authorization": null, - "Log": null, - "Network": null, - "Volume": null - }, - "RegistryConfig": null, - "RuncCommit": { - "Expected": "", - "ID": "" - }, - "Runtimes": null, - "SecurityOptions": null, - "ServerVersion": "", - "SwapLimit": false, - "Swarm": { - "ControlAvailable": false, - "Error": "", - "LocalNodeState": "", - "NodeAddr": "", - "NodeID": "", - "RemoteManagers": null - }, - "SystemTime": "", - "Warnings": null - }, - "Networks": null, - "Version": { - "ApiVersion": "", - "Arch": "", - "GitCommit": "", - "GoVersion": "", - "Os": "", - "Platform": { - "Name": "" - }, - "Version": "" - }, - "Volumes": { - "Volumes": null, - "Warnings": null - } - }, - "DockerVersion": "20.10.13", - "GpuUseAll": false, - "GpuUseList": null, - "HealthyContainerCount": 0, - "ImageCount": 9, - "NodeCount": 0, - "RunningContainerCount": 5, - "ServiceCount": 0, - "StackCount": 2, - "StoppedContainerCount": 0, - "Swarm": false, - "Time": 1648610112, - "TotalCPU": 8, - "TotalMemory": 25098706944, - "UnhealthyContainerCount": 0, - "VolumeCount": 10 - } - ], + "Snapshots": [], "Status": 1, "TLSConfig": { "TLS": false, @@ -777,6 +657,131 @@ "mpsUser": "" } }, + "snapshots": [ + { + "Docker": { + "DockerSnapshotRaw": { + "Containers": null, + "Images": null, + "Info": { + "Architecture": "", + "BridgeNfIp6tables": false, + "BridgeNfIptables": false, + "CPUSet": false, + "CPUShares": false, + "CgroupDriver": "", + "ContainerdCommit": { + "Expected": "", + "ID": "" + }, + "Containers": 0, + "ContainersPaused": 0, + "ContainersRunning": 0, + "ContainersStopped": 0, + "CpuCfsPeriod": false, + "CpuCfsQuota": false, + "Debug": false, + "DefaultRuntime": "", + "DockerRootDir": "", + "Driver": "", + "DriverStatus": null, + "ExperimentalBuild": false, + "GenericResources": null, + "HttpProxy": "", + "HttpsProxy": "", + "ID": "", + "IPv4Forwarding": false, + "Images": 0, + "IndexServerAddress": "", + "InitBinary": "", + "InitCommit": { + "Expected": "", + "ID": "" + }, + "Isolation": "", + "KernelMemory": false, + "KernelMemoryTCP": false, + "KernelVersion": "", + "Labels": null, + "LiveRestoreEnabled": false, + "LoggingDriver": "", + "MemTotal": 0, + "MemoryLimit": false, + "NCPU": 0, + "NEventsListener": 0, + "NFd": 0, + "NGoroutines": 0, + "Name": "", + "NoProxy": "", + "OSType": "", + "OSVersion": "", + "OomKillDisable": false, + "OperatingSystem": "", + "PidsLimit": false, + "Plugins": { + "Authorization": null, + "Log": null, + "Network": null, + "Volume": null + }, + "RegistryConfig": null, + "RuncCommit": { + "Expected": "", + "ID": "" + }, + "Runtimes": null, + "SecurityOptions": null, + "ServerVersion": "", + "SwapLimit": false, + "Swarm": { + "ControlAvailable": false, + "Error": "", + "LocalNodeState": "", + "NodeAddr": "", + "NodeID": "", + "RemoteManagers": null + }, + "SystemTime": "", + "Warnings": null + }, + "Networks": null, + "Version": { + "ApiVersion": "", + "Arch": "", + "GitCommit": "", + "GoVersion": "", + "Os": "", + "Platform": { + "Name": "" + }, + "Version": "" + }, + "Volumes": { + "Volumes": null, + "Warnings": null + } + }, + "DockerVersion": "20.10.13", + "GpuUseAll": false, + "GpuUseList": null, + "HealthyContainerCount": 0, + "ImageCount": 9, + "NodeCount": 0, + "RunningContainerCount": 5, + "ServiceCount": 0, + "StackCount": 2, + "StoppedContainerCount": 0, + "Swarm": false, + "Time": 1648610112, + "TotalCPU": 8, + "TotalMemory": 25098706944, + "UnhealthyContainerCount": 0, + "VolumeCount": 10 + }, + "EndpointId": 1, + "Kubernetes": null + } + ], "ssl": { "certPath": "", "httpEnabled": true, diff --git a/api/demo/init.go b/api/demo/init.go index a27b6baa7..da4090bb6 100644 --- a/api/demo/init.go +++ b/api/demo/init.go @@ -60,6 +60,15 @@ func initDemoLocalEndpoint(store dataservices.DataStore) (portainer.EndpointID, } err := store.Endpoint().Create(localEndpoint) + if err != nil { + return id, errors.WithMessage(err, "failed creating local endpoint") + } + + err = store.Snapshot().Create(&portainer.Snapshot{EndpointID: id}) + if err != nil { + return id, errors.WithMessage(err, "failed creating snapshot") + } + return id, errors.WithMessage(err, "failed creating local endpoint") } diff --git a/api/http/handler/endpoints/endpoint_inspect.go b/api/http/handler/endpoints/endpoint_inspect.go index 2c7dc27d5..2d728b1bd 100644 --- a/api/http/handler/endpoints/endpoint_inspect.go +++ b/api/http/handler/endpoints/endpoint_inspect.go @@ -44,5 +44,18 @@ func (handler *Handler) endpointInspect(w http.ResponseWriter, r *http.Request) hideFields(endpoint) endpoint.ComposeSyntaxMaxVersion = handler.ComposeStackManager.ComposeSyntaxMaxVersion() + if !excludeSnapshot(r) { + err = handler.SnapshotService.FillSnapshotData(endpoint) + if err != nil { + return httperror.InternalServerError("Unable to add snapshot data", err) + } + } + return response.JSON(w, endpoint) } + +func excludeSnapshot(r *http.Request) bool { + excludeSnapshot, _ := request.RetrieveBooleanQueryParameter(r, "excludeSnapshot", true) + + return excludeSnapshot +} diff --git a/api/http/handler/endpoints/endpoint_list.go b/api/http/handler/endpoints/endpoint_list.go index a16ab228c..73de737f0 100644 --- a/api/http/handler/endpoints/endpoint_list.go +++ b/api/http/handler/endpoints/endpoint_list.go @@ -103,6 +103,12 @@ func (handler *Handler) endpointList(w http.ResponseWriter, r *http.Request) *ht paginatedEndpoints[idx].EdgeCheckinInterval = settings.EdgeAgentCheckinInterval } paginatedEndpoints[idx].QueryDate = time.Now().Unix() + if !query.excludeSnapshots { + err = handler.SnapshotService.FillSnapshotData(&paginatedEndpoints[idx]) + if err != nil { + return httperror.InternalServerError("Unable to add snapshot data", err) + } + } } w.Header().Set("X-Total-Count", strconv.Itoa(filteredEndpointCount)) diff --git a/api/http/handler/endpoints/endpoint_list_test.go b/api/http/handler/endpoints/endpoint_list_test.go index 3fff08851..a3b5ad174 100644 --- a/api/http/handler/endpoints/endpoint_list_test.go +++ b/api/http/handler/endpoints/endpoint_list_test.go @@ -11,6 +11,7 @@ import ( portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/datastore" "github.com/portainer/portainer/api/http/security" + "github.com/portainer/portainer/api/internal/snapshot" "github.com/portainer/portainer/api/internal/testhelpers" helper "github.com/portainer/portainer/api/internal/testhelpers" "github.com/stretchr/testify/assert" @@ -203,6 +204,8 @@ func setup(t *testing.T, endpoints []portainer.Endpoint) (handler *Handler, tear handler.DataStore = store handler.ComposeStackManager = testhelpers.NewComposeStackManager() + handler.SnapshotService, _ = snapshot.NewService("1s", store, nil, nil, nil) + return handler, teardown } diff --git a/api/http/handler/endpoints/endpoint_snapshot.go b/api/http/handler/endpoints/endpoint_snapshot.go index 5224c68e9..fa44a43e5 100644 --- a/api/http/handler/endpoints/endpoint_snapshot.go +++ b/api/http/handler/endpoints/endpoint_snapshot.go @@ -53,8 +53,6 @@ func (handler *Handler) endpointSnapshot(w http.ResponseWriter, r *http.Request) latestEndpointReference.Status = portainer.EndpointStatusDown } - latestEndpointReference.Snapshots = endpoint.Snapshots - latestEndpointReference.Kubernetes.Snapshots = endpoint.Kubernetes.Snapshots latestEndpointReference.Agent.Version = endpoint.Agent.Version err = handler.DataStore.Endpoint().UpdateEndpoint(latestEndpointReference.ID, latestEndpointReference) diff --git a/api/http/handler/endpoints/endpoint_snapshots.go b/api/http/handler/endpoints/endpoint_snapshots.go index 5af529a85..4b86bb8ca 100644 --- a/api/http/handler/endpoints/endpoint_snapshots.go +++ b/api/http/handler/endpoints/endpoint_snapshots.go @@ -56,8 +56,6 @@ func (handler *Handler) endpointSnapshots(w http.ResponseWriter, r *http.Request endpoint.Status = portainer.EndpointStatusDown } - latestEndpointReference.Snapshots = endpoint.Snapshots - latestEndpointReference.Kubernetes.Snapshots = endpoint.Kubernetes.Snapshots latestEndpointReference.Agent.Version = endpoint.Agent.Version err = handler.DataStore.Endpoint().UpdateEndpoint(latestEndpointReference.ID, latestEndpointReference) diff --git a/api/http/handler/endpoints/endpoint_update.go b/api/http/handler/endpoints/endpoint_update.go index b9bce5383..d34d33428 100644 --- a/api/http/handler/endpoints/endpoint_update.go +++ b/api/http/handler/endpoints/endpoint_update.go @@ -329,5 +329,10 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) * } } + err = handler.SnapshotService.FillSnapshotData(endpoint) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to add snapshot data", err} + } + return response.JSON(w, endpoint) } diff --git a/api/http/handler/endpoints/filter.go b/api/http/handler/endpoints/filter.go index 0ce18764b..7fb787185 100644 --- a/api/http/handler/endpoints/filter.go +++ b/api/http/handler/endpoints/filter.go @@ -24,6 +24,7 @@ type EnvironmentsQuery struct { status []portainer.EndpointStatus edgeDevice *bool edgeDeviceUntrusted bool + excludeSnapshots bool name string agentVersions []string } @@ -74,6 +75,8 @@ func parseQuery(r *http.Request) (EnvironmentsQuery, error) { edgeDeviceUntrusted, _ := request.RetrieveBooleanQueryParameter(r, "edgeDeviceUntrusted", true) + excludeSnapshots, _ := request.RetrieveBooleanQueryParameter(r, "excludeSnapshots", true) + return EnvironmentsQuery{ search: search, types: endpointTypes, @@ -84,6 +87,7 @@ func parseQuery(r *http.Request) (EnvironmentsQuery, error) { status: status, edgeDevice: edgeDevice, edgeDeviceUntrusted: edgeDeviceUntrusted, + excludeSnapshots: excludeSnapshots, name: name, agentVersions: agentVersions, }, nil diff --git a/api/http/proxy/factory/docker.go b/api/http/proxy/factory/docker.go index 0b43f59da..e916aa449 100644 --- a/api/http/proxy/factory/docker.go +++ b/api/http/proxy/factory/docker.go @@ -57,6 +57,15 @@ func (factory *ProxyFactory) newDockerHTTPProxy(endpoint *portainer.Endpoint) (h endpointURL.Scheme = "https" } + snapshot, err := factory.dataStore.Snapshot().Snapshot(endpoint.ID) + if err != nil { + return nil, err + } + + if snapshot.Docker != nil { + endpoint.Snapshots = []portainer.DockerSnapshot{*snapshot.Docker} + } + transportParameters := &docker.TransportParameters{ Endpoint: endpoint, DataStore: factory.dataStore, diff --git a/api/internal/snapshot/snapshot.go b/api/internal/snapshot/snapshot.go index 4216aa3fb..55f8eadec 100644 --- a/api/internal/snapshot/snapshot.go +++ b/api/internal/snapshot/snapshot.go @@ -119,27 +119,59 @@ func (service *Service) SnapshotEndpoint(endpoint *portainer.Endpoint) error { return service.snapshotDockerEndpoint(endpoint) } -func (service *Service) snapshotKubernetesEndpoint(endpoint *portainer.Endpoint) error { - snapshot, err := service.kubernetesSnapshotter.CreateSnapshot(endpoint) +func (service *Service) Create(snapshot portainer.Snapshot) error { + return service.dataStore.Snapshot().Create(&snapshot) +} + +func (service *Service) FillSnapshotData(endpoint *portainer.Endpoint) error { + snapshot, err := service.dataStore.Snapshot().Snapshot(endpoint.ID) + if service.dataStore.IsErrObjectNotFound(err) { + endpoint.Snapshots = []portainer.DockerSnapshot{} + endpoint.Kubernetes.Snapshots = []portainer.KubernetesSnapshot{} + + return nil + } + if err != nil { return err } - if snapshot != nil { - endpoint.Kubernetes.Snapshots = []portainer.KubernetesSnapshot{*snapshot} + if snapshot.Docker != nil { + endpoint.Snapshots = []portainer.DockerSnapshot{*snapshot.Docker} + } + + if snapshot.Kubernetes != nil { + endpoint.Kubernetes.Snapshots = []portainer.KubernetesSnapshot{*snapshot.Kubernetes} + } + + return nil +} + +func (service *Service) snapshotKubernetesEndpoint(endpoint *portainer.Endpoint) error { + kubernetesSnapshot, err := service.kubernetesSnapshotter.CreateSnapshot(endpoint) + if err != nil { + return err + } + + if kubernetesSnapshot != nil { + snapshot := &portainer.Snapshot{EndpointID: endpoint.ID, Kubernetes: kubernetesSnapshot} + + return service.dataStore.Snapshot().Create(snapshot) } return nil } func (service *Service) snapshotDockerEndpoint(endpoint *portainer.Endpoint) error { - snapshot, err := service.dockerSnapshotter.CreateSnapshot(endpoint) + dockerSnapshot, err := service.dockerSnapshotter.CreateSnapshot(endpoint) if err != nil { return err } - if snapshot != nil { - endpoint.Snapshots = []portainer.DockerSnapshot{*snapshot} + if dockerSnapshot != nil { + snapshot := &portainer.Snapshot{EndpointID: endpoint.ID, Docker: dockerSnapshot} + + return service.dataStore.Snapshot().Create(snapshot) } return nil @@ -203,8 +235,6 @@ func (service *Service) snapshotEndpoints() error { latestEndpointReference.Status = portainer.EndpointStatusDown } - latestEndpointReference.Snapshots = endpoint.Snapshots - latestEndpointReference.Kubernetes.Snapshots = endpoint.Kubernetes.Snapshots latestEndpointReference.Agent.Version = endpoint.Agent.Version err = service.dataStore.Endpoint().UpdateEndpoint(latestEndpointReference.ID, latestEndpointReference) diff --git a/api/internal/testhelpers/datastore.go b/api/internal/testhelpers/datastore.go index 642a03c1e..642aaa1d4 100644 --- a/api/internal/testhelpers/datastore.go +++ b/api/internal/testhelpers/datastore.go @@ -25,6 +25,7 @@ type testDatastore struct { role dataservices.RoleService sslSettings dataservices.SSLSettingsService settings dataservices.SettingsService + snapshot dataservices.SnapshotService stack dataservices.StackService tag dataservices.TagService teamMembership dataservices.TeamMembershipService @@ -69,6 +70,7 @@ func (d *testDatastore) APIKeyRepository() dataservices.APIKeyRepository { return d.apiKeyRepositoryService } func (d *testDatastore) Settings() dataservices.SettingsService { return d.settings } +func (d *testDatastore) Snapshot() dataservices.SnapshotService { return d.snapshot } func (d *testDatastore) SSLSettings() dataservices.SSLSettingsService { return d.sslSettings } func (d *testDatastore) Stack() dataservices.StackService { return d.stack } func (d *testDatastore) Tag() dataservices.TagService { return d.tag } diff --git a/api/portainer.go b/api/portainer.go index 48e9ba073..f292332c3 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -1243,6 +1243,12 @@ type ( // WebhookType represents the type of resource a webhook is related to WebhookType int + Snapshot struct { + EndpointID EndpointID `json:"EndpointId"` + Docker *DockerSnapshot `json:"Docker"` + Kubernetes *KubernetesSnapshot `json:"Kubernetes"` + } + // CLIService represents a service for managing CLI CLIService interface { ParseFlags(version string) (*CLIFlags, error) @@ -1420,6 +1426,7 @@ type ( Start() SetSnapshotInterval(snapshotInterval string) error SnapshotEndpoint(endpoint *Endpoint) error + FillSnapshotData(endpoint *Endpoint) error } // SwarmStackManager represents a service to manage Swarm stacks diff --git a/app/edge/EdgeDevices/EdgeDevicesView/EdgeDevicesDatatable/EdgeDevicesDatatableContainer.tsx b/app/edge/EdgeDevices/EdgeDevicesView/EdgeDevicesDatatable/EdgeDevicesDatatableContainer.tsx index a1b8384e9..c6e2b6672 100644 --- a/app/edge/EdgeDevices/EdgeDevicesView/EdgeDevicesDatatable/EdgeDevicesDatatableContainer.tsx +++ b/app/edge/EdgeDevices/EdgeDevicesView/EdgeDevicesDatatable/EdgeDevicesDatatableContainer.tsx @@ -92,6 +92,7 @@ function Loader({ children, storageKey }: LoaderProps) { edgeDevice: true, search: debouncedSearchValue, types: EdgeTypes, + excludeSnapshots: true, ...pagination, }, settings.autoRefreshRate * 1000 diff --git a/app/edge/EdgeDevices/WaitingRoomView/WaitingRoomView.tsx b/app/edge/EdgeDevices/WaitingRoomView/WaitingRoomView.tsx index 7f5336af5..698181b84 100644 --- a/app/edge/EdgeDevices/WaitingRoomView/WaitingRoomView.tsx +++ b/app/edge/EdgeDevices/WaitingRoomView/WaitingRoomView.tsx @@ -17,6 +17,7 @@ export function WaitingRoomView() { const { environments, isLoading, totalCount } = useEnvironmentList({ edgeDevice: true, edgeDeviceUntrusted: true, + excludeSnapshots: true, types: EdgeTypes, }); diff --git a/app/portainer/environments/environment.service/index.ts b/app/portainer/environments/environment.service/index.ts index 2aaa0e40f..b5e6030cc 100644 --- a/app/portainer/environments/environment.service/index.ts +++ b/app/portainer/environments/environment.service/index.ts @@ -24,6 +24,7 @@ export interface EnvironmentsQueryParams { status?: EnvironmentStatus[]; edgeDevice?: boolean; edgeDeviceUntrusted?: boolean; + excludeSnapshots?: boolean; provisioned?: boolean; name?: string; agentVersions?: string[]; diff --git a/app/portainer/views/endpoints/endpointsController.js b/app/portainer/views/endpoints/endpointsController.js index 4f6303667..24e6ba4e9 100644 --- a/app/portainer/views/endpoints/endpointsController.js +++ b/app/portainer/views/endpoints/endpointsController.js @@ -50,7 +50,7 @@ function EndpointsController($q, $scope, $state, $async, EndpointService, GroupS function getPaginatedEndpoints(start, limit, search) { const deferred = $q.defer(); $q.all({ - endpoints: getEnvironments({ start, limit, query: { search } }), + endpoints: getEnvironments({ start, limit, query: { search, excludeSnapshots: true } }), groups: GroupService.groups(), }) .then(function success(data) {