Skip to content

Commit

Permalink
Merge branch 'master' into 789-capture-wave-metrics-by-container-arch…
Browse files Browse the repository at this point in the history
…itecture
  • Loading branch information
munishchouhan authored Mar 4, 2025
2 parents 467162a + 1cf23c9 commit aa8a38e
Show file tree
Hide file tree
Showing 41 changed files with 562 additions and 68 deletions.
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.18.1
1.18.4
11 changes: 11 additions & 0 deletions changelog.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,15 @@
# Wave changelog
1.18.4 - 3 Mar 2025
- Add robots and favicon files [40c91f5f]
- Improve errors and warns logging [216b8227]

1.18.3 - 28 Feb 2025
- Add DenyCrawlerFilter (#803) [edfae007]

1.18.2 - 27 Feb 2025
- Use post for container scan (#802) [1d4bc194]
- Wv 102 merge https docs.seqera.io wave api into openapi (#800) [206881ec]

1.18.1 - 21 Feb 2025
- Add denyHosts to pairing websocket [f5369eed]
- Use virtual threads for build, scan and mirror jobs (#742) [9dfce1f7]
Expand Down
25 changes: 16 additions & 9 deletions src/main/groovy/io/seqera/wave/ErrorHandler.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

package io.seqera.wave

import groovy.transform.CompileStatic
import groovy.util.logging.Slf4j
import io.micronaut.context.annotation.Value
import io.micronaut.http.HttpRequest
Expand All @@ -36,6 +37,7 @@ import io.seqera.wave.exception.SlowDownException
import io.seqera.wave.exception.UnauthorizedException
import io.seqera.wave.exception.WaveException
import io.seqera.wave.util.LongRndKey
import io.seqera.wave.util.RegHelper
import jakarta.inject.Singleton
/**
* Common error handling logic
Expand All @@ -44,6 +46,7 @@ import jakarta.inject.Singleton
*/
@Slf4j
@Singleton
@CompileStatic
class ErrorHandler {

static interface Mapper<T> {
Expand All @@ -53,17 +56,17 @@ class ErrorHandler {
@Value('${wave.debug:false}')
private Boolean debug

def <T> HttpResponse<T> handle(HttpRequest httpRequest, Throwable t, Mapper<T> responseFactory) {
<T> HttpResponse<T> handle(HttpRequest request, Throwable t, Mapper<T> responseFactory) {
final errId = LongRndKey.rndHex()
final request = httpRequest?.toString()
final knownException = t instanceof WaveException || t instanceof HttpStatusException
def msg = t.message
String msg = t.message
if( knownException && msg ) {
// the the error cause
if( t.cause ) msg += " - Cause: ${t.cause.message ?: t.cause}".toString()
// render the message for logging
def render = msg
if( request ) render += " - Request: ${request}"
String render = msg
if( request )
render += toString(request)
if( !debug ) {
log.warn(render)
}
Expand All @@ -78,8 +81,9 @@ class ErrorHandler {
msg = "Oops... Unable to process request"
msg += " - Error ID: ${errId}"
// render the message for logging
def render = msg
if( request ) render += " - Request: ${request}"
String render = msg
if( request )
render += toString(request)
log.error(render, t)
}

Expand All @@ -92,10 +96,10 @@ class ErrorHandler {

if( t instanceof RegistryForwardException ) {
// report this error as it has been returned by the target registry
return HttpResponse
return (HttpResponse<T>) HttpResponse
.status(HttpStatus.valueOf(t.statusCode))
.body(t.response)
.headers(t.headers)
.headers(t.headers as Map<CharSequence,CharSequence>)
}

if( t instanceof DockerRegistryException ) {
Expand Down Expand Up @@ -143,4 +147,7 @@ class ErrorHandler {

}

static String toString(HttpRequest request) {
"\n- Request: [${request.methodName}] ${request.uri}\n- Headers:${RegHelper.dumpHeaders(request)}"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import io.micronaut.core.annotation.Nullable
import groovy.util.logging.Slf4j
import io.micronaut.context.annotation.Value
import io.micronaut.http.HttpResponse
import io.micronaut.http.MediaType
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import io.micronaut.scheduling.TaskExecutors
Expand Down Expand Up @@ -59,9 +60,20 @@ class ServiceInfoController {
: HttpResponse.badRequest()
}

@Get(uri = "/openapi")
@Get("/openapi")
HttpResponse getOpenAPI() {
HttpResponse.redirect(URI.create("/openapi/"))
}

@Get(uri = "/favicon.ico", produces = MediaType.IMAGE_X_ICON)
HttpResponse getFavicon() {
final inputStream = getClass().getResourceAsStream("/io/seqera/wave/assets/wave.ico");
return inputStream != null ? HttpResponse.ok(inputStream) : HttpResponse.notFound();
}

@Get(uri = "/robots.txt", produces = MediaType.TEXT_PLAIN)
HttpResponse getRobotsTxt() {
final inputStream = getClass().getResourceAsStream("/io/seqera/wave/assets/robots.txt");
return inputStream != null ? HttpResponse.ok(inputStream) : HttpResponse.notFound();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,10 @@ import io.micronaut.core.annotation.Nullable
import io.micronaut.http.HttpRequest
import io.micronaut.http.HttpResponse
import io.micronaut.http.HttpStatus
import io.micronaut.http.annotation.Body
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import io.micronaut.http.annotation.Post
import io.micronaut.http.annotation.QueryValue
import io.micronaut.http.client.HttpClient
import io.micronaut.http.client.annotation.Client
Expand All @@ -56,7 +58,6 @@ import io.seqera.wave.service.scan.ScanEntry
import io.seqera.wave.service.scan.ScanVulnerability
import io.seqera.wave.util.JacksonHelper
import jakarta.inject.Inject
import jakarta.ws.rs.QueryParam
import org.reactivestreams.Publisher
import org.reactivestreams.Subscriber
import org.reactivestreams.Subscription
Expand Down Expand Up @@ -315,8 +316,8 @@ class ViewController {
* @return
* The redirect response to the scan view for the requested container image
*/
@Get('/scans')
Publisher<HttpResponse> requestScan(@QueryParam String image) {
@Post('/scans')
Publisher<HttpResponse> requestScan(@Body String image) {
final req = new SubmitContainerTokenRequest(containerImage: image, scanMode: ScanMode.required)
final post = HttpRequest.POST("/v1alpha2/container", req)
final resp = httpClient.retrieve(post, SubmitContainerTokenResponse)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ class RegistryProxyService {
log.warn "Unexpected redirect location '${redirect}' with status code: ${status}"
}
else if( status>=300 && status<400 ) {
log.warn "Unexpected redirect status code: ${status}; headers: ${RegHelper.dumpHeaders(resp1.headers())}"
log.warn "Unexpected redirect status code: ${status}; headers: ${RegHelper.dumpHeaders(resp1)}"
}

final len = resp1.headers().firstValueAsLong('Content-Length').orElse(0)
Expand Down Expand Up @@ -331,7 +331,7 @@ class RegistryProxyService {
final resp = proxyClient.head(route.path, WaveDefault.ACCEPT_HEADERS)
final result = resp.headers().firstValue('docker-content-digest').orElse(null)
if( !result && (resp.statusCode()!=404 || retryOnNotFound) ) {
log.warn "Unable to retrieve digest for image '$image' -- response status=${resp.statusCode()}; headers:\n${RegHelper.dumpHeaders(resp.headers())}"
log.warn "Unable to retrieve digest for image '$image' -- response status=${resp.statusCode()}; headers:\n${RegHelper.dumpHeaders(resp)}"
}
return result
}
Expand Down
79 changes: 79 additions & 0 deletions src/main/groovy/io/seqera/wave/filter/DenyCrawlerFilter.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* Wave, containers provisioning service
* Copyright (c) 2023-2024, Seqera Labs
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

package io.seqera.wave.filter

import groovy.transform.CompileStatic
import groovy.util.logging.Slf4j
import io.micronaut.http.HttpRequest
import io.micronaut.http.HttpResponse
import io.micronaut.http.HttpStatus
import io.micronaut.http.MutableHttpResponse
import io.micronaut.http.annotation.Filter
import io.micronaut.http.filter.HttpServerFilter
import io.micronaut.http.filter.ServerFilterChain
import io.seqera.wave.util.RegHelper
import org.reactivestreams.Publisher
import reactor.core.publisher.Flux
/**
* Block the access to known crawler bots
*
* @author Paolo Di Tommaso <[email protected]>
*/
@Slf4j
@CompileStatic
@Filter("/**")
class DenyCrawlerFilter implements HttpServerFilter {

private static final List<String> CRAWLER_AGENTS = Arrays.asList(
"googlebot",
"bingbot",
"yandexbot",
"baiduspider",
"duckduckbot",
"slurp",
"facebot",
"twitterbot",
"mj12bot",
"ahrefsbot"
)

static boolean isCrawler(String userAgent) {
return userAgent
? CRAWLER_AGENTS.stream().anyMatch(userAgent::contains)
: false
}

@Override
Publisher<MutableHttpResponse<?>> doFilter(HttpRequest<?> request, ServerFilterChain chain) {
final userAgent = request.getHeaders().get("User-Agent")?.toLowerCase()
// Check if the request path matches any of the ignored paths
if (isCrawler(userAgent) && request.path!='/robots.txt') {
// Return immediately without processing the request
log.warn("Request denied [${request.methodName}] ${request.uri}\n- Headers:${RegHelper.dumpHeaders(request)}")
return Flux.just(HttpResponse.status(HttpStatus.METHOD_NOT_ALLOWED))
}
// Continue processing the request
return chain.proceed(request)
}

@Override
int getOrder() {
return FilterOrder.DENY_CRAWLER
}
}
3 changes: 2 additions & 1 deletion src/main/groovy/io/seqera/wave/filter/DenyPathsFilter.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import io.micronaut.http.MutableHttpResponse
import io.micronaut.http.annotation.Filter
import io.micronaut.http.filter.HttpServerFilter;
import io.micronaut.http.filter.ServerFilterChain
import io.seqera.wave.util.RegHelper
import org.reactivestreams.Publisher
import reactor.core.publisher.Flux

Expand All @@ -53,7 +54,7 @@ class DenyPathsFilter implements HttpServerFilter {
// Check if the request path matches any of the ignored paths
if (isDeniedPath(request.path, deniedPaths)) {
// Return immediately without processing the request
log.debug("Request denied: ${request}")
log.warn("Request denied [${request.methodName}] ${request.uri}\n- Headers:${RegHelper.dumpHeaders(request)}")
return Flux.just(HttpResponse.status(HttpStatus.METHOD_NOT_ALLOWED))
}
// Continue processing the request
Expand Down
1 change: 1 addition & 0 deletions src/main/groovy/io/seqera/wave/filter/FilterOrder.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ package io.seqera.wave.filter
*/
interface FilterOrder {

final int DENY_CRAWLER = -110
final int DENY_PATHS = -100
final int RATE_LIMITER = -50
final int PULL_METRICS = 10
Expand Down
2 changes: 1 addition & 1 deletion src/main/groovy/io/seqera/wave/proxy/ProxyClient.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ class ProxyClient {
}
if( result.statusCode() in HTTP_REDIRECT_CODES && followRedirect ) {
final redirect = result.headers().firstValue('location').orElse(null)
log.trace "Redirecting (${++redirectCount}) $target ==> $redirect ${RegHelper.dumpHeaders(result.headers())}"
log.trace "Redirecting (${++redirectCount}) $target ==> $redirect ${RegHelper.dumpHeaders(result)}"
if( !redirect ) {
final msg = "Missing `Location` header for request URI '$target' ― origin request '$origin'"
throw new ClientResponseException(msg, result.request())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ interface MessageStream<M> {
/**
* Initialize the stream with the given Id
*
* @param streamId The uniqur ID of the stream to be initialized
* @param streamId The unique ID of the stream to be initialized
*/
void init(String streamId)

Expand Down
17 changes: 14 additions & 3 deletions src/main/groovy/io/seqera/wave/util/RegHelper.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@

package io.seqera.wave.util

import java.net.http.HttpHeaders
import java.net.http.HttpResponse
import java.nio.charset.Charset
import java.nio.file.Files
Expand Down Expand Up @@ -105,8 +104,20 @@ class RegHelper {
}
}

static String dumpHeaders(HttpHeaders headers) {
return dumpHeaders(headers.map())
static String dumpHeaders(io.micronaut.http.HttpRequest request) {
dumpHeaders(request.getHeaders().asMap())
}

static String dumpHeaders(io.micronaut.http.HttpResponse response) {
dumpHeaders(response.getHeaders().asMap())
}

static String dumpHeaders(java.net.http.HttpRequest request) {
return dumpHeaders(request.headers().map())
}

static String dumpHeaders(java.net.http.HttpResponse response) {
return dumpHeaders(response.headers().map())
}

static String dumpHeaders(Map<String, List<String>> headers) {
Expand Down
2 changes: 2 additions & 0 deletions src/main/resources/io/seqera/wave/assets/robots.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
User-agent: *
Disallow: /
Binary file added src/main/resources/io/seqera/wave/assets/wave.ico
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ import io.micronaut.test.annotation.MockBean
import io.micronaut.test.extensions.spock.annotation.MicronautTest
import io.seqera.wave.api.ContainerInspectRequest
import io.seqera.wave.api.ContainerInspectResponse
import io.seqera.wave.exception.BadRequestException
import io.seqera.wave.service.logs.BuildLogService
import io.seqera.wave.service.logs.BuildLogServiceImpl
import jakarta.inject.Inject
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,11 +92,10 @@ class ScanControllerTest extends Specification {
res.body().requestId == scan.requestId
}


def "should return 404 and null"() {
when:
def req = HttpRequest.GET("/v1alpha1/scans/unknown")
def res = client.toBlocking().exchange(req, WaveScanRecord)
client.toBlocking().exchange(req, WaveScanRecord)

then:
def e = thrown(HttpClientResponseException)
Expand Down
Loading

0 comments on commit aa8a38e

Please sign in to comment.