1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-07-25 00:09:40 +02:00

feat(backup): Add backup/restore to the server

This commit is contained in:
Dmitry Salakhov 2021-04-06 22:08:43 +12:00
parent c04bbb5775
commit a3ec2f8e85
65 changed files with 2394 additions and 564 deletions

View file

@ -0,0 +1,71 @@
package offlinegate
import (
"log"
"net/http"
"sync"
"time"
httperror "github.com/portainer/libhttp/error"
)
// OfflineGate is a entity that works similar to a mutex with a signaling
// Only the caller that have Locked an gate can unlock it, otherw will be blocked with a call to Lock.
// Gate provides a passthrough http middleware that will wait for a locked gate to be unlocked.
// For a safety reasons, middleware will timeout
type OfflineGate struct {
lock *sync.Mutex
signalingCh chan interface{}
}
// NewOfflineGate creates a new gate
func NewOfflineGate() *OfflineGate {
return &OfflineGate{
lock: &sync.Mutex{},
}
}
// Lock locks readonly gate and returns a function to unlock
func (o *OfflineGate) Lock() func() {
o.lock.Lock()
o.signalingCh = make(chan interface{})
return o.unlock
}
func (o *OfflineGate) unlock() {
if o.signalingCh == nil {
return
}
close(o.signalingCh)
o.signalingCh = nil
o.lock.Unlock()
}
// Watch returns a signaling channel.
// Unless channel is nil, client needs to watch for a signal on a channel to know when gate is unlocked.
// Signal channel is disposable: onced signaled, has to be disposed and acquired again.
func (o *OfflineGate) Watch() chan interface{} {
return o.signalingCh
}
// WaitingMiddleware returns an http handler that waits for the gate to be unlocked before continuing
func (o *OfflineGate) WaitingMiddleware(timeout time.Duration, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
signalingCh := o.Watch()
if signalingCh != nil {
if r.Method != "GET" && r.Method != "HEAD" && r.Method != "OPTIONS" {
select {
case <-signalingCh:
case <-time.After(timeout):
log.Println("error: Timeout waiting for the offline gate to signal")
httperror.WriteError(w, http.StatusRequestTimeout, "Timeout waiting for the offline gate to signal", http.ErrHandlerTimeout)
}
}
}
next.ServeHTTP(w, r)
})
}

View file

@ -0,0 +1,217 @@
package offlinegate
import (
"io"
"net/http"
"net/http/httptest"
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func Test_canLockAndUnlock(t *testing.T) {
o := NewOfflineGate()
unlock := o.Lock()
unlock()
}
func Test_hasToBeUnlockedToLockAgain(t *testing.T) {
// scenario:
// 1. first routine starts and locks the gate
// 2. first routine starts a second and wait for the second to start
// 3. second start but waits for the gate to be released
// 4. first continues and unlocks the gate, when done
// 5. second be able to continue
// 6. second lock the gate, does the job and unlocks it
o := NewOfflineGate()
wg := sync.WaitGroup{}
wg.Add(2)
result := make([]string, 0, 2)
go func() {
unlock := o.Lock()
defer unlock()
waitForSecondToStart := sync.WaitGroup{}
waitForSecondToStart.Add(1)
go func() {
waitForSecondToStart.Done()
unlock := o.Lock()
defer unlock()
result = append(result, "second")
wg.Done()
}()
waitForSecondToStart.Wait()
result = append(result, "first")
wg.Done()
}()
wg.Wait()
if len(result) != 2 || result[0] != "first" || result[1] != "second" {
t.Error("Second call have disresregarded a raised lock")
}
}
func Test_waitChannelWillBeEmpty_ifGateIsUnlocked(t *testing.T) {
o := NewOfflineGate()
signalingCh := o.Watch()
if signalingCh != nil {
t.Error("Signaling channel should be empty")
}
}
func Test_startWaitingForSignal_beforeGateGetsUnlocked(t *testing.T) {
// scenario:
// 1. main routing locks the gate and waits for a consumer to start up
// 2. consumer starts up, notifies main and begins waiting for the gate to be unlocked
// 3. main unlocks the gate
// 4. consumer be able to continue
o := NewOfflineGate()
unlock := o.Lock()
signalingCh := o.Watch()
wg := sync.WaitGroup{}
wg.Add(1)
readerIsReady := sync.WaitGroup{}
readerIsReady.Add(1)
go func(t *testing.T) {
readerIsReady.Done()
// either wait for a signal or timeout
select {
case <-signalingCh:
case <-time.After(10 * time.Second):
t.Error("Failed to wait for a signal, exit by timeout")
}
wg.Done()
}(t)
readerIsReady.Wait()
unlock()
wg.Wait()
}
func Test_startWaitingForSignal_afterGateGetsUnlocked(t *testing.T) {
// scenario:
// 1. main routing locks, gets waiting channel and unlocks
// 2. consumer starts up and begins waiting for the gate to be unlocked
// 3. consumer gets signal immediately and continues
o := NewOfflineGate()
unlock := o.Lock()
signalingCh := o.Watch()
unlock()
wg := sync.WaitGroup{}
wg.Add(1)
go func(t *testing.T) {
// either wait for a signal or timeout
select {
case <-signalingCh:
case <-time.After(10 * time.Second):
t.Error("Failed to wait for a signal, exit by timeout")
}
wg.Done()
}(t)
wg.Wait()
}
func Test_waitingMiddleware_executesImmediately_whenNotLocked(t *testing.T) {
// scenario:
// 1. create an gate
// 2. kick off a waiting middleware that will release immediately as gate wasn't locked
// 3. middleware shouldn't timeout
o := NewOfflineGate()
request := httptest.NewRequest(http.MethodPost, "/", nil)
response := httptest.NewRecorder()
timeout := 2 * time.Second
start := time.Now()
o.WaitingMiddleware(timeout, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
elapsed := time.Since(start)
if elapsed >= timeout {
t.Error("WaitingMiddleware had likely timeout, when it shouldn't")
}
w.Write([]byte("success"))
})).ServeHTTP(response, request)
body, _ := io.ReadAll(response.Body)
if string(body) != "success" {
t.Error("Didn't receive expected result from the hanlder")
}
}
func Test_waitingMiddleware_waitsForTheLockToBeReleased(t *testing.T) {
// scenario:
// 1. create an gate and lock it
// 2. kick off a routing that will unlock the gate after 1 second
// 3. kick off a waiting middleware that will wait for lock to be eventually released
// 4. middleware shouldn't timeout
o := NewOfflineGate()
unlock := o.Lock()
request := httptest.NewRequest(http.MethodPost, "/", nil)
response := httptest.NewRecorder()
go func() {
time.Sleep(1 * time.Second)
unlock()
}()
timeout := 10 * time.Second
start := time.Now()
o.WaitingMiddleware(timeout, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
elapsed := time.Since(start)
if elapsed >= timeout {
t.Error("WaitingMiddleware had likely timeout, when it shouldn't")
}
w.Write([]byte("success"))
})).ServeHTTP(response, request)
body, _ := io.ReadAll(response.Body)
if string(body) != "success" {
t.Error("Didn't receive expected result from the hanlder")
}
}
func Test_waitingMiddleware_mayTimeout_whenLockedForTooLong(t *testing.T) {
/*
scenario:
1. create an gate and lock it
2. kick off a waiting middleware that will wait for lock to be eventually released
3. because we never unlocked the gate, middleware suppose to timeout
*/
o := NewOfflineGate()
o.Lock()
request := httptest.NewRequest(http.MethodPost, "/", nil)
response := httptest.NewRecorder()
timeout := 1 * time.Second
start := time.Now()
o.WaitingMiddleware(timeout, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
elapsed := time.Since(start)
if elapsed < timeout {
t.Error("WaitingMiddleware suppose to timeout, but it didnt")
}
w.Write([]byte("success"))
})).ServeHTTP(response, request)
assert.Equal(t, http.StatusRequestTimeout, response.Result().StatusCode, "Request support to timeout waiting for the gate")
}