mirror of https://github.com/portainer/portainer
446 lines
11 KiB
Go
446 lines
11 KiB
Go
|
package endpointedge
|
||
|
|
||
|
import (
|
||
|
"context"
|
||
|
"encoding/json"
|
||
|
"fmt"
|
||
|
"net/http"
|
||
|
"net/http/httptest"
|
||
|
"testing"
|
||
|
"time"
|
||
|
|
||
|
portainer "github.com/portainer/portainer/api"
|
||
|
"github.com/portainer/portainer/api/apikey"
|
||
|
"github.com/portainer/portainer/api/chisel"
|
||
|
"github.com/portainer/portainer/api/datastore"
|
||
|
"github.com/portainer/portainer/api/filesystem"
|
||
|
"github.com/portainer/portainer/api/http/security"
|
||
|
"github.com/portainer/portainer/api/jwt"
|
||
|
|
||
|
"github.com/stretchr/testify/assert"
|
||
|
)
|
||
|
|
||
|
type endpointTestCase struct {
|
||
|
endpoint portainer.Endpoint
|
||
|
endpointRelation portainer.EndpointRelation
|
||
|
expectedStatusCode int
|
||
|
}
|
||
|
|
||
|
var endpointTestCases = []endpointTestCase{
|
||
|
{
|
||
|
portainer.Endpoint{},
|
||
|
portainer.EndpointRelation{},
|
||
|
http.StatusNotFound,
|
||
|
},
|
||
|
{
|
||
|
portainer.Endpoint{
|
||
|
ID: -1,
|
||
|
Name: "endpoint-id--1",
|
||
|
Type: portainer.EdgeAgentOnDockerEnvironment,
|
||
|
URL: "https://portainer.io:9443",
|
||
|
EdgeID: "edge-id",
|
||
|
},
|
||
|
portainer.EndpointRelation{
|
||
|
EndpointID: -1,
|
||
|
},
|
||
|
http.StatusNotFound,
|
||
|
},
|
||
|
{
|
||
|
portainer.Endpoint{
|
||
|
ID: 2,
|
||
|
Name: "endpoint-id-2",
|
||
|
Type: portainer.EdgeAgentOnDockerEnvironment,
|
||
|
URL: "https://portainer.io:9443",
|
||
|
EdgeID: "",
|
||
|
},
|
||
|
portainer.EndpointRelation{
|
||
|
EndpointID: 2,
|
||
|
},
|
||
|
http.StatusForbidden,
|
||
|
},
|
||
|
{
|
||
|
portainer.Endpoint{
|
||
|
ID: 4,
|
||
|
Name: "endpoint-id-4",
|
||
|
Type: portainer.EdgeAgentOnDockerEnvironment,
|
||
|
URL: "https://portainer.io:9443",
|
||
|
EdgeID: "edge-id",
|
||
|
},
|
||
|
portainer.EndpointRelation{
|
||
|
EndpointID: 4,
|
||
|
},
|
||
|
http.StatusOK,
|
||
|
},
|
||
|
}
|
||
|
|
||
|
func setupHandler(t *testing.T) (*Handler, func(), error) {
|
||
|
tmpDir := t.TempDir()
|
||
|
fs, err := filesystem.NewService(tmpDir, "")
|
||
|
if err != nil {
|
||
|
return nil, nil, fmt.Errorf("could not start a new filesystem service: %w", err)
|
||
|
}
|
||
|
|
||
|
_, store, storeTeardown := datastore.MustNewTestStore(t, true, true)
|
||
|
|
||
|
ctx := context.Background()
|
||
|
shutdownCtx, cancelFn := context.WithCancel(ctx)
|
||
|
|
||
|
teardown := func() {
|
||
|
cancelFn()
|
||
|
storeTeardown()
|
||
|
}
|
||
|
|
||
|
jwtService, err := jwt.NewService("1h", store)
|
||
|
if err != nil {
|
||
|
teardown()
|
||
|
return nil, nil, fmt.Errorf("could not start a new jwt service: %w", err)
|
||
|
}
|
||
|
|
||
|
apiKeyService := apikey.NewAPIKeyService(nil, nil)
|
||
|
|
||
|
settings, err := store.Settings().Settings()
|
||
|
if err != nil {
|
||
|
teardown()
|
||
|
return nil, nil, fmt.Errorf("could not create new settings: %w", err)
|
||
|
}
|
||
|
settings.TrustOnFirstConnect = true
|
||
|
|
||
|
err = store.Settings().UpdateSettings(settings)
|
||
|
if err != nil {
|
||
|
teardown()
|
||
|
return nil, nil, fmt.Errorf("could not update settings: %w", err)
|
||
|
}
|
||
|
|
||
|
handler := NewHandler(
|
||
|
security.NewRequestBouncer(store, jwtService, apiKeyService),
|
||
|
store,
|
||
|
fs,
|
||
|
chisel.NewService(store, shutdownCtx),
|
||
|
)
|
||
|
|
||
|
handler.ReverseTunnelService = chisel.NewService(store, shutdownCtx)
|
||
|
|
||
|
return handler, teardown, nil
|
||
|
}
|
||
|
|
||
|
func createEndpoint(handler *Handler, endpoint portainer.Endpoint, endpointRelation portainer.EndpointRelation) (err error) {
|
||
|
// Avoid setting ID below 0 to generate invalid test cases
|
||
|
if endpoint.ID <= 0 {
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
err = handler.DataStore.Endpoint().Create(&endpoint)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
return handler.DataStore.EndpointRelation().Create(&endpointRelation)
|
||
|
}
|
||
|
|
||
|
func TestMissingEdgeIdentifier(t *testing.T) {
|
||
|
handler, teardown, err := setupHandler(t)
|
||
|
defer teardown()
|
||
|
|
||
|
if err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
|
||
|
endpointID := portainer.EndpointID(45)
|
||
|
err = createEndpoint(handler, portainer.Endpoint{
|
||
|
ID: endpointID,
|
||
|
Name: "endpoint-id-45",
|
||
|
Type: portainer.EdgeAgentOnDockerEnvironment,
|
||
|
URL: "https://portainer.io:9443",
|
||
|
EdgeID: "edge-id",
|
||
|
}, portainer.EndpointRelation{EndpointID: endpointID})
|
||
|
|
||
|
if err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
|
||
|
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/%d/edge/status", endpointID), nil)
|
||
|
if err != nil {
|
||
|
t.Fatal("request error:", err)
|
||
|
}
|
||
|
|
||
|
rec := httptest.NewRecorder()
|
||
|
handler.ServeHTTP(rec, req)
|
||
|
|
||
|
if rec.Code != http.StatusForbidden {
|
||
|
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d without Edge identifier", http.StatusForbidden, rec.Code))
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func TestWithEndpoints(t *testing.T) {
|
||
|
handler, teardown, err := setupHandler(t)
|
||
|
defer teardown()
|
||
|
|
||
|
if err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
|
||
|
for _, test := range endpointTestCases {
|
||
|
err = createEndpoint(handler, test.endpoint, test.endpointRelation)
|
||
|
if err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
|
||
|
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/%d/edge/status", test.endpoint.ID), nil)
|
||
|
if err != nil {
|
||
|
t.Fatal("request error:", err)
|
||
|
}
|
||
|
|
||
|
req.Header.Set(portainer.PortainerAgentEdgeIDHeader, test.endpoint.EdgeID)
|
||
|
req.Header.Set(portainer.HTTPResponseAgentPlatform, "1")
|
||
|
|
||
|
rec := httptest.NewRecorder()
|
||
|
handler.ServeHTTP(rec, req)
|
||
|
|
||
|
if rec.Code != test.expectedStatusCode {
|
||
|
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d for endpoint ID: %d", test.expectedStatusCode, rec.Code, test.endpoint.ID))
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func TestLastCheckInDateIncreases(t *testing.T) {
|
||
|
handler, teardown, err := setupHandler(t)
|
||
|
defer teardown()
|
||
|
|
||
|
if err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
|
||
|
endpointID := portainer.EndpointID(56)
|
||
|
endpoint := portainer.Endpoint{
|
||
|
ID: endpointID,
|
||
|
Name: "test-endpoint-56",
|
||
|
Type: portainer.EdgeAgentOnDockerEnvironment,
|
||
|
URL: "https://portainer.io:9443",
|
||
|
EdgeID: "edge-id",
|
||
|
LastCheckInDate: time.Now().Unix(),
|
||
|
}
|
||
|
|
||
|
endpointRelation := portainer.EndpointRelation{
|
||
|
EndpointID: endpoint.ID,
|
||
|
}
|
||
|
|
||
|
err = createEndpoint(handler, endpoint, endpointRelation)
|
||
|
if err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
|
||
|
time.Sleep(1 * time.Second)
|
||
|
|
||
|
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/%d/edge/status", endpoint.ID), nil)
|
||
|
if err != nil {
|
||
|
t.Fatal("request error:", err)
|
||
|
}
|
||
|
req.Header.Set(portainer.PortainerAgentEdgeIDHeader, "edge-id")
|
||
|
req.Header.Set(portainer.HTTPResponseAgentPlatform, "1")
|
||
|
|
||
|
rec := httptest.NewRecorder()
|
||
|
handler.ServeHTTP(rec, req)
|
||
|
|
||
|
if rec.Code != http.StatusOK {
|
||
|
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d", http.StatusOK, rec.Code))
|
||
|
}
|
||
|
|
||
|
updatedEndpoint, err := handler.DataStore.Endpoint().Endpoint(endpoint.ID)
|
||
|
if err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
|
||
|
assert.Greater(t, updatedEndpoint.LastCheckInDate, endpoint.LastCheckInDate)
|
||
|
}
|
||
|
|
||
|
func TestEmptyEdgeIdWithAgentPlatformHeader(t *testing.T) {
|
||
|
handler, teardown, err := setupHandler(t)
|
||
|
defer teardown()
|
||
|
|
||
|
if err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
|
||
|
endpointID := portainer.EndpointID(44)
|
||
|
edgeId := "edge-id"
|
||
|
endpoint := portainer.Endpoint{
|
||
|
ID: endpointID,
|
||
|
Name: "test-endpoint-44",
|
||
|
Type: portainer.EdgeAgentOnDockerEnvironment,
|
||
|
URL: "https://portainer.io:9443",
|
||
|
EdgeID: "",
|
||
|
}
|
||
|
endpointRelation := portainer.EndpointRelation{
|
||
|
EndpointID: endpoint.ID,
|
||
|
}
|
||
|
|
||
|
err = createEndpoint(handler, endpoint, endpointRelation)
|
||
|
if err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
|
||
|
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/%d/edge/status", endpoint.ID), nil)
|
||
|
if err != nil {
|
||
|
t.Fatal("request error:", err)
|
||
|
}
|
||
|
req.Header.Set(portainer.PortainerAgentEdgeIDHeader, edgeId)
|
||
|
req.Header.Set(portainer.HTTPResponseAgentPlatform, "1")
|
||
|
|
||
|
rec := httptest.NewRecorder()
|
||
|
handler.ServeHTTP(rec, req)
|
||
|
|
||
|
if rec.Code != http.StatusOK {
|
||
|
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d with empty edge ID", http.StatusOK, rec.Code))
|
||
|
}
|
||
|
|
||
|
updatedEndpoint, err := handler.DataStore.Endpoint().Endpoint(endpoint.ID)
|
||
|
if err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
|
||
|
assert.Equal(t, updatedEndpoint.EdgeID, edgeId)
|
||
|
}
|
||
|
|
||
|
func TestEdgeStackStatus(t *testing.T) {
|
||
|
handler, teardown, err := setupHandler(t)
|
||
|
defer teardown()
|
||
|
|
||
|
if err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
|
||
|
endpointID := portainer.EndpointID(7)
|
||
|
endpoint := portainer.Endpoint{
|
||
|
ID: endpointID,
|
||
|
Name: "test-endpoint-7",
|
||
|
Type: portainer.EdgeAgentOnDockerEnvironment,
|
||
|
URL: "https://portainer.io:9443",
|
||
|
EdgeID: "edge-id",
|
||
|
LastCheckInDate: time.Now().Unix(),
|
||
|
}
|
||
|
|
||
|
edgeStackID := portainer.EdgeStackID(17)
|
||
|
edgeStack := portainer.EdgeStack{
|
||
|
ID: edgeStackID,
|
||
|
Name: "test-edge-stack-17",
|
||
|
Status: map[portainer.EndpointID]portainer.EdgeStackStatus{
|
||
|
endpointID: {Type: portainer.StatusOk, Error: "", EndpointID: endpoint.ID},
|
||
|
},
|
||
|
CreationDate: time.Now().Unix(),
|
||
|
EdgeGroups: []portainer.EdgeGroupID{1, 2},
|
||
|
ProjectPath: "/project/path",
|
||
|
EntryPoint: "entrypoint",
|
||
|
Version: 237,
|
||
|
ManifestPath: "/manifest/path",
|
||
|
DeploymentType: 1,
|
||
|
}
|
||
|
|
||
|
endpointRelation := portainer.EndpointRelation{
|
||
|
EndpointID: endpoint.ID,
|
||
|
EdgeStacks: map[portainer.EdgeStackID]bool{
|
||
|
edgeStack.ID: true,
|
||
|
},
|
||
|
}
|
||
|
handler.DataStore.EdgeStack().Create(edgeStack.ID, &edgeStack)
|
||
|
|
||
|
err = createEndpoint(handler, endpoint, endpointRelation)
|
||
|
if err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
|
||
|
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/%d/edge/status", endpoint.ID), nil)
|
||
|
if err != nil {
|
||
|
t.Fatal("request error:", err)
|
||
|
}
|
||
|
req.Header.Set(portainer.PortainerAgentEdgeIDHeader, "edge-id")
|
||
|
req.Header.Set(portainer.HTTPResponseAgentPlatform, "1")
|
||
|
|
||
|
rec := httptest.NewRecorder()
|
||
|
handler.ServeHTTP(rec, req)
|
||
|
|
||
|
if rec.Code != http.StatusOK {
|
||
|
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d", http.StatusOK, rec.Code))
|
||
|
}
|
||
|
|
||
|
var data endpointEdgeStatusInspectResponse
|
||
|
err = json.NewDecoder(rec.Body).Decode(&data)
|
||
|
if err != nil {
|
||
|
t.Fatal("error decoding response:", err)
|
||
|
}
|
||
|
|
||
|
assert.Len(t, data.Stacks, 1)
|
||
|
assert.Equal(t, edgeStack.ID, data.Stacks[0].ID)
|
||
|
assert.Equal(t, edgeStack.Version, data.Stacks[0].Version)
|
||
|
}
|
||
|
|
||
|
func TestEdgeJobsResponse(t *testing.T) {
|
||
|
handler, teardown, err := setupHandler(t)
|
||
|
defer teardown()
|
||
|
|
||
|
if err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
|
||
|
endpointID := portainer.EndpointID(77)
|
||
|
endpoint := portainer.Endpoint{
|
||
|
ID: endpointID,
|
||
|
Name: "test-endpoint-77",
|
||
|
Type: portainer.EdgeAgentOnDockerEnvironment,
|
||
|
URL: "https://portainer.io:9443",
|
||
|
EdgeID: "edge-id",
|
||
|
LastCheckInDate: time.Now().Unix(),
|
||
|
}
|
||
|
|
||
|
endpointRelation := portainer.EndpointRelation{
|
||
|
EndpointID: endpoint.ID,
|
||
|
}
|
||
|
|
||
|
err = createEndpoint(handler, endpoint, endpointRelation)
|
||
|
if err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
|
||
|
path, err := handler.FileService.StoreEdgeJobFileFromBytes("test-script", []byte("pwd"))
|
||
|
if err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
|
||
|
edgeJobID := portainer.EdgeJobID(35)
|
||
|
edgeJob := portainer.EdgeJob{
|
||
|
ID: edgeJobID,
|
||
|
Created: time.Now().Unix(),
|
||
|
CronExpression: "* * * * *",
|
||
|
Name: "test-edge-job",
|
||
|
ScriptPath: path,
|
||
|
Recurring: true,
|
||
|
Version: 57,
|
||
|
}
|
||
|
|
||
|
handler.ReverseTunnelService.AddEdgeJob(endpoint.ID, &edgeJob)
|
||
|
|
||
|
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/%d/edge/status", endpoint.ID), nil)
|
||
|
if err != nil {
|
||
|
t.Fatal("request error:", err)
|
||
|
}
|
||
|
req.Header.Set(portainer.PortainerAgentEdgeIDHeader, "edge-id")
|
||
|
req.Header.Set(portainer.HTTPResponseAgentPlatform, "1")
|
||
|
|
||
|
rec := httptest.NewRecorder()
|
||
|
handler.ServeHTTP(rec, req)
|
||
|
|
||
|
if rec.Code != http.StatusOK {
|
||
|
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d", http.StatusOK, rec.Code))
|
||
|
}
|
||
|
|
||
|
var data endpointEdgeStatusInspectResponse
|
||
|
err = json.NewDecoder(rec.Body).Decode(&data)
|
||
|
if err != nil {
|
||
|
t.Fatal("error decoding response:", err)
|
||
|
}
|
||
|
|
||
|
assert.Len(t, data.Schedules, 1)
|
||
|
assert.Equal(t, edgeJob.ID, data.Schedules[0].ID)
|
||
|
assert.Equal(t, edgeJob.CronExpression, data.Schedules[0].CronExpression)
|
||
|
assert.Equal(t, edgeJob.Version, data.Schedules[0].Version)
|
||
|
}
|