You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
portainer/api/http/handler/endpointedge/endpoint_edgestatus_inspect...

446 lines
11 KiB

package endpointedge
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"os"
"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.StatusBadRequest,
},
{
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() (*Handler, func(), error) {
tmpDir, err := os.MkdirTemp(os.TempDir(), "portainer-test")
if err != nil {
return nil, nil, fmt.Errorf("could not create a tmp dir: %w", err)
}
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(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()
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()
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, "edge-id")
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()
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")
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()
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()
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")
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()
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")
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)
}