Skip to content

Commit

Permalink
feat: 支持挂载RemoteGeneric仓库 TencentBlueKing#1462 (TencentBlueKing#1464)
Browse files Browse the repository at this point in the history
* feat: 支持自定义RemoteGeneric仓库dns解析 TencentBlueKing#1462

* feat: 支持FSServer查询RemoteGeneric仓库 TencentBlueKing#1462

* feat: 支持RemoteGeneric仓库分片下载 TencentBlueKing#1462

* feat: 支持FsServer下载RemoteGeneric制品时重定向 TencentBlueKing#1462

* feat: 修复查询远程仓库节点失败 TencentBlueKing#1462

* feat: 支持RemoteGeneric仓库分片下载 TencentBlueKing#1462

* feat: 支持RemoteGeneric边下载边缓存 TencentBlueKing#1462

* feat: 支持RemoteGeneric仓库分片下载时触发异步缓存任务 TencentBlueKing#1462

* feat: 增加锁避免重复缓存同一远程制品 TencentBlueKing#1462
  • Loading branch information
cnlkl authored Nov 25, 2023
1 parent ac07c66 commit 07d0061
Show file tree
Hide file tree
Showing 29 changed files with 1,334 additions and 140 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ const val FS_SERVER_SERVICE_NAME = "\${service.prefix:}fs-server\${service.suffi
const val MAVEN_SERVICE_NAME = "\${service.prefix:}maven\${service.suffix:}"
const val ARCHIVE_SERVICE_NAME = "\${service.prefix:}archive\${service.suffix:}"
const val OPDATA_SERVICE_NAME = "\${service.prefix:}opdata\${service.suffix:}"
const val GENERIC_SERVICE_NAME = "\${service.prefix:}generic\${service.suffix:}"

/**
* 认证相关
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,7 @@

package com.tencent.bkrepo.common.artifact.repository.remote

import com.tencent.bkrepo.common.api.constant.HttpHeaders
import com.tencent.bkrepo.common.artifact.api.ArtifactFile
import com.tencent.bkrepo.common.artifact.pojo.configuration.remote.NetworkProxyConfiguration
import com.tencent.bkrepo.common.artifact.pojo.configuration.remote.RemoteConfiguration
import com.tencent.bkrepo.common.artifact.repository.context.ArtifactContext
import com.tencent.bkrepo.common.artifact.repository.context.ArtifactDownloadContext
Expand All @@ -46,24 +44,16 @@ import com.tencent.bkrepo.common.artifact.resolve.response.ArtifactResource
import com.tencent.bkrepo.common.artifact.stream.Range
import com.tencent.bkrepo.common.artifact.stream.artifactStream
import com.tencent.bkrepo.common.artifact.util.http.UrlFormatter
import com.tencent.bkrepo.common.service.util.okhttp.BasicAuthInterceptor
import com.tencent.bkrepo.common.service.util.okhttp.HttpClientBuilderFactory
import com.tencent.bkrepo.repository.pojo.node.NodeDetail
import com.tencent.bkrepo.repository.pojo.node.service.NodeCreateRequest
import okhttp3.Authenticator
import okhttp3.Credentials
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import okhttp3.ResponseBody
import org.slf4j.LoggerFactory
import java.net.InetSocketAddress
import java.net.Proxy
import java.time.Duration
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import java.util.concurrent.TimeUnit

/**
* 远程仓库抽象逻辑
Expand Down Expand Up @@ -224,50 +214,7 @@ abstract class RemoteRepository : AbstractArtifactRepository() {
* 创建http client
*/
protected fun createHttpClient(configuration: RemoteConfiguration, addInterceptor: Boolean = true): OkHttpClient {
val builder = HttpClientBuilderFactory.create()
builder.readTimeout(configuration.network.readTimeout, TimeUnit.MILLISECONDS)
builder.connectTimeout(configuration.network.connectTimeout, TimeUnit.MILLISECONDS)
builder.proxy(createProxy(configuration.network.proxy))
builder.proxyAuthenticator(createProxyAuthenticator(configuration.network.proxy))
if (addInterceptor) {
createAuthenticateInterceptor(configuration)?.let { builder.addInterceptor(it) }
}
builder.retryOnConnectionFailure(true)
return builder.build()
}

/**
* 创建代理
*/
private fun createProxy(configuration: NetworkProxyConfiguration?): Proxy {
return configuration?.let { Proxy(Proxy.Type.HTTP, InetSocketAddress(it.host, it.port)) } ?: Proxy.NO_PROXY
}

/**
* 创建代理身份认证
*/
private fun createProxyAuthenticator(configuration: NetworkProxyConfiguration?): Authenticator {
val username = configuration?.username
val password = configuration?.password
return if (username != null && password != null) {
Authenticator { _, response ->
response.request
.newBuilder()
.header(HttpHeaders.PROXY_AUTHORIZATION, Credentials.basic(username, password))
.build()
}
} else Authenticator.NONE
}

/**
* 创建身份认证拦截器
*/
protected open fun createAuthenticateInterceptor(configuration: RemoteConfiguration): Interceptor? {
val username = configuration.credentials.username
val password = configuration.credentials.password
return if (username != null && password != null) {
BasicAuthInterceptor(username, password)
} else null
return buildOkHttpClient(configuration, addInterceptor).build()
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* Tencent is pleased to support the open source community by making BK-CI 蓝鲸持续集成平台 available.
*
* Copyright (C) 2023 THL A29 Limited, a Tencent company. All rights reserved.
*
* BK-CI 蓝鲸持续集成平台 is licensed under the MIT license.
*
* A copy of the MIT License is included in this file.
*
*
* Terms of the MIT License:
* ---------------------------------------------------
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
* documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of
* the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
* LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
* NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

package com.tencent.bkrepo.common.artifact.repository.remote

import com.tencent.bkrepo.common.api.constant.HttpHeaders
import com.tencent.bkrepo.common.artifact.pojo.configuration.remote.NetworkProxyConfiguration
import com.tencent.bkrepo.common.artifact.pojo.configuration.remote.RemoteConfiguration
import com.tencent.bkrepo.common.artifact.pojo.configuration.remote.RemoteCredentialsConfiguration
import com.tencent.bkrepo.common.service.util.okhttp.BasicAuthInterceptor
import com.tencent.bkrepo.common.service.util.okhttp.HttpClientBuilderFactory
import okhttp3.Authenticator
import okhttp3.Credentials
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import java.net.InetSocketAddress
import java.net.Proxy
import java.util.concurrent.TimeUnit

/**
* 创建代理
*/
fun createProxy(configuration: NetworkProxyConfiguration?): Proxy {
return configuration?.let { Proxy(Proxy.Type.HTTP, InetSocketAddress(it.host, it.port)) } ?: Proxy.NO_PROXY
}

/**
* 创建代理身份认证
*/
fun createProxyAuthenticator(configuration: NetworkProxyConfiguration?): Authenticator {
val username = configuration?.username
val password = configuration?.password
return if (username != null && password != null) {
Authenticator { _, response ->
response.request
.newBuilder()
.header(HttpHeaders.PROXY_AUTHORIZATION, Credentials.basic(username, password))
.build()
}
} else Authenticator.NONE
}

/**
* 创建身份认证拦截器
*/
fun createAuthenticateInterceptor(configuration: RemoteCredentialsConfiguration): Interceptor? {
val username = configuration.username
val password = configuration.password
return if (username != null && password != null) {
BasicAuthInterceptor(username, password)
} else {
null
}
}

fun buildOkHttpClient(configuration: RemoteConfiguration, addInterceptor: Boolean = true): OkHttpClient.Builder {
val builder = HttpClientBuilderFactory.create()
builder.readTimeout(configuration.network.readTimeout, TimeUnit.MILLISECONDS)
builder.connectTimeout(configuration.network.connectTimeout, TimeUnit.MILLISECONDS)
builder.proxy(createProxy(configuration.network.proxy))
builder.proxyAuthenticator(createProxyAuthenticator(configuration.network.proxy))
if (addInterceptor) {
createAuthenticateInterceptor(configuration.credentials)?.let { builder.addInterceptor(it) }
}
builder.retryOnConnectionFailure(true)
return builder
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
package com.tencent.bkrepo.common.artifact.util.http

import com.tencent.bkrepo.common.artifact.stream.Range
import org.slf4j.LoggerFactory
import org.springframework.http.HttpHeaders
import java.util.regex.Pattern
import javax.servlet.http.HttpServletRequest
Expand All @@ -42,6 +43,8 @@ import javax.servlet.http.HttpServletRequest
object HttpRangeUtils {

private val RANGE_HEADER_PATTERN = Pattern.compile("bytes=(\\d+)?-(\\d+)?")
private val CONTENT_RANGE_HEADER_PATTERN = Pattern.compile("bytes (\\d+)-(\\d+)/(\\d+)")
private val logger = LoggerFactory.getLogger(HttpRangeUtils::class.java)

/**
* 从[request]中解析Range,[total]代表总长度
Expand All @@ -63,4 +66,23 @@ object HttpRangeUtils {
Range(start, end, total)
}
}

/**
* 从[contentRange]中解析Range
*/
fun resolveContentRange(contentRange: String?): Range? {
if (contentRange.isNullOrEmpty()) {
return null
}
val matcher = CONTENT_RANGE_HEADER_PATTERN.matcher(contentRange)
return if (matcher.matches() && matcher.groupCount() == 3) {
val start = matcher.group(1).toLong()
val end = matcher.group(2).toLong()
val total = matcher.group(3).toLong()
Range(start, end, total)
} else {
logger.error("unknown range[$contentRange]")
null
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,13 @@

package com.tencent.bkrepo.fs.server

import com.tencent.bkrepo.common.artifact.pojo.RepositoryCategory
import com.tencent.bkrepo.fs.server.model.Node
import com.tencent.bkrepo.repository.pojo.node.NodeInfo
import org.slf4j.LoggerFactory
import java.time.Instant
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.format.DateTimeFormatter

fun NodeInfo.toNode(): Node {
Expand All @@ -50,3 +55,36 @@ fun NodeInfo.toNode(): Node {
lastAccessDate = this.lastAccessDate?.format(DateTimeFormatter.ISO_DATE_TIME)
)
}

fun Map<String, Any?>.toNode(): Node? {
return try {
Node(
createdBy = this[Node::createdBy.name] as String,
createdDate = (this[Node::createdDate.name] as String).format(DateTimeFormatter.ISO_DATE_TIME),
lastModifiedBy = this[Node::lastModifiedBy.name] as String,
lastModifiedDate = (this[Node::lastModifiedDate.name] as String).format(DateTimeFormatter.ISO_DATE_TIME),
projectId = this[Node::projectId.name] as String,
repoName = this[Node::repoName.name] as String,
folder = this[Node::folder.name] as Boolean,
path = this[Node::path.name] as String,
name = this[Node::name.name] as String,
fullPath = this[Node::fullPath.name] as String,
size = this[Node::size.name].toString().toLong(),
sha256 = this[Node::sha256.name] as String?,
md5 = this[Node::md5.name] as String?,
metadata = this[Node::metadata.name] as Map<String, Any>?,
lastAccessDate = (this[Node::lastAccessDate.name]?.toString()?.toLong())?.let { convertDateTime(it) },
category = (this[Node::category.name] as String?) ?: RepositoryCategory.LOCAL.name
)
} catch (e: Exception) {
logger.error("convert to node failed", e)
null
}
}

fun convertDateTime(value: Long): String {
return LocalDateTime.ofInstant(Instant.ofEpochMilli(value), ZoneId.systemDefault())
.format(DateTimeFormatter.ISO_DATE_TIME)
}

private val logger = LoggerFactory.getLogger("NodeExtensions")
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/*
* Tencent is pleased to support the open source community by making BK-CI 蓝鲸持续集成平台 available.
*
* Copyright (C) 2023 THL A29 Limited, a Tencent company. All rights reserved.
*
* BK-CI 蓝鲸持续集成平台 is licensed under the MIT license.
*
* A copy of the MIT License is included in this file.
*
*
* Terms of the MIT License:
* ---------------------------------------------------
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
* documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of
* the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
* LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
* NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

package com.tencent.bkrepo.fs.server.api

import com.tencent.bkrepo.common.api.constant.ensureSuffix
import com.tencent.bkrepo.common.artifact.pojo.RepositoryCategory
import com.tencent.bkrepo.common.artifact.pojo.RepositoryType
import com.tencent.bkrepo.common.artifact.pojo.configuration.composite.CompositeConfiguration
import com.tencent.bkrepo.fs.server.context.ReactiveArtifactContextHolder
import com.tencent.bkrepo.fs.server.model.Node
import com.tencent.bkrepo.fs.server.toNode
import com.tencent.bkrepo.repository.pojo.node.NodeDetail
import com.tencent.bkrepo.repository.pojo.node.NodeListOption
import com.tencent.bkrepo.repository.pojo.repo.RepositoryDetail
import com.tencent.bkrepo.repository.pojo.search.NodeQueryBuilder
import kotlinx.coroutines.reactor.awaitSingle
import kotlin.reflect.full.declaredMemberProperties

class NodeClient(
private val repositoryClient: RRepositoryClient,
private val genericClient: RGenericClient,
) {

suspend fun getNodeDetail(
projectId: String,
repo: RepositoryDetail,
fullPath: String,
category: String,
): NodeDetail? {
return if (category == RepositoryCategory.LOCAL.name) {
repositoryClient.getNodeDetail(
projectId = projectId,
repoName = repo.name,
fullPath = fullPath
).awaitSingle().data
} else if (repo.type == RepositoryType.GENERIC) {
genericClient.getNodeDetail(
projectId = projectId,
repoName = repo.name,
fullPath = fullPath
).awaitSingle().data
} else {
null
}
}


suspend fun listNodes(
projectId: String,
repo: RepositoryDetail,
path: String,
option: NodeListOption
): List<Node>? {
val nodes = if (ReactiveArtifactContextHolder.getRepoDetail().isLocalRepo()) {
repositoryClient.listNodePage(
path = path,
projectId = projectId,
repoName = repo.name,
option = option
).awaitSingle().data?.records?.map { it.toNode() }?.toList()
} else if (repo.type == RepositoryType.GENERIC) {
val builder = NodeQueryBuilder()
.page(option.pageNumber, option.pageSize)
.select(*select)
.projectId(projectId)
.repoName(repo.name)
.path(path.ensureSuffix("/"))
if (!option.includeFolder) {
builder.excludeFolder()
}
return genericClient.search(projectId, repo.name, builder.build()).awaitSingle().data
?.map { (it as Map<String, Any?>).toNode() }
?.toList()
?.filterNotNull()
} else {
null
}
return nodes
}

private fun RepositoryDetail.isLocalRepo(): Boolean {
val repoConfiguration = configuration
return category == RepositoryCategory.LOCAL ||
repoConfiguration is CompositeConfiguration && repoConfiguration.proxy.channelList.isEmpty()
}

companion object {
private val select = Node::class.declaredMemberProperties.map { it.name }.toTypedArray()
}
}
Loading

0 comments on commit 07d0061

Please sign in to comment.