refactor(deployment): Remove WireGuard VPN dependency and restore public service access

Remove WireGuard integration from production deployment to simplify infrastructure:
- Remove docker-compose-direct-access.yml (VPN-bound services)
- Remove VPN-only middlewares from Grafana, Prometheus, Portainer
- Remove WireGuard middleware definitions from Traefik
- Remove WireGuard IPs (10.8.0.0/24) from Traefik forwarded headers

All monitoring services now publicly accessible via subdomains:
- grafana.michaelschiemer.de (with Grafana native auth)
- prometheus.michaelschiemer.de (with Basic Auth)
- portainer.michaelschiemer.de (with Portainer native auth)

All services use Let's Encrypt SSL certificates via Traefik.
This commit is contained in:
2025-11-05 12:48:25 +01:00
parent 7c52065aae
commit 95147ff23e
215 changed files with 29490 additions and 368 deletions

View File

@@ -0,0 +1,634 @@
<?php
declare(strict_types=1);
use App\Framework\ApiGateway\ApiGateway;
use App\Framework\ApiGateway\ApiRequest;
use App\Framework\ApiGateway\HasAuth;
use App\Framework\ApiGateway\HasPayload;
use App\Framework\ApiGateway\ValueObjects\ApiEndpoint;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Http\Headers;
use App\Framework\Http\Method as HttpMethod;
use App\Framework\Http\Url\Url;
use App\Framework\HttpClient\AuthConfig;
use App\Framework\HttpClient\ClientRequest;
use App\Framework\HttpClient\ClientResponse;
use App\Framework\HttpClient\HttpClient;
use App\Framework\HttpClient\Status;
use App\Framework\Retry\RetryStrategy;
describe('ApiGateway', function () {
beforeEach(function () {
// Track requests for assertion
$this->capturedRequest = null;
// Create mock HttpClient that captures request details
$this->httpClient = new class($this) implements HttpClient {
private $testContext;
public function __construct($testContext)
{
$this->testContext = $testContext;
}
public function send(ClientRequest $request): ClientResponse
{
// Capture request for assertions
$this->testContext->capturedRequest = $request;
// Mock successful response
return new ClientResponse(
status: Status::OK,
headers: new Headers(['Content-Type' => 'application/json']),
body: '{"success": true}'
);
}
};
// Create mock dependencies for ApiGateway
// Create mock dependencies for CircuitBreakerManager
$mockCache = new class implements \App\Framework\Cache\Cache {
public function get(\App\Framework\Cache\CacheIdentifier ...$identifiers): \App\Framework\Cache\CacheResult
{
return new \App\Framework\Cache\CacheResult(hits: [], misses: $identifiers);
}
public function set(\App\Framework\Cache\CacheItem ...$items): bool { return true; }
public function has(\App\Framework\Cache\CacheIdentifier ...$identifiers): array { return []; }
public function delete(\App\Framework\Cache\CacheIdentifier ...$identifiers): bool { return true; }
public function forget(\App\Framework\Cache\CacheIdentifier ...$identifiers): bool { return true; }
public function clear(): bool { return true; }
public function flush(): bool { return true; }
public function remember(\App\Framework\Cache\CacheKey $key, callable $callback, ?\App\Framework\Core\ValueObjects\Duration $ttl = null): \App\Framework\Cache\CacheItem
{
$value = $callback();
return new \App\Framework\Cache\CacheItem($key, $value, $ttl);
}
};
$mockClock = new class implements \App\Framework\DateTime\Clock {
public function now(): \DateTimeImmutable { return new \DateTimeImmutable(); }
public function fromTimestamp(\App\Framework\Core\ValueObjects\Timestamp $timestamp): \DateTimeImmutable {
return new \DateTimeImmutable('@' . $timestamp->toUnixTimestamp());
}
public function fromString(string $dateTime, ?string $format = null): \DateTimeImmutable {
return new \DateTimeImmutable($dateTime);
}
public function today(): \DateTimeImmutable { return new \DateTimeImmutable('today'); }
public function yesterday(): \DateTimeImmutable { return new \DateTimeImmutable('yesterday'); }
public function tomorrow(): \DateTimeImmutable { return new \DateTimeImmutable('tomorrow'); }
public function time(): \App\Framework\Core\ValueObjects\Timestamp {
return \App\Framework\Core\ValueObjects\Timestamp::now();
}
};
$this->circuitBreakerManager = new \App\Framework\CircuitBreaker\CircuitBreakerManager(
cache: $mockCache,
clock: $mockClock
);
$this->metrics = new \App\Framework\ApiGateway\Metrics\ApiMetrics();
$this->operationTracker = new class implements \App\Framework\Performance\OperationTracker {
public function startOperation(
string $operationId,
\App\Framework\Performance\PerformanceCategory $category,
array $contextData = []
): \App\Framework\Performance\PerformanceSnapshot {
return new \App\Framework\Performance\PerformanceSnapshot(
operationId: $operationId,
category: $category,
startTime: microtime(true),
duration: \App\Framework\Core\ValueObjects\Duration::fromMilliseconds(10),
memoryUsed: 1024,
peakMemory: 2048,
contextData: $contextData
);
}
public function completeOperation(string $operationId): ?\App\Framework\Performance\PerformanceSnapshot {
return new \App\Framework\Performance\PerformanceSnapshot(
operationId: $operationId,
category: \App\Framework\Performance\PerformanceCategory::HTTP,
startTime: microtime(true) - 0.01,
duration: \App\Framework\Core\ValueObjects\Duration::fromMilliseconds(10),
memoryUsed: 1024,
peakMemory: 2048,
contextData: []
);
}
public function failOperation(string $operationId, \Throwable $exception): ?\App\Framework\Performance\PerformanceSnapshot {
return null;
}
};
$this->apiGateway = new ApiGateway(
$this->httpClient,
$this->circuitBreakerManager,
$this->metrics,
$this->operationTracker
);
});
describe('HasAuth Interface Integration', function () {
it('applies Basic authentication when ApiRequest implements HasAuth', function () {
$request = new class implements ApiRequest, HasAuth {
public function getEndpoint(): ApiEndpoint
{
return ApiEndpoint::fromUrl(Url::parse('https://api.example.com/test'));
}
public function getMethod(): HttpMethod
{
return HttpMethod::GET;
}
public function getTimeout(): Duration
{
return Duration::fromSeconds(10);
}
public function getRetryStrategy(): ?RetryStrategy
{
return null;
}
public function getAuth(): AuthConfig
{
return AuthConfig::basic('testuser', 'testpass');
}
public function getHeaders(): Headers
{
return new Headers(['Accept' => 'application/json']);
}
public function getRequestName(): string
{
return 'test.basic_auth';
}
};
$response = $this->apiGateway->send($request);
expect($response->status)->toBe(Status::OK);
expect($this->capturedRequest->options->auth)->not->toBeNull();
expect($this->capturedRequest->options->auth->type)->toBe('basic');
});
it('applies custom header authentication when ApiRequest uses AuthConfig::custom()', function () {
$request = new class implements ApiRequest, HasAuth {
public function getEndpoint(): ApiEndpoint
{
return ApiEndpoint::fromUrl(Url::parse('https://api.example.com/test'));
}
public function getMethod(): HttpMethod
{
return HttpMethod::GET;
}
public function getTimeout(): Duration
{
return Duration::fromSeconds(10);
}
public function getRetryStrategy(): ?RetryStrategy
{
return null;
}
public function getAuth(): AuthConfig
{
return AuthConfig::custom([
'X-API-Key' => 'test-api-key-123',
]);
}
public function getHeaders(): Headers
{
return new Headers(['Accept' => 'application/json']);
}
public function getRequestName(): string
{
return 'test.custom_auth';
}
};
$response = $this->apiGateway->send($request);
expect($response->status)->toBe(Status::OK);
expect($this->capturedRequest->options->auth)->not->toBeNull();
expect($this->capturedRequest->options->auth->type)->toBe('custom');
});
it('does not apply authentication when ApiRequest does not implement HasAuth', function () {
$request = new class implements ApiRequest {
public function getEndpoint(): ApiEndpoint
{
return ApiEndpoint::fromUrl(Url::parse('https://api.example.com/test'));
}
public function getMethod(): HttpMethod
{
return HttpMethod::GET;
}
public function getTimeout(): Duration
{
return Duration::fromSeconds(10);
}
public function getRetryStrategy(): ?RetryStrategy
{
return null;
}
public function getHeaders(): Headers
{
return new Headers(['Accept' => 'application/json']);
}
public function getRequestName(): string
{
return 'test.no_auth';
}
};
$response = $this->apiGateway->send($request);
expect($response->status)->toBe(Status::OK);
// No auth should be applied
expect($this->capturedRequest->options->auth ?? null)->toBeNull();
});
});
describe('HasPayload Interface Integration', function () {
it('includes payload when ApiRequest implements HasPayload', function () {
$request = new class implements ApiRequest, HasPayload, HasAuth {
public function getEndpoint(): ApiEndpoint
{
return ApiEndpoint::fromUrl(Url::parse('https://api.example.com/test'));
}
public function getMethod(): HttpMethod
{
return HttpMethod::POST;
}
public function getPayload(): array
{
return [
'name' => 'Test User',
'email' => 'test@example.com',
];
}
public function getTimeout(): Duration
{
return Duration::fromSeconds(10);
}
public function getRetryStrategy(): ?RetryStrategy
{
return null;
}
public function getAuth(): AuthConfig
{
return AuthConfig::basic('testuser', 'testpass');
}
public function getHeaders(): Headers
{
return new Headers([
'Content-Type' => 'application/json',
'Accept' => 'application/json',
]);
}
public function getRequestName(): string
{
return 'test.with_payload';
}
};
$response = $this->apiGateway->send($request);
expect($response->status)->toBe(Status::OK);
$bodyData = json_decode($this->capturedRequest->body, true);
expect($bodyData)->toBe([
'name' => 'Test User',
'email' => 'test@example.com',
]);
});
it('does not include payload when ApiRequest does not implement HasPayload', function () {
$request = new class implements ApiRequest, HasAuth {
public function getEndpoint(): ApiEndpoint
{
return ApiEndpoint::fromUrl(Url::parse('https://api.example.com/test'));
}
public function getMethod(): HttpMethod
{
return HttpMethod::GET;
}
public function getTimeout(): Duration
{
return Duration::fromSeconds(10);
}
public function getRetryStrategy(): ?RetryStrategy
{
return null;
}
public function getAuth(): AuthConfig
{
return AuthConfig::basic('testuser', 'testpass');
}
public function getHeaders(): Headers
{
return new Headers(['Accept' => 'application/json']);
}
public function getRequestName(): string
{
return 'test.no_payload';
}
};
$response = $this->apiGateway->send($request);
expect($response->status)->toBe(Status::OK);
// No payload should be sent for GET request
expect($this->capturedRequest->body)->toBeEmpty();
});
});
describe('Request Name Tracking', function () {
it('uses request name from ApiRequest', function () {
$request = new class implements ApiRequest {
public function getEndpoint(): ApiEndpoint
{
return ApiEndpoint::fromUrl(Url::parse('https://api.example.com/test'));
}
public function getMethod(): HttpMethod
{
return HttpMethod::GET;
}
public function getTimeout(): Duration
{
return Duration::fromSeconds(10);
}
public function getRetryStrategy(): ?RetryStrategy
{
return null;
}
public function getHeaders(): Headers
{
return new Headers(['Accept' => 'application/json']);
}
public function getRequestName(): string
{
return 'custom.request.name';
}
};
expect($request->getRequestName())->toBe('custom.request.name');
});
});
describe('HTTP Method Support', function () {
it('supports GET requests', function () {
$request = new class implements ApiRequest {
public function getEndpoint(): ApiEndpoint
{
return ApiEndpoint::fromUrl(Url::parse('https://api.example.com/test'));
}
public function getMethod(): HttpMethod
{
return HttpMethod::GET;
}
public function getTimeout(): Duration
{
return Duration::fromSeconds(10);
}
public function getRetryStrategy(): ?RetryStrategy
{
return null;
}
public function getHeaders(): Headers
{
return new Headers(['Accept' => 'application/json']);
}
public function getRequestName(): string
{
return 'test.get';
}
};
$response = $this->apiGateway->send($request);
expect($response->status)->toBe(Status::OK);
expect($this->capturedRequest->method)->toBe('GET');
});
it('supports POST requests with payload', function () {
$request = new class implements ApiRequest, HasPayload {
public function getEndpoint(): ApiEndpoint
{
return ApiEndpoint::fromUrl(Url::parse('https://api.example.com/test'));
}
public function getMethod(): HttpMethod
{
return HttpMethod::POST;
}
public function getPayload(): array
{
return ['data' => 'test'];
}
public function getTimeout(): Duration
{
return Duration::fromSeconds(10);
}
public function getRetryStrategy(): ?RetryStrategy
{
return null;
}
public function getHeaders(): Headers
{
return new Headers(['Content-Type' => 'application/json']);
}
public function getRequestName(): string
{
return 'test.post';
}
};
$response = $this->apiGateway->send($request);
expect($response->status)->toBe(Status::OK);
expect($this->capturedRequest->method)->toBe('POST');
$bodyData = json_decode($this->capturedRequest->body, true);
expect($bodyData)->toBe(['data' => 'test']);
});
it('supports DELETE requests', function () {
$request = new class implements ApiRequest, HasAuth {
public function getEndpoint(): ApiEndpoint
{
return ApiEndpoint::fromUrl(Url::parse('https://api.example.com/test/123'));
}
public function getMethod(): HttpMethod
{
return HttpMethod::DELETE;
}
public function getTimeout(): Duration
{
return Duration::fromSeconds(10);
}
public function getRetryStrategy(): ?RetryStrategy
{
return null;
}
public function getAuth(): AuthConfig
{
return AuthConfig::basic('user', 'pass');
}
public function getHeaders(): Headers
{
return new Headers(['Accept' => 'application/json']);
}
public function getRequestName(): string
{
return 'test.delete';
}
};
$response = $this->apiGateway->send($request);
expect($response->status)->toBe(Status::OK);
expect($this->capturedRequest->method)->toBe('DELETE');
});
it('supports PATCH requests with payload', function () {
$request = new class implements ApiRequest, HasPayload, HasAuth {
public function getEndpoint(): ApiEndpoint
{
return ApiEndpoint::fromUrl(Url::parse('https://api.example.com/test/123'));
}
public function getMethod(): HttpMethod
{
return HttpMethod::PATCH;
}
public function getPayload(): array
{
return ['name' => 'Updated'];
}
public function getTimeout(): Duration
{
return Duration::fromSeconds(10);
}
public function getRetryStrategy(): ?RetryStrategy
{
return null;
}
public function getAuth(): AuthConfig
{
return AuthConfig::basic('user', 'pass');
}
public function getHeaders(): Headers
{
return new Headers(['Content-Type' => 'application/json']);
}
public function getRequestName(): string
{
return 'test.patch';
}
};
$response = $this->apiGateway->send($request);
expect($response->status)->toBe(Status::OK);
expect($this->capturedRequest->method)->toBe('PATCH');
$bodyData = json_decode($this->capturedRequest->body, true);
expect($bodyData)->toBe(['name' => 'Updated']);
});
});
describe('Headers Configuration', function () {
it('includes custom headers from ApiRequest', function () {
$request = new class implements ApiRequest {
public function getEndpoint(): ApiEndpoint
{
return ApiEndpoint::fromUrl(Url::parse('https://api.example.com/test'));
}
public function getMethod(): HttpMethod
{
return HttpMethod::GET;
}
public function getTimeout(): Duration
{
return Duration::fromSeconds(10);
}
public function getRetryStrategy(): ?RetryStrategy
{
return null;
}
public function getHeaders(): Headers
{
return new Headers([
'Accept' => 'application/json',
'X-Custom-Header' => 'custom-value',
'X-API-Version' => '2.0',
]);
}
public function getRequestName(): string
{
return 'test.custom_headers';
}
};
$headers = $request->getHeaders();
expect($headers->get('Accept'))->toBe(['application/json']);
expect($headers->get('X-Custom-Header'))->toBe(['custom-value']);
expect($headers->get('X-API-Version'))->toBe(['2.0']);
});
});
});