上一篇我们讲了如何对路由策略进行配置,但是登录到服务器并执行命令,对于用户来说是一种非常不好的体验,那么本篇就来讲一下,如何对命令进行一系列的包装,以使得操作可以直观简便。
需要知道的是,在 K8S 体系下,我们可以将宿主机的命令和文件等挂载到 Pod 内,以便在 Pod 里面也可以访问到这些内容,或是执行宿主机上的命令,我们需要修改一下部署的 yaml:
apiVersion: v1
kind: Pod
metadata:
name: sample-service
labels:
app: sample-service
spec:
containers:
- name: sample-service-container
image: sample-service:v0
ports:
- containerPort: 9001
hostPort: 32001
imagePullPolicy: IfNotPresent
volumeMounts:
- mountPath: /usr/local/bin/kubectl
name: sample-service-kube
readOnly: true
- mountPath: /root/.kube/config
name: sample-service-config
readOnly: true
- mountPath: /usr/local/bin/istioctl
name: sample-service-istio
readOnly: true
volumes:
- hostPath:
path: /usr/bin/kubectl
name: sample-service-kube
- hostPath:
path: /home/rarnu/.kube/config
name: sample-service-config
- hostPath:
path: /usr/local/bin/istioctl
name: sample-service-istio
这里需要特别注意的是,必须挂载 ~/.kube/config
文件到 Pod 内,这个文件决定了 kubectl
命令的权限,如果没有这个文件,将不能在 Pod 内执行 kubectl
命令。
然后删去原先的 Pod 并重新创建之:
$ kubectl delete -f sample-service.yaml
$ kubectl apply -f sample-service.yaml
等 Pod 启动后,可以进入 Pod 内查看命令是否已挂载,并且尝试执行它:
$ kubectl exec -it sample-service sh
# which kubectl
/usr/local/bin/kubectl
# which istioctl
/usr/local/bin/istioctl
# kubectl get pods
此时可以发现,在 Pod 内拥有命令并且可以正常执行。
接下来只需要写一些代码就可以完成对路由的修改了:
data class ReqClusters(val clusters: List)
data class Response(val code: String, val message: String, val data: T? = null) {
companion object {
fun success(): Response<*> = Response("200", "succ", null)
fun fatal(): Response<*> = Response("500", "fail", null)
fun success(item: T): Response = Response("200", "succ", item)
fun fatal(item: T): Response = Response("500", "fail", item)
}
}
fun Routing.istioControllerRouting() {
post("/update") { param ->
addNewClusters(param.clusters)
call.respond(Response.success())
}
}
fun addNewClusters(list: List) {
if (list.isNotEmpty()) {
val script = when (list.size) {
1 -> " - destination:\n" +
" host: reviews\n" +
" subset: ${list[0]}"
2 -> " - destination:\n" +
" host: reviews\n" +
" subset: ${list[0]}\n" +
" weight: 50\n" +
" - destination:\n" +
" host: reviews\n" +
" subset: ${list[1]}\n" +
" weight: 50\n"
3 -> " - destination:\n" +
" host: reviews\n" +
" subset: ${list[0]}\n" +
" weight: 33\n" +
" - destination:\n" +
" host: reviews\n" +
" subset: ${list[1]}\n" +
" weight: 33\n" +
" - destination:\n" +
" host: reviews\n" +
" subset: ${list[2]}\n" +
" weight: 34\n"
else -> ""
}
val scriptText = ROUTE_TEMPLATE.format(script)
val destFile = File(SCRIPT_FILE_PATH, "script.yaml")
destFile.writeText(scriptText)
runCommand("kubectl apply -f ${destFile.absolutePath}").apply {
println("output: $output, error: $error")
}
}
}
private const val ROUTE_TEMPLATE = """
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: reviews
spec:
hosts:
- reviews
http:
- route:
%s
"""
然后只需要在前端写个简单的页面,请求一下接口即可:
这里你可能看到了,上面有展示当前的路由情况,这个内容获取也很容易:
fun getRouteCluster(): List {
val jsonStr = runCommand("istioctl pc route $POD_NAME --name $POD_PORT -o json").output.trim()
return parseRouteClusterList(jsonStr)
}
private fun parseRouteClusterList(jsonStr: String): List {
val ret = mutableListOf()
val jVirtualService = (JSONArray(jsonStr).first() as JSONObject).getJSONArray("virtualHosts").filter {
it as JSONObject
it.getString("name") == "reviews.default.svc.cluster.local:9080"
}.first() as JSONObject
val jRoute = (jVirtualService.getJSONArray("routes").first() as JSONObject).getJSONObject("route")
if (jRoute.has("cluster")) {
ret.add(jRoute.getString("cluster"))
} else if (jRoute.has("weightedClusters")) {
ret.addAll(
jRoute.getJSONObject("weightedClusters").getJSONArray("clusters").map {
it as JSONObject
it.getString("name")
})
}
return ret
}
看到这里你可能要说了,直接挂载并调用命令会产生不安全的情况,比如说命令被非法使用了,可能整个 K8S 体系都完蛋,所以接下去我们需要把相应的代码改成 SDK 调用的方式。
先来看如何获取当前的路由列表:
fun getRouteCluster(): List {
val istio: IstioClient = DefaultIstioClient()
val list = istio.v1alpha3VirtualService().list().items
return list.firstOrNull { it.spec.hosts[0] == "reviews" }?.spec?.http?.get(0)?.route?.map { it.destination.subset } ?: listOf()
}
然后再来看看如何新增路由,本质上新增路由的代码,就是将 yaml
内的代码改为用 SDK 方案来实现:
fun addRouteClusters() {
val vs23 = VirtualServiceBuilder()
.withApiVersion("networking.istio.io/v1alpha3")
.withKind("VirtualService")
.withNewMetadata()
.withName("reviews")
.withNamespace("default")
.endMetadata()
.withNewSpec()
.withHosts("reviews")
.addNewHttp()
.addNewRoute()
.withNewDestination()
.withHost("reviews")
.withSubset("v2")
.endDestination()
.withWeight(50)
.endRoute()
.addNewRoute()
.withNewDestination()
.withHost("reviews")
.withSubset("v3")
.endDestination()
.withWeight(50)
.endRoute()
.endHttp()
.endSpec()
.build()
val istio: IstioClient = DefaultIstioClient()
istio.istio.v1alpha3VirtualService().create(vs23)
}
不得不说,写起来确实有些麻烦,不过好在编程语言是灵活的,如此封装一下就好了:
fun makeVirtualService(name: String, namespace: String = "default", host: String, subsets: List>): VirtualService {
val http = VirtualServiceBuilder()
.withApiVersion("networking.istio.io/v1alpha3")
.withKind("VirtualService")
.withNewMetadata()
.withName(name)
.withNamespace(namespace)
.endMetadata()
.withNewSpec()
.withHosts(host)
.addNewHttp()
subsets.forEach { (subset, weight) ->
http
.addNewRoute()
.withNewDestination()
.withHost(host)
.withSubset(subset)
.endDestination()
.withWeight(weight)
.endRoute()
}
return http
.endHttp()
.endSpec()
.build()
}
最后还有必要提的是,Istio SDK
对于依赖版本有相当高的要求,经过测试,发现依赖必须如下配置:
compile 'me.snowdrop:istio-client:1.7.5-Beta2'
compile 'io.fabric8:kubernetes-client:4.10.2'
compile 'com.squareup.okhttp3:okhttp:3.12.12'
以上三个依赖项的版本号必须是如此,才能正常通过编译并正常工作,对于一些库本身就依赖了 okhttp
等的,需要进行 exclude
操作,如:
compile ("com.github.isyscore:common-ktor:1.3.1") {
exclude group: 'com.squareup.okhttp3', module: 'okhttp'
}