mirror of
https://github.com/sablierapp/sablier.git
synced 2025-12-25 06:49:31 +01:00
refactor: instance are in unrecoverable state instead of error
To avoid confusion between error from a call or network. Instance has an Unrecoverable state instead of Error.
This commit is contained in:
@@ -15,7 +15,7 @@ import (
|
|||||||
//go:embed themes/*
|
//go:embed themes/*
|
||||||
var themes embed.FS
|
var themes embed.FS
|
||||||
|
|
||||||
type RenderOptionsRequestState struct {
|
type RenderOptionsInstanceState struct {
|
||||||
Name string
|
Name string
|
||||||
CurrentReplicas int
|
CurrentReplicas int
|
||||||
DesiredReplicas int
|
DesiredReplicas int
|
||||||
@@ -25,7 +25,7 @@ type RenderOptionsRequestState struct {
|
|||||||
|
|
||||||
type RenderOptions struct {
|
type RenderOptions struct {
|
||||||
DisplayName string
|
DisplayName string
|
||||||
RequestStates []RenderOptionsRequestState
|
InstanceStates []RenderOptionsInstanceState
|
||||||
SessionDuration time.Duration
|
SessionDuration time.Duration
|
||||||
RefreshFrequency time.Duration
|
RefreshFrequency time.Duration
|
||||||
Theme string
|
Theme string
|
||||||
@@ -35,7 +35,7 @@ type RenderOptions struct {
|
|||||||
|
|
||||||
type TemplateValues struct {
|
type TemplateValues struct {
|
||||||
DisplayName string
|
DisplayName string
|
||||||
RequestStates []RenderOptionsRequestState
|
InstanceStates []RenderOptionsInstanceState
|
||||||
SessionDuration string
|
SessionDuration string
|
||||||
RefreshFrequency time.Duration
|
RefreshFrequency time.Duration
|
||||||
Version string
|
Version string
|
||||||
@@ -59,7 +59,7 @@ func Render(options RenderOptions, writer io.Writer) error {
|
|||||||
|
|
||||||
return tpl.Execute(writer, TemplateValues{
|
return tpl.Execute(writer, TemplateValues{
|
||||||
DisplayName: options.DisplayName,
|
DisplayName: options.DisplayName,
|
||||||
RequestStates: options.RequestStates,
|
InstanceStates: options.InstanceStates,
|
||||||
SessionDuration: humanizeDuration(options.SessionDuration),
|
SessionDuration: humanizeDuration(options.SessionDuration),
|
||||||
RefreshFrequency: options.RefreshFrequency,
|
RefreshFrequency: options.RefreshFrequency,
|
||||||
Version: options.Version,
|
Version: options.Version,
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
var requestsStates []RenderOptionsRequestState = []RenderOptionsRequestState{
|
var instanceStates []RenderOptionsInstanceState = []RenderOptionsInstanceState{
|
||||||
{
|
{
|
||||||
Name: "nginx",
|
Name: "nginx",
|
||||||
CurrentReplicas: 0,
|
CurrentReplicas: 0,
|
||||||
@@ -46,7 +46,7 @@ func TestRender(t *testing.T) {
|
|||||||
args: args{
|
args: args{
|
||||||
options: RenderOptions{
|
options: RenderOptions{
|
||||||
DisplayName: "Test",
|
DisplayName: "Test",
|
||||||
RequestStates: requestsStates,
|
InstanceStates: instanceStates,
|
||||||
Theme: "ghost",
|
Theme: "ghost",
|
||||||
SessionDuration: 10 * time.Minute,
|
SessionDuration: 10 * time.Minute,
|
||||||
RefreshFrequency: 5 * time.Second,
|
RefreshFrequency: 5 * time.Second,
|
||||||
@@ -61,7 +61,7 @@ func TestRender(t *testing.T) {
|
|||||||
args: args{
|
args: args{
|
||||||
options: RenderOptions{
|
options: RenderOptions{
|
||||||
DisplayName: "Test",
|
DisplayName: "Test",
|
||||||
RequestStates: requestsStates,
|
InstanceStates: instanceStates,
|
||||||
Theme: "hacker-terminal",
|
Theme: "hacker-terminal",
|
||||||
SessionDuration: 10 * time.Minute,
|
SessionDuration: 10 * time.Minute,
|
||||||
RefreshFrequency: 5 * time.Second,
|
RefreshFrequency: 5 * time.Second,
|
||||||
@@ -76,7 +76,7 @@ func TestRender(t *testing.T) {
|
|||||||
args: args{
|
args: args{
|
||||||
options: RenderOptions{
|
options: RenderOptions{
|
||||||
DisplayName: "Test",
|
DisplayName: "Test",
|
||||||
RequestStates: requestsStates,
|
InstanceStates: instanceStates,
|
||||||
Theme: "matrix",
|
Theme: "matrix",
|
||||||
SessionDuration: 10 * time.Minute,
|
SessionDuration: 10 * time.Minute,
|
||||||
RefreshFrequency: 5 * time.Second,
|
RefreshFrequency: 5 * time.Second,
|
||||||
@@ -91,7 +91,7 @@ func TestRender(t *testing.T) {
|
|||||||
args: args{
|
args: args{
|
||||||
options: RenderOptions{
|
options: RenderOptions{
|
||||||
DisplayName: "Test",
|
DisplayName: "Test",
|
||||||
RequestStates: requestsStates,
|
InstanceStates: instanceStates,
|
||||||
Theme: "shuffle",
|
Theme: "shuffle",
|
||||||
SessionDuration: 10 * time.Minute,
|
SessionDuration: 10 * time.Minute,
|
||||||
RefreshFrequency: 5 * time.Second,
|
RefreshFrequency: 5 * time.Second,
|
||||||
@@ -106,7 +106,7 @@ func TestRender(t *testing.T) {
|
|||||||
args: args{
|
args: args{
|
||||||
options: RenderOptions{
|
options: RenderOptions{
|
||||||
DisplayName: "Test",
|
DisplayName: "Test",
|
||||||
RequestStates: requestsStates,
|
InstanceStates: instanceStates,
|
||||||
Theme: "nonexistant",
|
Theme: "nonexistant",
|
||||||
SessionDuration: 10 * time.Minute,
|
SessionDuration: 10 * time.Minute,
|
||||||
RefreshFrequency: 5 * time.Second,
|
RefreshFrequency: 5 * time.Second,
|
||||||
@@ -121,7 +121,7 @@ func TestRender(t *testing.T) {
|
|||||||
args: args{
|
args: args{
|
||||||
options: RenderOptions{
|
options: RenderOptions{
|
||||||
DisplayName: "Test",
|
DisplayName: "Test",
|
||||||
RequestStates: requestsStates,
|
InstanceStates: instanceStates,
|
||||||
Theme: "dc-comics.html",
|
Theme: "dc-comics.html",
|
||||||
SessionDuration: 10 * time.Minute,
|
SessionDuration: 10 * time.Minute,
|
||||||
RefreshFrequency: 5 * time.Second,
|
RefreshFrequency: 5 * time.Second,
|
||||||
@@ -139,7 +139,7 @@ func TestRender(t *testing.T) {
|
|||||||
args: args{
|
args: args{
|
||||||
options: RenderOptions{
|
options: RenderOptions{
|
||||||
DisplayName: "Test",
|
DisplayName: "Test",
|
||||||
RequestStates: requestsStates,
|
InstanceStates: instanceStates,
|
||||||
Theme: "nonexistant",
|
Theme: "nonexistant",
|
||||||
SessionDuration: 10 * time.Minute,
|
SessionDuration: 10 * time.Minute,
|
||||||
RefreshFrequency: 5 * time.Second,
|
RefreshFrequency: 5 * time.Second,
|
||||||
|
|||||||
@@ -53,13 +53,13 @@
|
|||||||
<p class="description">Your instance(s) will stop after {{ .SessionDuration }} of inactivity}</p>
|
<p class="description">Your instance(s) will stop after {{ .SessionDuration }} of inactivity}</p>
|
||||||
<div class="details">
|
<div class="details">
|
||||||
<table>
|
<table>
|
||||||
{{- range $i, $request := .RequestStates }}
|
{{- range $i, $instance := .InstanceStates }}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="name">{{ $request.Name }}</td>
|
<td class="name">{{ $instance.Name }}</td>
|
||||||
{{- if $request.Error }}
|
{{- if $instance.Error }}
|
||||||
<td class="value error">{{ $request.Error }}</td>
|
<td class="value error">{{ $instance.Error }}</td>
|
||||||
{{- else }}
|
{{- else }}
|
||||||
<td class="value success">{{ $request.Status }} ({{ $request.CurrentReplicas }}/{{ $request.DesiredReplicas }})</td>
|
<td class="value success">{{ $instance.Status }} ({{ $instance.CurrentReplicas }}/{{ $instance.DesiredReplicas }})</td>
|
||||||
{{- end}}
|
{{- end}}
|
||||||
</tr>
|
</tr>
|
||||||
{{ end -}}
|
{{ end -}}
|
||||||
|
|||||||
@@ -162,11 +162,11 @@
|
|||||||
<div class="terminal">
|
<div class="terminal">
|
||||||
<h1><span>Starting </span> <span class="error_code">{{ .DisplayName }}</span>...</h1>
|
<h1><span>Starting </span> <span class="error_code">{{ .DisplayName }}</span>...</h1>
|
||||||
<p class="output"><span>Your instance(s) will stop after {{ .SessionDuration }} of inactivity</span>.</p>
|
<p class="output"><span>Your instance(s) will stop after {{ .SessionDuration }} of inactivity</span>.</p>
|
||||||
{{ range $i, $request := .RequestStates }}
|
{{ range $i, $instance := .InstanceStates }}
|
||||||
<div class="details">
|
<div class="details">
|
||||||
<p class="output small command"><span>sablier status <span class="error_code">{{ $request.Name }}</span></span></code></p>
|
<p class="output small command"><span>sablier status <span class="error_code">{{ $instance.Name }}</span></span></code></p>
|
||||||
{{ if $request.Error }}<p class="output small error">An error occured</span>: <code>{{ $request.Error }}</code></p>
|
{{ if $instance.Error }}<p class="output small error">An error occured</span>: <code>{{ $instance.Error }}</code></p>
|
||||||
{{ else }}<p class="output small success"><span>{{ $request.Name }}</span> is {{ $request.Status }} <code>({{ $request.CurrentReplicas }}/{{ $request.DesiredReplicas }})</code></p>{{ end }}
|
{{ else }}<p class="output small success"><span>{{ $instance.Name }}</span> is {{ $instance.Status }} <code>({{ $instance.CurrentReplicas }}/{{ $instance.DesiredReplicas }})</code></p>{{ end }}
|
||||||
</div>
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -48,13 +48,13 @@
|
|||||||
|
|
||||||
<div class="details">
|
<div class="details">
|
||||||
<ul>
|
<ul>
|
||||||
{{- range $i, $request := .RequestStates }}
|
{{- range $i, $instance := .InstanceStates }}
|
||||||
<li>
|
<li>
|
||||||
<span><span>{{ $request.Name }}</span>:</span>
|
<span><span>{{ $instance.Name }}</span>:</span>
|
||||||
{{- if $request.Error }}
|
{{- if $instance.Error }}
|
||||||
<span class="error">{{ $request.Error }}</span>
|
<span class="error">{{ $instance.Error }}</span>
|
||||||
{{- else }}
|
{{- else }}
|
||||||
<span class="success">{{ $request.Status }} ({{ $request.CurrentReplicas }}/{{ $request.DesiredReplicas }})</span>
|
<span class="success">{{ $instance.Status }} ({{ $instance.CurrentReplicas }}/{{ $instance.DesiredReplicas }})</span>
|
||||||
{{- end}}
|
{{- end}}
|
||||||
</li>
|
</li>
|
||||||
{{ end -}}
|
{{ end -}}
|
||||||
|
|||||||
@@ -81,13 +81,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="hidden" id="details">
|
<div class="hidden" id="details">
|
||||||
<table>
|
<table>
|
||||||
{{- range $i, $request := .RequestStates }}
|
{{- range $i, $instance := .InstanceStates }}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="name">{{ $request.Name }}</td>
|
<td class="name">{{ $instance.Name }}</td>
|
||||||
{{- if $request.Error }}
|
{{- if $instance.Error }}
|
||||||
<td class="value error">{{ $request.Error }}</td>
|
<td class="value error">{{ $instance.Error }}</td>
|
||||||
{{- else }}
|
{{- else }}
|
||||||
<td class="value success">{{ $request.Status }} ({{ $request.CurrentReplicas }}/{{ $request.DesiredReplicas }})</td>
|
<td class="value success">{{ $instance.Status }} ({{ $instance.CurrentReplicas }}/{{ $instance.DesiredReplicas }})</td>
|
||||||
{{- end}}
|
{{- end}}
|
||||||
</tr>
|
</tr>
|
||||||
{{ end -}}
|
{{ end -}}
|
||||||
|
|||||||
9
app/http/routes/models/blocking_request.go
Normal file
9
app/http/routes/models/blocking_request.go
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type BlockingRequest struct {
|
||||||
|
Names []string
|
||||||
|
SessionDuration time.Duration
|
||||||
|
Timeout time.Duration
|
||||||
|
}
|
||||||
12
app/http/routes/models/dynamic_request.go
Normal file
12
app/http/routes/models/dynamic_request.go
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DynamicRequest struct {
|
||||||
|
Names []string
|
||||||
|
DisplayName string
|
||||||
|
Theme string
|
||||||
|
SessionDuration time.Duration
|
||||||
|
}
|
||||||
92
app/http/routes/strategies.go
Normal file
92
app/http/routes/strategies.go
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"github.com/acouvreur/sablier/app/http/pages"
|
||||||
|
"github.com/acouvreur/sablier/app/http/routes/models"
|
||||||
|
"github.com/acouvreur/sablier/app/instance"
|
||||||
|
"github.com/acouvreur/sablier/app/sessions"
|
||||||
|
"github.com/acouvreur/sablier/version"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ServeStrategy struct {
|
||||||
|
SessionsManager sessions.Manager
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServeDynamic returns a waiting page displaying the session request if the session is not ready
|
||||||
|
// If the session is ready, returns a redirect 307 with an arbitrary location
|
||||||
|
func (s *ServeStrategy) ServeDynamic(c *gin.Context) {
|
||||||
|
request := models.DynamicRequest{}
|
||||||
|
|
||||||
|
if err := c.BindJSON(&request); err != nil {
|
||||||
|
c.AbortWithError(http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionState := s.SessionsManager.RequestSession(request.Names, request.SessionDuration)
|
||||||
|
|
||||||
|
if sessionState.IsReady() {
|
||||||
|
// All requests are fulfilled, redirect to
|
||||||
|
c.Redirect(http.StatusTemporaryRedirect, "origin")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
renderOptions := pages.RenderOptions{
|
||||||
|
DisplayName: request.DisplayName,
|
||||||
|
SessionDuration: request.SessionDuration,
|
||||||
|
Theme: request.Theme,
|
||||||
|
Version: version.Version,
|
||||||
|
RefreshFrequency: 5 * time.Second,
|
||||||
|
InstanceStates: sessionStateToRenderOptionsInstanceState(sessionState),
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Header("Content-Type", "text/html")
|
||||||
|
if err := pages.Render(renderOptions, c.Writer); err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ServeStrategy) ServeBlocking(c *gin.Context) {
|
||||||
|
request := models.BlockingRequest{}
|
||||||
|
|
||||||
|
if err := c.BindJSON(&request); err != nil {
|
||||||
|
c.AbortWithError(http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionState := s.SessionsManager.RequestReadySession(request.Names, request.SessionDuration, request.Timeout)
|
||||||
|
|
||||||
|
if sessionState.IsReady() {
|
||||||
|
// All requests are fulfilled, redirect to
|
||||||
|
c.Redirect(http.StatusTemporaryRedirect, "origin")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func sessionStateToRenderOptionsInstanceState(sessionState *sessions.SessionState) (instances []pages.RenderOptionsInstanceState) {
|
||||||
|
sessionState.Instances.Range(func(key, value any) bool {
|
||||||
|
instances = append(instances, instanceStateToRenderOptionsRequestState(value.(*instance.State)))
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func instanceStateToRenderOptionsRequestState(instanceState *instance.State) pages.RenderOptionsInstanceState {
|
||||||
|
return pages.RenderOptionsInstanceState{
|
||||||
|
Name: instanceState.Name,
|
||||||
|
Status: instanceState.Status,
|
||||||
|
CurrentReplicas: instanceState.CurrentReplicas,
|
||||||
|
DesiredReplicas: 1, //instanceState.DesiredReplicas,
|
||||||
|
Error: fmt.Errorf(instanceState.Message),
|
||||||
|
}
|
||||||
|
}
|
||||||
163
app/http/routes/strategies_test.go
Normal file
163
app/http/routes/strategies_test.go
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/acouvreur/sablier/app/http/routes/models"
|
||||||
|
"github.com/acouvreur/sablier/app/instance"
|
||||||
|
"github.com/acouvreur/sablier/app/sessions"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"gotest.tools/v3/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SessionsManagerMock struct {
|
||||||
|
SessionState sessions.SessionState
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SessionsManagerMock) RequestSession(names []string, duration time.Duration) *sessions.SessionState {
|
||||||
|
return &s.SessionState
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SessionsManagerMock) RequestReadySession(names []string, duration time.Duration, timeout time.Duration) *sessions.SessionState {
|
||||||
|
return &s.SessionState
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServeStrategy_ServeDynamic(t *testing.T) {
|
||||||
|
type arg struct {
|
||||||
|
body models.DynamicRequest
|
||||||
|
session sessions.SessionState
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
arg arg
|
||||||
|
expectedCode int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "return HTML Theme",
|
||||||
|
arg: arg{
|
||||||
|
body: models.DynamicRequest{
|
||||||
|
Names: []string{"nginx"},
|
||||||
|
DisplayName: "Test",
|
||||||
|
Theme: "hacker-terminal",
|
||||||
|
SessionDuration: 1 * time.Minute,
|
||||||
|
},
|
||||||
|
session: sessions.SessionState{
|
||||||
|
Instances: createMap([]instance.State{
|
||||||
|
{Name: "nginx", Status: instance.NotReady},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedCode: http.StatusOK,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "temporary redirect when session is ready",
|
||||||
|
arg: arg{
|
||||||
|
body: models.DynamicRequest{
|
||||||
|
Names: []string{"nginx"},
|
||||||
|
DisplayName: "Test",
|
||||||
|
Theme: "hacker-terminal",
|
||||||
|
SessionDuration: 1 * time.Minute,
|
||||||
|
},
|
||||||
|
session: sessions.SessionState{
|
||||||
|
Instances: createMap([]instance.State{
|
||||||
|
{Name: "nginx", Status: instance.Ready},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedCode: http.StatusTemporaryRedirect,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
|
||||||
|
s := &ServeStrategy{
|
||||||
|
SessionsManager: &SessionsManagerMock{
|
||||||
|
SessionState: tt.arg.session,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
c := GetTestGinContext(recorder)
|
||||||
|
MockJsonPost(c, tt.arg.body)
|
||||||
|
|
||||||
|
s.ServeDynamic(c)
|
||||||
|
|
||||||
|
res := recorder.Result()
|
||||||
|
defer res.Body.Close()
|
||||||
|
|
||||||
|
assert.Equal(t, c.Writer.Status(), tt.expectedCode)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// mock gin context
|
||||||
|
func GetTestGinContext(w *httptest.ResponseRecorder) *gin.Context {
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
|
||||||
|
ctx, _ := gin.CreateTestContext(w)
|
||||||
|
ctx.Request = &http.Request{
|
||||||
|
Header: make(http.Header),
|
||||||
|
URL: &url.URL{},
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
// mock getrequest
|
||||||
|
func MockJsonGet(c *gin.Context, params gin.Params, u url.Values) {
|
||||||
|
c.Request.Method = "GET"
|
||||||
|
c.Request.Header.Set("Content-Type", "application/json")
|
||||||
|
c.Params = params
|
||||||
|
c.Request.URL.RawQuery = u.Encode()
|
||||||
|
}
|
||||||
|
|
||||||
|
func MockJsonPost(c *gin.Context, content interface{}) {
|
||||||
|
c.Request.Method = "POST"
|
||||||
|
c.Request.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
jsonbytes, err := json.Marshal(content)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// the request body must be an io.ReadCloser
|
||||||
|
// the bytes buffer though doesn't implement io.Closer,
|
||||||
|
// so you wrap it in a no-op closer
|
||||||
|
c.Request.Body = io.NopCloser(bytes.NewBuffer(jsonbytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
func MockJsonPut(c *gin.Context, content interface{}, params gin.Params) {
|
||||||
|
c.Request.Method = "PUT"
|
||||||
|
c.Request.Header.Set("Content-Type", "application/json")
|
||||||
|
c.Params = params
|
||||||
|
|
||||||
|
jsonbytes, err := json.Marshal(content)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Request.Body = io.NopCloser(bytes.NewBuffer(jsonbytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
func MockJsonDelete(c *gin.Context, params gin.Params) {
|
||||||
|
c.Request.Method = "DELETE"
|
||||||
|
c.Request.Header.Set("Content-Type", "application/json")
|
||||||
|
c.Params = params
|
||||||
|
}
|
||||||
|
|
||||||
|
func createMap(instances []instance.State) (store *sync.Map) {
|
||||||
|
store = &sync.Map{}
|
||||||
|
|
||||||
|
for _, v := range instances {
|
||||||
|
store.Store(v.Name, &v)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -4,40 +4,36 @@ import log "github.com/sirupsen/logrus"
|
|||||||
|
|
||||||
var Ready = "ready"
|
var Ready = "ready"
|
||||||
var NotReady = "not-ready"
|
var NotReady = "not-ready"
|
||||||
var Error = "error"
|
var Unrecoverable = "unrecoverable"
|
||||||
|
|
||||||
type State struct {
|
type State struct {
|
||||||
Name string
|
Name string
|
||||||
CurrentReplicas int
|
CurrentReplicas int
|
||||||
Status string
|
Status string
|
||||||
Error string
|
Message string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (instance State) IsReady() bool {
|
func (instance State) IsReady() bool {
|
||||||
return instance.Status == Ready
|
return instance.Status == Ready
|
||||||
}
|
}
|
||||||
|
|
||||||
func (instance State) HasError() bool {
|
|
||||||
return instance.Status == Error
|
|
||||||
}
|
|
||||||
|
|
||||||
func ErrorInstanceState(name string, err error) (State, error) {
|
func ErrorInstanceState(name string, err error) (State, error) {
|
||||||
log.Error(err.Error())
|
log.Error(err.Error())
|
||||||
return State{
|
return State{
|
||||||
Name: name,
|
Name: name,
|
||||||
CurrentReplicas: 0,
|
CurrentReplicas: 0,
|
||||||
Status: Error,
|
Status: Unrecoverable,
|
||||||
Error: err.Error(),
|
Message: err.Error(),
|
||||||
}, err
|
}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func UnrecoverableInstanceState(name string, err string) (State, error) {
|
func UnrecoverableInstanceState(name string, message string) (State, error) {
|
||||||
log.Warn(err)
|
log.Warn(message)
|
||||||
return State{
|
return State{
|
||||||
Name: name,
|
Name: name,
|
||||||
CurrentReplicas: 0,
|
CurrentReplicas: 0,
|
||||||
Status: Error,
|
Status: Unrecoverable,
|
||||||
Error: err,
|
Message: message,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -86,8 +86,8 @@ func TestDockerClassicProvider_GetState(t *testing.T) {
|
|||||||
want: instance.State{
|
want: instance.State{
|
||||||
Name: "nginx",
|
Name: "nginx",
|
||||||
CurrentReplicas: 0,
|
CurrentReplicas: 0,
|
||||||
Status: instance.Error,
|
Status: instance.Unrecoverable,
|
||||||
Error: "container is unhealthy: curl http://localhost failed (1)",
|
Message: "container is unhealthy: curl http://localhost failed (1)",
|
||||||
},
|
},
|
||||||
wantErr: false,
|
wantErr: false,
|
||||||
containerSpec: mocks.RunningWithHealthcheckContainerSpec("nginx", "unhealthy"),
|
containerSpec: mocks.RunningWithHealthcheckContainerSpec("nginx", "unhealthy"),
|
||||||
@@ -183,8 +183,8 @@ func TestDockerClassicProvider_GetState(t *testing.T) {
|
|||||||
want: instance.State{
|
want: instance.State{
|
||||||
Name: "nginx",
|
Name: "nginx",
|
||||||
CurrentReplicas: 0,
|
CurrentReplicas: 0,
|
||||||
Status: instance.Error,
|
Status: instance.Unrecoverable,
|
||||||
Error: "container exited with code \"137\"",
|
Message: "container exited with code \"137\"",
|
||||||
},
|
},
|
||||||
wantErr: false,
|
wantErr: false,
|
||||||
containerSpec: mocks.ExitedContainerSpec("nginx", 137),
|
containerSpec: mocks.ExitedContainerSpec("nginx", 137),
|
||||||
@@ -200,8 +200,8 @@ func TestDockerClassicProvider_GetState(t *testing.T) {
|
|||||||
want: instance.State{
|
want: instance.State{
|
||||||
Name: "nginx",
|
Name: "nginx",
|
||||||
CurrentReplicas: 0,
|
CurrentReplicas: 0,
|
||||||
Status: instance.Error,
|
Status: instance.Unrecoverable,
|
||||||
Error: "container in \"dead\" state cannot be restarted",
|
Message: "container in \"dead\" state cannot be restarted",
|
||||||
},
|
},
|
||||||
wantErr: false,
|
wantErr: false,
|
||||||
containerSpec: mocks.DeadContainerSpec("nginx"),
|
containerSpec: mocks.DeadContainerSpec("nginx"),
|
||||||
@@ -217,8 +217,8 @@ func TestDockerClassicProvider_GetState(t *testing.T) {
|
|||||||
want: instance.State{
|
want: instance.State{
|
||||||
Name: "nginx",
|
Name: "nginx",
|
||||||
CurrentReplicas: 0,
|
CurrentReplicas: 0,
|
||||||
Status: instance.Error,
|
Status: instance.Unrecoverable,
|
||||||
Error: "container with name \"nginx\" was not found",
|
Message: "container with name \"nginx\" was not found",
|
||||||
},
|
},
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
containerSpec: types.ContainerJSON{},
|
containerSpec: types.ContainerJSON{},
|
||||||
@@ -271,8 +271,8 @@ func TestDockerClassicProvider_Stop(t *testing.T) {
|
|||||||
want: instance.State{
|
want: instance.State{
|
||||||
Name: "nginx",
|
Name: "nginx",
|
||||||
CurrentReplicas: 0,
|
CurrentReplicas: 0,
|
||||||
Status: instance.Error,
|
Status: instance.Unrecoverable,
|
||||||
Error: "container with name \"nginx\" was not found",
|
Message: "container with name \"nginx\" was not found",
|
||||||
},
|
},
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
err: fmt.Errorf("container with name \"nginx\" was not found"),
|
err: fmt.Errorf("container with name \"nginx\" was not found"),
|
||||||
@@ -340,8 +340,8 @@ func TestDockerClassicProvider_Start(t *testing.T) {
|
|||||||
want: instance.State{
|
want: instance.State{
|
||||||
Name: "nginx",
|
Name: "nginx",
|
||||||
CurrentReplicas: 0,
|
CurrentReplicas: 0,
|
||||||
Status: instance.Error,
|
Status: instance.Unrecoverable,
|
||||||
Error: "container with name \"nginx\" was not found",
|
Message: "container with name \"nginx\" was not found",
|
||||||
},
|
},
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
err: fmt.Errorf("container with name \"nginx\" was not found"),
|
err: fmt.Errorf("container with name \"nginx\" was not found"),
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ type DockerSwarmProvider struct {
|
|||||||
func NewDockerSwarmProvider() (*DockerSwarmProvider, error) {
|
func NewDockerSwarmProvider() (*DockerSwarmProvider, error) {
|
||||||
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
|
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(fmt.Errorf("%+v", "Could not connect to docker API"))
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &DockerSwarmProvider{
|
return &DockerSwarmProvider{
|
||||||
|
|||||||
@@ -68,8 +68,8 @@ func TestDockerSwarmProvider_Start(t *testing.T) {
|
|||||||
want: instance.State{
|
want: instance.State{
|
||||||
Name: "nginx",
|
Name: "nginx",
|
||||||
CurrentReplicas: 0,
|
CurrentReplicas: 0,
|
||||||
Status: instance.Error,
|
Status: instance.Unrecoverable,
|
||||||
Error: "ambiguous service names found for \"nginx\" (STACK1_nginx, STACK2_nginx)",
|
Message: "ambiguous service names found for \"nginx\" (STACK1_nginx, STACK2_nginx)",
|
||||||
},
|
},
|
||||||
wantService: mocks.ServiceReplicated("nginx", 1),
|
wantService: mocks.ServiceReplicated("nginx", 1),
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
@@ -138,8 +138,8 @@ func TestDockerSwarmProvider_Start(t *testing.T) {
|
|||||||
want: instance.State{
|
want: instance.State{
|
||||||
Name: "nginx",
|
Name: "nginx",
|
||||||
CurrentReplicas: 0,
|
CurrentReplicas: 0,
|
||||||
Status: instance.Error,
|
Status: instance.Unrecoverable,
|
||||||
Error: "swarm service is not in \"replicated\" mode",
|
Message: "swarm service is not in \"replicated\" mode",
|
||||||
},
|
},
|
||||||
wantService: mocks.ServiceReplicated("nginx", 1),
|
wantService: mocks.ServiceReplicated("nginx", 1),
|
||||||
wantErr: false,
|
wantErr: false,
|
||||||
@@ -223,8 +223,8 @@ func TestDockerSwarmProvider_Stop(t *testing.T) {
|
|||||||
want: instance.State{
|
want: instance.State{
|
||||||
Name: "nginx",
|
Name: "nginx",
|
||||||
CurrentReplicas: 0,
|
CurrentReplicas: 0,
|
||||||
Status: instance.Error,
|
Status: instance.Unrecoverable,
|
||||||
Error: "ambiguous service names found for \"nginx\" (STACK1_nginx, STACK2_nginx)",
|
Message: "ambiguous service names found for \"nginx\" (STACK1_nginx, STACK2_nginx)",
|
||||||
},
|
},
|
||||||
wantService: mocks.ServiceReplicated("nginx", 1),
|
wantService: mocks.ServiceReplicated("nginx", 1),
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
@@ -293,8 +293,8 @@ func TestDockerSwarmProvider_Stop(t *testing.T) {
|
|||||||
want: instance.State{
|
want: instance.State{
|
||||||
Name: "nginx",
|
Name: "nginx",
|
||||||
CurrentReplicas: 0,
|
CurrentReplicas: 0,
|
||||||
Status: instance.Error,
|
Status: instance.Unrecoverable,
|
||||||
Error: "swarm service is not in \"replicated\" mode",
|
Message: "swarm service is not in \"replicated\" mode",
|
||||||
},
|
},
|
||||||
wantService: mocks.ServiceReplicated("nginx", 1),
|
wantService: mocks.ServiceReplicated("nginx", 1),
|
||||||
wantErr: false,
|
wantErr: false,
|
||||||
@@ -404,8 +404,8 @@ func TestDockerSwarmProvider_GetState(t *testing.T) {
|
|||||||
want: instance.State{
|
want: instance.State{
|
||||||
Name: "nginx",
|
Name: "nginx",
|
||||||
CurrentReplicas: 0,
|
CurrentReplicas: 0,
|
||||||
Status: instance.Error,
|
Status: instance.Unrecoverable,
|
||||||
Error: "swarm service is not in \"replicated\" mode",
|
Message: "swarm service is not in \"replicated\" mode",
|
||||||
},
|
},
|
||||||
wantErr: false,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/acouvreur/sablier/app/instance"
|
"github.com/acouvreur/sablier/app/instance"
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
autoscalingv1 "k8s.io/api/autoscaling/v1"
|
autoscalingv1 "k8s.io/api/autoscaling/v1"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/client-go/kubernetes"
|
"k8s.io/client-go/kubernetes"
|
||||||
@@ -55,18 +54,18 @@ type KubernetesProvider struct {
|
|||||||
Client kubernetes.Interface
|
Client kubernetes.Interface
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewKubernetesProvider() *KubernetesProvider {
|
func NewKubernetesProvider() (*KubernetesProvider, error) {
|
||||||
config, err := rest.InClusterConfig()
|
config, err := rest.InClusterConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
return nil, err
|
||||||
}
|
}
|
||||||
client, err := kubernetes.NewForConfig(config)
|
client, err := kubernetes.NewForConfig(config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
return nil, err
|
||||||
}
|
}
|
||||||
return &KubernetesProvider{
|
return &KubernetesProvider{
|
||||||
Client: client,
|
Client: client,
|
||||||
}
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (provider *KubernetesProvider) Start(name string) (instance.State, error) {
|
func (provider *KubernetesProvider) Start(name string) (instance.State, error) {
|
||||||
|
|||||||
@@ -70,8 +70,8 @@ func TestKubernetesProvider_Start(t *testing.T) {
|
|||||||
want: instance.State{
|
want: instance.State{
|
||||||
Name: "gateway_default_nginx_2",
|
Name: "gateway_default_nginx_2",
|
||||||
CurrentReplicas: 0,
|
CurrentReplicas: 0,
|
||||||
Status: instance.Error,
|
Status: instance.Unrecoverable,
|
||||||
Error: "unsupported kind \"gateway\" must be one of \"deployment\", \"statefulset\"",
|
Message: "unsupported kind \"gateway\" must be one of \"deployment\", \"statefulset\"",
|
||||||
},
|
},
|
||||||
data: data{
|
data: data{
|
||||||
name: "nginx",
|
name: "nginx",
|
||||||
@@ -165,8 +165,8 @@ func TestKubernetesProvider_Stop(t *testing.T) {
|
|||||||
want: instance.State{
|
want: instance.State{
|
||||||
Name: "gateway_default_nginx_2",
|
Name: "gateway_default_nginx_2",
|
||||||
CurrentReplicas: 0,
|
CurrentReplicas: 0,
|
||||||
Status: instance.Error,
|
Status: instance.Unrecoverable,
|
||||||
Error: "unsupported kind \"gateway\" must be one of \"deployment\", \"statefulset\"",
|
Message: "unsupported kind \"gateway\" must be one of \"deployment\", \"statefulset\"",
|
||||||
},
|
},
|
||||||
data: data{
|
data: data{
|
||||||
name: "nginx",
|
name: "nginx",
|
||||||
@@ -290,8 +290,8 @@ func TestKubernetesProvider_GetState(t *testing.T) {
|
|||||||
want: instance.State{
|
want: instance.State{
|
||||||
Name: "gateway_default_nginx_2",
|
Name: "gateway_default_nginx_2",
|
||||||
CurrentReplicas: 0,
|
CurrentReplicas: 0,
|
||||||
Status: instance.Error,
|
Status: instance.Unrecoverable,
|
||||||
Error: "unsupported kind \"gateway\" must be one of \"deployment\", \"statefulset\"",
|
Message: "unsupported kind \"gateway\" must be one of \"deployment\", \"statefulset\"",
|
||||||
},
|
},
|
||||||
data: data{
|
data: data{
|
||||||
name: "nginx",
|
name: "nginx",
|
||||||
|
|||||||
@@ -1,9 +1,26 @@
|
|||||||
package providers
|
package providers
|
||||||
|
|
||||||
import "github.com/acouvreur/sablier/app/instance"
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/acouvreur/sablier/app/instance"
|
||||||
|
"github.com/acouvreur/sablier/config"
|
||||||
|
)
|
||||||
|
|
||||||
type Provider interface {
|
type Provider interface {
|
||||||
Start(name string) (instance.State, error)
|
Start(name string) (instance.State, error)
|
||||||
Stop(name string) (instance.State, error)
|
Stop(name string) (instance.State, error)
|
||||||
GetState(name string) (instance.State, error)
|
GetState(name string) (instance.State, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewProvider(config config.Provider) (Provider, error) {
|
||||||
|
switch {
|
||||||
|
case config.Name == "swarm":
|
||||||
|
return NewDockerSwarmProvider()
|
||||||
|
case config.Name == "docker":
|
||||||
|
return NewDockerClassicProvider()
|
||||||
|
case config.Name == "kubernetes":
|
||||||
|
return NewKubernetesProvider()
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("unimplemented provider %s", config.Name)
|
||||||
|
}
|
||||||
|
|||||||
126
app/sessions/sessions_manager.go
Normal file
126
app/sessions/sessions_manager.go
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
package sessions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/acouvreur/sablier/app/instance"
|
||||||
|
"github.com/acouvreur/sablier/app/providers"
|
||||||
|
"github.com/acouvreur/sablier/config"
|
||||||
|
"github.com/acouvreur/sablier/pkg/tinykv"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Manager interface {
|
||||||
|
RequestSession(names []string, duration time.Duration) *SessionState
|
||||||
|
RequestReadySession(names []string, duration time.Duration, timeout time.Duration) *SessionState
|
||||||
|
}
|
||||||
|
|
||||||
|
type SessionsManager struct {
|
||||||
|
store tinykv.KV[instance.State]
|
||||||
|
provider providers.Provider
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSessionsManager(conf config.Sessions, provider providers.Provider) (Manager, error) {
|
||||||
|
|
||||||
|
store := tinykv.New(conf.ExpirationInterval, onSessionExpires(provider))
|
||||||
|
|
||||||
|
return &SessionsManager{
|
||||||
|
store: store,
|
||||||
|
provider: provider,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type InstanceState struct {
|
||||||
|
Instance *instance.State
|
||||||
|
Error error
|
||||||
|
}
|
||||||
|
|
||||||
|
type SessionState struct {
|
||||||
|
Instances *sync.Map
|
||||||
|
}
|
||||||
|
|
||||||
|
func onSessionExpires(provider providers.Provider) func(key string, instance instance.State) {
|
||||||
|
return func(key string, instance instance.State) {
|
||||||
|
log.Debugf("stopping %s...", key)
|
||||||
|
_, err := provider.Stop(key)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("error stopping %s: %s", key, err.Error())
|
||||||
|
} else {
|
||||||
|
log.Debugf("stopped %s", key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SessionState) IsReady() bool {
|
||||||
|
ready := true
|
||||||
|
|
||||||
|
s.Instances.Range(func(key, value interface{}) bool {
|
||||||
|
state := value.(InstanceState)
|
||||||
|
if state.Error != nil || state.Instance.Status != instance.Ready {
|
||||||
|
ready = false
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
return ready
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SessionsManager) RequestSession(names []string, duration time.Duration) (sessionState *SessionState) {
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
|
wg.Add(len(names))
|
||||||
|
|
||||||
|
for i := 0; i < len(names); i++ {
|
||||||
|
name := names[i]
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
state, err := s.requestSessionInstance(name, duration)
|
||||||
|
|
||||||
|
sessionState.Instances.Store(name, InstanceState{
|
||||||
|
Instance: state,
|
||||||
|
Error: err,
|
||||||
|
})
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
return sessionState
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SessionsManager) requestSessionInstance(name string, duration time.Duration) (*instance.State, error) {
|
||||||
|
|
||||||
|
requestState, exists := s.store.Get(name)
|
||||||
|
|
||||||
|
// Trust the stored value
|
||||||
|
// TODO: Provider background check on the store
|
||||||
|
// Via polling or whatever
|
||||||
|
if !exists || requestState.Status != instance.Ready {
|
||||||
|
state, err := s.provider.Start(name)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
requestState.Name = state.Name
|
||||||
|
requestState.CurrentReplicas = state.CurrentReplicas
|
||||||
|
requestState.Status = state.Status
|
||||||
|
requestState.Error = state.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh the duration
|
||||||
|
s.ExpiresAfter(&requestState, duration)
|
||||||
|
return &requestState, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SessionsManager) RequestReadySession(names []string, duration time.Duration, timeout time.Duration) *SessionState {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SessionsManager) ExpiresAfter(request *instance.State, duration time.Duration) {
|
||||||
|
s.store.Put(request.Name, *request, tinykv.ExpiresAfter(duration))
|
||||||
|
}
|
||||||
81
app/sessions/sessions_manager_test.go
Normal file
81
app/sessions/sessions_manager_test.go
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
package sessions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/acouvreur/sablier/app/instance"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSessionState_IsReady(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
Instances *sync.Map
|
||||||
|
Error error
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "all instances are ready",
|
||||||
|
fields: fields{
|
||||||
|
Instances: createMap([]instance.State{
|
||||||
|
{Name: "nginx", Status: instance.Ready},
|
||||||
|
{Name: "apache", Status: instance.Ready},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "one instance is not ready",
|
||||||
|
fields: fields{
|
||||||
|
Instances: createMap([]instance.State{
|
||||||
|
{Name: "nginx", Status: instance.Ready},
|
||||||
|
{Name: "apache", Status: instance.NotReady},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no instances specified",
|
||||||
|
fields: fields{
|
||||||
|
Instances: createMap([]instance.State{}),
|
||||||
|
},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "one instance has an error",
|
||||||
|
fields: fields{
|
||||||
|
Instances: createMap([]instance.State{
|
||||||
|
{Name: "nginx", Status: instance.Error, Error: "connection timeout"},
|
||||||
|
{Name: "apache", Status: instance.Ready},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
s := &SessionState{
|
||||||
|
Instances: tt.fields.Instances,
|
||||||
|
}
|
||||||
|
if got := s.IsReady(); got != tt.want {
|
||||||
|
t.Errorf("SessionState.IsReady() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func createMap(instances []instance.State) (store *sync.Map) {
|
||||||
|
store = &sync.Map{}
|
||||||
|
|
||||||
|
for _, v := range instances {
|
||||||
|
store.Store(v.Name, InstanceState{
|
||||||
|
Instance: &v,
|
||||||
|
Error: nil,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user